gitfamiliar 0.5.0 → 0.7.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/README.md +64 -73
- package/dist/bin/gitfamiliar.js +22 -163
- package/dist/bin/gitfamiliar.js.map +1 -1
- package/dist/{chunk-LZ67KNHF.js → chunk-BQCHOSLA.js} +14 -285
- package/dist/chunk-BQCHOSLA.js.map +1 -0
- package/dist/index.d.ts +2 -11
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LZ67KNHF.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
<a href="https://www.npmjs.com/package/gitfamiliar"><img src="https://img.shields.io/npm/v/gitfamiliar.svg" alt="npm version"></a>
|
|
8
8
|
<a href="https://www.npmjs.com/package/gitfamiliar"><img src="https://img.shields.io/npm/dm/gitfamiliar.svg" alt="npm downloads"></a>
|
|
9
9
|
<a href="https://github.com/kuze/gitfamiliar/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/gitfamiliar.svg" alt="license"></a>
|
|
10
|
-
<a href="https://github.com/kuze/gitfamiliar/actions"><img src="https://github.com/kuze/gitfamiliar/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
11
10
|
</p>
|
|
12
11
|
</p>
|
|
13
12
|
|
|
@@ -35,7 +34,7 @@ Overall: 58/172 files (34%)
|
|
|
35
34
|
tests/ ░░░░░░░░░░ 0% (0/14 files)
|
|
36
35
|
config/ ██████░░░░ 60% (3/5 files)
|
|
37
36
|
|
|
38
|
-
Written: 42 files
|
|
37
|
+
Written: 42 files
|
|
39
38
|
```
|
|
40
39
|
|
|
41
40
|
Use `--html` to generate an interactive treemap in the browser:
|
|
@@ -45,7 +44,7 @@ $ npx gitfamiliar --html
|
|
|
45
44
|
```
|
|
46
45
|
|
|
47
46
|
> Area = lines of code, Color = familiarity (red -> green).
|
|
48
|
-
> Click folders to drill down.
|
|
47
|
+
> Click folders to drill down.
|
|
49
48
|
|
|
50
49
|
## Quick Start
|
|
51
50
|
|
|
@@ -68,31 +67,21 @@ npm install -g gitfamiliar
|
|
|
68
67
|
| What it measures | How much you **wrote** | How well you **understand** |
|
|
69
68
|
| Metric | Lines / commits (cumulative) | Familiarity score (multi-signal) |
|
|
70
69
|
| Use case | Contribution stats | Onboarding progress |
|
|
71
|
-
| Review awareness | No | Yes (PR reviews count) |
|
|
72
70
|
| Time decay | No | Yes (knowledge fades) |
|
|
71
|
+
| Team analysis | No | Yes (bus factor, multi-user comparison) |
|
|
73
72
|
|
|
74
73
|
## Scoring Modes
|
|
75
74
|
|
|
76
|
-
GitFamiliar provides 4 modes so you can choose the right lens for your situation.
|
|
77
|
-
|
|
78
75
|
### Binary (default)
|
|
79
76
|
|
|
80
|
-
Files are "
|
|
77
|
+
Files are "written" or "not written". A file counts as written if you have at least one commit touching it.
|
|
81
78
|
|
|
82
79
|
```
|
|
83
|
-
familiarity =
|
|
80
|
+
familiarity = written_files / total_files
|
|
84
81
|
```
|
|
85
82
|
|
|
86
|
-
| View | Shows |
|
|
87
|
-
|---|---|
|
|
88
|
-
| All (default) | Written + Reviewed files |
|
|
89
|
-
| Written only | Only files you committed to |
|
|
90
|
-
| Reviewed only | Only files you reviewed via PR |
|
|
91
|
-
|
|
92
83
|
```bash
|
|
93
|
-
gitfamiliar #
|
|
94
|
-
gitfamiliar --filter written # Written only
|
|
95
|
-
gitfamiliar --filter reviewed # Reviewed only
|
|
84
|
+
gitfamiliar # default
|
|
96
85
|
```
|
|
97
86
|
|
|
98
87
|
> Best for: **New team members** tracking onboarding progress.
|
|
@@ -112,41 +101,24 @@ gitfamiliar --mode authorship
|
|
|
112
101
|
```
|
|
113
102
|
|
|
114
103
|
> Best for: **Tech leads** assessing bus factor and code ownership.
|
|
115
|
-
> A file where one person owns 95% of the lines is a risk signal.
|
|
116
|
-
|
|
117
|
-
### Review Coverage
|
|
118
|
-
|
|
119
|
-
Files you reviewed through PR approvals or comments, excluding your own commits.
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
score = reviewed_files / total_files
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
gitfamiliar --mode review-coverage
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
> Best for: **Senior engineers** tracking how broadly they review.
|
|
130
|
-
> Requires a GitHub token (see [GitHub Integration](#github-integration)).
|
|
131
104
|
|
|
132
105
|
### Weighted
|
|
133
106
|
|
|
134
|
-
|
|
107
|
+
Combines two signals with configurable weights and time decay:
|
|
135
108
|
|
|
136
109
|
```
|
|
137
|
-
familiarity = w1 x blame_score + w2 x commit_score
|
|
110
|
+
familiarity = w1 x blame_score + w2 x commit_score
|
|
138
111
|
```
|
|
139
112
|
|
|
140
|
-
Default weights: `blame=0.5, commit=0.
|
|
113
|
+
Default weights: `blame=0.5, commit=0.5`
|
|
141
114
|
|
|
142
115
|
Key features:
|
|
143
116
|
- **Sigmoid normalization** prevents a single large commit from dominating
|
|
144
117
|
- **Recency decay** (half-life: 180 days) models knowledge fading over time
|
|
145
|
-
- **Scope factor** discounts reviews on huge PRs (attention dilution)
|
|
146
118
|
|
|
147
119
|
```bash
|
|
148
120
|
gitfamiliar --mode weighted
|
|
149
|
-
gitfamiliar --mode weighted --weights "0.6,0.
|
|
121
|
+
gitfamiliar --mode weighted --weights "0.6,0.4" # custom weights
|
|
150
122
|
```
|
|
151
123
|
|
|
152
124
|
<details>
|
|
@@ -162,31 +134,65 @@ commit_score:
|
|
|
162
134
|
commit 2 (45 days ago, +5/-2): sigmoid(6/200) x decay(45d) = 0.09 x 0.84
|
|
163
135
|
total: min(1, 0.39) = 0.39
|
|
164
136
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
familiarity = 0.5 x 0.15 + 0.35 x 0.39 + 0.15 x 0.28
|
|
169
|
-
= 0.075 + 0.137 + 0.042
|
|
170
|
-
= 0.254 -> 25%
|
|
137
|
+
familiarity = 0.5 x 0.15 + 0.5 x 0.39
|
|
138
|
+
= 0.075 + 0.195
|
|
139
|
+
= 0.27 -> 27%
|
|
171
140
|
```
|
|
172
141
|
|
|
173
142
|
</details>
|
|
174
143
|
|
|
175
144
|
> Best for: **Power users** who want the most accurate picture.
|
|
176
|
-
|
|
145
|
+
|
|
146
|
+
## Team Features
|
|
147
|
+
|
|
148
|
+
### Multi-User Comparison
|
|
149
|
+
|
|
150
|
+
Compare familiarity across multiple team members:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
gitfamiliar --user "Alice" --user "Bob" # specific users
|
|
154
|
+
gitfamiliar --team # all contributors
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Team Coverage Map
|
|
158
|
+
|
|
159
|
+
Visualize bus factor — how many people know each part of the codebase:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
gitfamiliar --team-coverage
|
|
163
|
+
gitfamiliar --team-coverage --html
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Shows risk levels per folder:
|
|
167
|
+
- **RISK** (0-1 contributors) — single point of failure
|
|
168
|
+
- **MODERATE** (2-3 contributors) — some coverage
|
|
169
|
+
- **SAFE** (4+ contributors) — well-distributed knowledge
|
|
170
|
+
|
|
171
|
+
### Hotspot Analysis
|
|
172
|
+
|
|
173
|
+
Find files that are frequently changed but poorly understood:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
gitfamiliar --hotspot # personal hotspots
|
|
177
|
+
gitfamiliar --hotspot team # team hotspots
|
|
178
|
+
gitfamiliar --hotspot --window 30 # last 30 days only
|
|
179
|
+
gitfamiliar --hotspot --html # scatter plot visualization
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Risk = high change frequency x low familiarity.
|
|
177
183
|
|
|
178
184
|
## Expiration Policies
|
|
179
185
|
|
|
180
|
-
By default, "
|
|
186
|
+
By default, "written" status never expires. But real knowledge fades. Configure expiration to keep scores honest:
|
|
181
187
|
|
|
182
188
|
| Policy | Flag | What happens |
|
|
183
189
|
|---|---|---|
|
|
184
|
-
| Never | `--expiration never` | Once
|
|
190
|
+
| Never | `--expiration never` | Once written, always counted (default) |
|
|
185
191
|
| Time-based | `--expiration time:180d` | Expires 180 days after your last touch |
|
|
186
192
|
| Change-based | `--expiration change:50%` | Expires if 50%+ of the file changed since you last touched it |
|
|
187
193
|
| Combined | `--expiration combined:365d:50%` | Expires if **either** condition is met |
|
|
188
194
|
|
|
189
|
-
The change-based policy is the smartest: it detects when the code you
|
|
195
|
+
The change-based policy is the smartest: it detects when the code you wrote has been substantially rewritten, meaning your understanding is likely outdated.
|
|
190
196
|
|
|
191
197
|
## File Filtering
|
|
192
198
|
|
|
@@ -210,20 +216,6 @@ third_party/
|
|
|
210
216
|
**/migrations/
|
|
211
217
|
```
|
|
212
218
|
|
|
213
|
-
## GitHub Integration
|
|
214
|
-
|
|
215
|
-
For review-related features (Review Coverage mode, reviewed files in Binary mode), GitFamiliar needs a GitHub token:
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
# Option 1: environment variable
|
|
219
|
-
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxx
|
|
220
|
-
|
|
221
|
-
# Option 2: GitHub CLI (auto-detected if installed)
|
|
222
|
-
gh auth login
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Without a token, GitFamiliar works perfectly for all non-review features. Review features degrade gracefully with a helpful message.
|
|
226
|
-
|
|
227
219
|
## CLI Reference
|
|
228
220
|
|
|
229
221
|
```
|
|
@@ -231,15 +223,18 @@ Usage: gitfamiliar [options]
|
|
|
231
223
|
|
|
232
224
|
Options:
|
|
233
225
|
-m, --mode <mode> Scoring mode (default: "binary")
|
|
234
|
-
Choices: binary, authorship,
|
|
235
|
-
-u, --user <user> Git user name or email (
|
|
236
|
-
|
|
237
|
-
Choices: all, written, reviewed
|
|
226
|
+
Choices: binary, authorship, weighted
|
|
227
|
+
-u, --user <user> Git user name or email (repeatable for comparison)
|
|
228
|
+
Default: git config user.name
|
|
238
229
|
-e, --expiration <policy> Expiration policy (default: "never")
|
|
239
230
|
Examples: time:180d, change:50%, combined:365d:50%
|
|
240
231
|
--html Generate interactive HTML treemap report
|
|
241
|
-
-w, --weights <weights> Weights for weighted mode: blame,commit
|
|
242
|
-
Example: "0.
|
|
232
|
+
-w, --weights <weights> Weights for weighted mode: blame,commit
|
|
233
|
+
Example: "0.6,0.4" (must sum to 1.0)
|
|
234
|
+
--team Compare all contributors
|
|
235
|
+
--team-coverage Show team coverage map (bus factor analysis)
|
|
236
|
+
--hotspot [mode] Hotspot analysis: personal (default) or team
|
|
237
|
+
--window <days> Time window for hotspot analysis (default: 90)
|
|
243
238
|
-V, --version Output version number
|
|
244
239
|
-h, --help Display help
|
|
245
240
|
```
|
|
@@ -253,9 +248,8 @@ import { computeFamiliarity } from 'gitfamiliar';
|
|
|
253
248
|
|
|
254
249
|
const result = await computeFamiliarity({
|
|
255
250
|
mode: 'binary',
|
|
256
|
-
filter: 'all',
|
|
257
251
|
expiration: { policy: 'never' },
|
|
258
|
-
weights: { blame: 0.5, commit: 0.
|
|
252
|
+
weights: { blame: 0.5, commit: 0.5 },
|
|
259
253
|
html: false,
|
|
260
254
|
repoPath: '/path/to/repo',
|
|
261
255
|
});
|
|
@@ -267,7 +261,6 @@ console.log(`Score: ${Math.round(result.tree.score * 100)}%`);
|
|
|
267
261
|
|
|
268
262
|
- **Node.js** >= 18
|
|
269
263
|
- **Git** (available in PATH)
|
|
270
|
-
- **GitHub token** (optional, for review features)
|
|
271
264
|
|
|
272
265
|
## Contributing
|
|
273
266
|
|
|
@@ -284,8 +277,6 @@ npm test
|
|
|
284
277
|
## Roadmap
|
|
285
278
|
|
|
286
279
|
- [ ] **Dependency Awareness** - Factor in understanding of imported files
|
|
287
|
-
- [ ] **Churn Risk Alert** - Highlight files with high change frequency + low familiarity
|
|
288
|
-
- [ ] **GitHub Action** - Post familiarity reports as PR comments
|
|
289
280
|
- [ ] **VS Code Extension** - See familiarity scores inline in the editor
|
|
290
281
|
- [ ] **README Badge** - Codecov-style badge for your project README
|
|
291
282
|
|
package/dist/bin/gitfamiliar.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
GitClient,
|
|
4
|
-
GitHubClient,
|
|
5
4
|
buildFileTree,
|
|
6
5
|
computeFamiliarity,
|
|
7
6
|
createFilter,
|
|
8
7
|
parseExpirationConfig,
|
|
9
8
|
processBatch,
|
|
10
|
-
resolveGitHubToken,
|
|
11
9
|
resolveUser,
|
|
12
10
|
walkFiles
|
|
13
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-BQCHOSLA.js";
|
|
14
12
|
|
|
15
13
|
// src/cli/index.ts
|
|
16
14
|
import { Command } from "commander";
|
|
@@ -18,8 +16,7 @@ import { Command } from "commander";
|
|
|
18
16
|
// src/core/types.ts
|
|
19
17
|
var DEFAULT_WEIGHTS = {
|
|
20
18
|
blame: 0.5,
|
|
21
|
-
commit: 0.
|
|
22
|
-
review: 0.15
|
|
19
|
+
commit: 0.5
|
|
23
20
|
};
|
|
24
21
|
var DEFAULT_EXPIRATION = {
|
|
25
22
|
policy: "never"
|
|
@@ -28,7 +25,6 @@ var DEFAULT_EXPIRATION = {
|
|
|
28
25
|
// src/cli/options.ts
|
|
29
26
|
function parseOptions(raw, repoPath) {
|
|
30
27
|
const mode = validateMode(raw.mode || "binary");
|
|
31
|
-
const filter = validateFilter(raw.filter || "all");
|
|
32
28
|
let weights = DEFAULT_WEIGHTS;
|
|
33
29
|
if (raw.weights) {
|
|
34
30
|
weights = parseWeights(raw.weights);
|
|
@@ -52,7 +48,6 @@ function parseOptions(raw, repoPath) {
|
|
|
52
48
|
return {
|
|
53
49
|
mode,
|
|
54
50
|
user,
|
|
55
|
-
filter,
|
|
56
51
|
expiration,
|
|
57
52
|
html: raw.html || false,
|
|
58
53
|
weights,
|
|
@@ -60,18 +55,11 @@ function parseOptions(raw, repoPath) {
|
|
|
60
55
|
team: raw.team || false,
|
|
61
56
|
teamCoverage: raw.teamCoverage || false,
|
|
62
57
|
hotspot,
|
|
63
|
-
window: windowDays
|
|
64
|
-
githubUrl: raw.githubUrl,
|
|
65
|
-
checkGithub: raw.checkGithub || false
|
|
58
|
+
window: windowDays
|
|
66
59
|
};
|
|
67
60
|
}
|
|
68
61
|
function validateMode(mode) {
|
|
69
|
-
const valid = [
|
|
70
|
-
"binary",
|
|
71
|
-
"authorship",
|
|
72
|
-
"review-coverage",
|
|
73
|
-
"weighted"
|
|
74
|
-
];
|
|
62
|
+
const valid = ["binary", "authorship", "weighted"];
|
|
75
63
|
if (!valid.includes(mode)) {
|
|
76
64
|
throw new Error(
|
|
77
65
|
`Invalid mode: "${mode}". Valid modes: ${valid.join(", ")}`
|
|
@@ -79,27 +67,16 @@ function validateMode(mode) {
|
|
|
79
67
|
}
|
|
80
68
|
return mode;
|
|
81
69
|
}
|
|
82
|
-
function validateFilter(filter) {
|
|
83
|
-
const valid = ["all", "written", "reviewed"];
|
|
84
|
-
if (!valid.includes(filter)) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`Invalid filter: "${filter}". Valid filters: ${valid.join(", ")}`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
return filter;
|
|
90
|
-
}
|
|
91
70
|
function parseWeights(s) {
|
|
92
71
|
const parts = s.split(",").map(Number);
|
|
93
|
-
if (parts.length !==
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Invalid weights: "${s}". Expected format: "0.5,0.35,0.15"`
|
|
96
|
-
);
|
|
72
|
+
if (parts.length !== 2 || parts.some(isNaN)) {
|
|
73
|
+
throw new Error(`Invalid weights: "${s}". Expected format: "0.5,0.5"`);
|
|
97
74
|
}
|
|
98
|
-
const sum = parts[0] + parts[1]
|
|
75
|
+
const sum = parts[0] + parts[1];
|
|
99
76
|
if (Math.abs(sum - 1) > 0.01) {
|
|
100
77
|
throw new Error(`Weights must sum to 1.0, got ${sum}`);
|
|
101
78
|
}
|
|
102
|
-
return { blame: parts[0], commit: parts[1]
|
|
79
|
+
return { blame: parts[0], commit: parts[1] };
|
|
103
80
|
}
|
|
104
81
|
|
|
105
82
|
// src/cli/output/terminal.ts
|
|
@@ -125,8 +102,6 @@ function getModeLabel(mode) {
|
|
|
125
102
|
return "Binary mode";
|
|
126
103
|
case "authorship":
|
|
127
104
|
return "Authorship mode";
|
|
128
|
-
case "review-coverage":
|
|
129
|
-
return "Review Coverage mode";
|
|
130
105
|
case "weighted":
|
|
131
106
|
return "Weighted mode";
|
|
132
107
|
default:
|
|
@@ -166,7 +141,9 @@ function renderFolder(node, indent, mode, maxDepth) {
|
|
|
166
141
|
function renderTerminal(result) {
|
|
167
142
|
const { tree, repoName, mode } = result;
|
|
168
143
|
console.log("");
|
|
169
|
-
console.log(
|
|
144
|
+
console.log(
|
|
145
|
+
chalk.bold(`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)})`)
|
|
146
|
+
);
|
|
170
147
|
console.log("");
|
|
171
148
|
if (mode === "binary") {
|
|
172
149
|
const readCount = tree.readCount || 0;
|
|
@@ -183,10 +160,8 @@ function renderTerminal(result) {
|
|
|
183
160
|
}
|
|
184
161
|
console.log("");
|
|
185
162
|
if (mode === "binary") {
|
|
186
|
-
const { writtenCount
|
|
187
|
-
console.log(
|
|
188
|
-
`Written: ${writtenCount} files | Reviewed: ${reviewedCount} files | Both: ${bothCount} files`
|
|
189
|
-
);
|
|
163
|
+
const { writtenCount } = result;
|
|
164
|
+
console.log(`Written: ${writtenCount} files`);
|
|
190
165
|
console.log("");
|
|
191
166
|
}
|
|
192
167
|
}
|
|
@@ -244,28 +219,7 @@ function generateTreemapHTML(result) {
|
|
|
244
219
|
#breadcrumb span { cursor: pointer; color: #5eadf7; }
|
|
245
220
|
#breadcrumb span:hover { text-decoration: underline; }
|
|
246
221
|
#breadcrumb .sep { color: #666; margin: 0 4px; }
|
|
247
|
-
|
|
248
|
-
padding: 8px 24px;
|
|
249
|
-
background: #16213e;
|
|
250
|
-
border-bottom: 1px solid #0f3460;
|
|
251
|
-
display: flex;
|
|
252
|
-
gap: 12px;
|
|
253
|
-
align-items: center;
|
|
254
|
-
}
|
|
255
|
-
#controls button {
|
|
256
|
-
padding: 4px 12px;
|
|
257
|
-
border: 1px solid #0f3460;
|
|
258
|
-
background: #1a1a2e;
|
|
259
|
-
color: #e0e0e0;
|
|
260
|
-
border-radius: 4px;
|
|
261
|
-
cursor: pointer;
|
|
262
|
-
font-size: 12px;
|
|
263
|
-
}
|
|
264
|
-
#controls button.active {
|
|
265
|
-
background: #e94560;
|
|
266
|
-
border-color: #e94560;
|
|
267
|
-
color: white;
|
|
268
|
-
}
|
|
222
|
+
|
|
269
223
|
#treemap { width: 100%; }
|
|
270
224
|
#tooltip {
|
|
271
225
|
position: absolute;
|
|
@@ -306,13 +260,7 @@ function generateTreemapHTML(result) {
|
|
|
306
260
|
<div class="info">${mode.charAt(0).toUpperCase() + mode.slice(1)} mode | ${result.totalFiles} files</div>
|
|
307
261
|
</div>
|
|
308
262
|
<div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
|
|
309
|
-
|
|
310
|
-
<div id="controls">
|
|
311
|
-
<span style="font-size:12px;color:#888;">Filter:</span>
|
|
312
|
-
<button class="active" onclick="setFilter('all')">All</button>
|
|
313
|
-
<button onclick="setFilter('written')">Written only</button>
|
|
314
|
-
<button onclick="setFilter('reviewed')">Reviewed only</button>
|
|
315
|
-
</div>` : ""}
|
|
263
|
+
|
|
316
264
|
<div id="treemap"></div>
|
|
317
265
|
<div id="tooltip"></div>
|
|
318
266
|
<div id="legend">
|
|
@@ -325,7 +273,6 @@ ${mode === "binary" ? `
|
|
|
325
273
|
<script>
|
|
326
274
|
const rawData = ${dataJson};
|
|
327
275
|
const mode = "${mode}";
|
|
328
|
-
let currentFilter = 'all';
|
|
329
276
|
let currentPath = '';
|
|
330
277
|
|
|
331
278
|
function scoreColor(score) {
|
|
@@ -340,9 +287,6 @@ function scoreColor(score) {
|
|
|
340
287
|
}
|
|
341
288
|
|
|
342
289
|
function getNodeScore(node) {
|
|
343
|
-
if (mode !== 'binary') return node.score;
|
|
344
|
-
if (currentFilter === 'written') return node.isWritten ? 1 : 0;
|
|
345
|
-
if (currentFilter === 'reviewed') return node.isReviewed ? 1 : 0;
|
|
346
290
|
return node.score;
|
|
347
291
|
}
|
|
348
292
|
|
|
@@ -382,10 +326,8 @@ function render() {
|
|
|
382
326
|
|
|
383
327
|
const headerH = document.getElementById('header').offsetHeight;
|
|
384
328
|
const breadcrumbH = document.getElementById('breadcrumb').offsetHeight;
|
|
385
|
-
const controlsEl = document.getElementById('controls');
|
|
386
|
-
const controlsH = controlsEl ? controlsEl.offsetHeight : 0;
|
|
387
329
|
const width = window.innerWidth;
|
|
388
|
-
const height = window.innerHeight - headerH - breadcrumbH
|
|
330
|
+
const height = window.innerHeight - headerH - breadcrumbH;
|
|
389
331
|
|
|
390
332
|
const targetNode = currentPath ? findNode(rawData, currentPath) : rawData;
|
|
391
333
|
if (!targetNode) return;
|
|
@@ -492,9 +434,7 @@ function showTooltip(data, event) {
|
|
|
492
434
|
if (data.commitScore !== undefined) {
|
|
493
435
|
html += '<br>Commit: ' + Math.round(data.commitScore * 100) + '%';
|
|
494
436
|
}
|
|
495
|
-
|
|
496
|
-
html += '<br>Review: ' + Math.round(data.reviewScore * 100) + '%';
|
|
497
|
-
}
|
|
437
|
+
|
|
498
438
|
if (data.isExpired) {
|
|
499
439
|
html += '<br><span style="color:#e94560">Expired</span>';
|
|
500
440
|
}
|
|
@@ -523,14 +463,6 @@ function updateBreadcrumb() {
|
|
|
523
463
|
el.innerHTML = html;
|
|
524
464
|
}
|
|
525
465
|
|
|
526
|
-
function setFilter(f) {
|
|
527
|
-
currentFilter = f;
|
|
528
|
-
document.querySelectorAll('#controls button').forEach(btn => {
|
|
529
|
-
btn.classList.toggle('active', btn.textContent.toLowerCase().includes(f));
|
|
530
|
-
});
|
|
531
|
-
render();
|
|
532
|
-
}
|
|
533
|
-
|
|
534
466
|
window.addEventListener('resize', render);
|
|
535
467
|
render();
|
|
536
468
|
</script>
|
|
@@ -1162,7 +1094,6 @@ async function computeMultiUser(options) {
|
|
|
1162
1094
|
const userSummaries = results.map((r) => ({
|
|
1163
1095
|
user: { name: r.result.userName, email: "" },
|
|
1164
1096
|
writtenCount: r.result.writtenCount,
|
|
1165
|
-
reviewedCount: r.result.reviewedCount,
|
|
1166
1097
|
overallScore: r.result.tree.score
|
|
1167
1098
|
}));
|
|
1168
1099
|
return {
|
|
@@ -1199,8 +1130,7 @@ function mergeResults(results) {
|
|
|
1199
1130
|
scores.push({
|
|
1200
1131
|
user: { name: userName, email: "" },
|
|
1201
1132
|
score: file.score,
|
|
1202
|
-
isWritten: file.isWritten
|
|
1203
|
-
isReviewed: file.isReviewed
|
|
1133
|
+
isWritten: file.isWritten
|
|
1204
1134
|
});
|
|
1205
1135
|
});
|
|
1206
1136
|
}
|
|
@@ -1275,8 +1205,6 @@ function getModeLabel2(mode) {
|
|
|
1275
1205
|
return "Binary mode";
|
|
1276
1206
|
case "authorship":
|
|
1277
1207
|
return "Authorship mode";
|
|
1278
|
-
case "review-coverage":
|
|
1279
|
-
return "Review Coverage mode";
|
|
1280
1208
|
case "weighted":
|
|
1281
1209
|
return "Weighted mode";
|
|
1282
1210
|
default:
|
|
@@ -1323,7 +1251,7 @@ function renderMultiUserTerminal(result) {
|
|
|
1323
1251
|
const pct = formatPercent2(summary.overallScore);
|
|
1324
1252
|
if (mode === "binary") {
|
|
1325
1253
|
console.log(
|
|
1326
|
-
` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount
|
|
1254
|
+
` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount}/${totalFiles} files)`
|
|
1327
1255
|
);
|
|
1328
1256
|
} else {
|
|
1329
1257
|
console.log(` ${name} ${bar} ${pct.padStart(4)}`);
|
|
@@ -1332,9 +1260,7 @@ function renderMultiUserTerminal(result) {
|
|
|
1332
1260
|
console.log("");
|
|
1333
1261
|
const nameWidth = 20;
|
|
1334
1262
|
const headerNames = userSummaries.map((s) => truncateName(s.user.name, 7).padStart(7)).join(" ");
|
|
1335
|
-
console.log(
|
|
1336
|
-
chalk3.bold("Folders:") + " ".repeat(nameWidth - 4) + headerNames
|
|
1337
|
-
);
|
|
1263
|
+
console.log(chalk3.bold("Folders:") + " ".repeat(nameWidth - 4) + headerNames);
|
|
1338
1264
|
const folderLines = renderFolder3(tree, 1, 2, nameWidth);
|
|
1339
1265
|
for (const line of folderLines) {
|
|
1340
1266
|
console.log(line);
|
|
@@ -2149,58 +2075,6 @@ async function generateAndOpenHotspotHTML(result, repoPath) {
|
|
|
2149
2075
|
await openBrowser(outputPath);
|
|
2150
2076
|
}
|
|
2151
2077
|
|
|
2152
|
-
// src/github/check.ts
|
|
2153
|
-
async function checkGitHubConnection(repoPath, githubUrl) {
|
|
2154
|
-
const gitClient = new GitClient(repoPath);
|
|
2155
|
-
if (!await gitClient.isRepo()) {
|
|
2156
|
-
console.error("Error: Not a git repository.");
|
|
2157
|
-
process.exit(1);
|
|
2158
|
-
}
|
|
2159
|
-
const remoteUrl = await gitClient.getRemoteUrl();
|
|
2160
|
-
if (!remoteUrl) {
|
|
2161
|
-
console.error("Error: No git remote found.");
|
|
2162
|
-
process.exit(1);
|
|
2163
|
-
}
|
|
2164
|
-
console.log(`Remote URL: ${remoteUrl}`);
|
|
2165
|
-
const parsed = GitHubClient.parseRemoteUrl(remoteUrl, githubUrl);
|
|
2166
|
-
if (!parsed) {
|
|
2167
|
-
console.error("Error: Could not parse remote URL as a GitHub repository.");
|
|
2168
|
-
process.exit(1);
|
|
2169
|
-
}
|
|
2170
|
-
console.log(`Hostname: ${parsed.hostname}`);
|
|
2171
|
-
console.log(`Repository: ${parsed.owner}/${parsed.repo}`);
|
|
2172
|
-
console.log(`API Base URL: ${parsed.apiBaseUrl}`);
|
|
2173
|
-
console.log(`
|
|
2174
|
-
Resolving token for hostname: ${parsed.hostname}`);
|
|
2175
|
-
const token = resolveGitHubToken(parsed.hostname);
|
|
2176
|
-
if (!token) {
|
|
2177
|
-
console.error(
|
|
2178
|
-
`No GitHub token found.
|
|
2179
|
-
Tried:
|
|
2180
|
-
1. Environment variables: GITHUB_TOKEN, GH_TOKEN
|
|
2181
|
-
2. gh auth token --hostname ${parsed.hostname}
|
|
2182
|
-
` + (parsed.hostname !== "github.com" ? ` 3. gh auth token (default host fallback)
|
|
2183
|
-
` : "") + `
|
|
2184
|
-
Please run: gh auth login` + (parsed.hostname !== "github.com" ? ` --hostname ${parsed.hostname}` : "")
|
|
2185
|
-
);
|
|
2186
|
-
process.exit(1);
|
|
2187
|
-
}
|
|
2188
|
-
console.log(`Token: ****${token.slice(-4)}`);
|
|
2189
|
-
console.log("\nVerifying API connectivity...");
|
|
2190
|
-
try {
|
|
2191
|
-
const client = new GitHubClient(token, parsed.apiBaseUrl);
|
|
2192
|
-
const user = await client.verifyConnection();
|
|
2193
|
-
console.log(
|
|
2194
|
-
`Authenticated as: ${user.login}${user.name ? ` (${user.name})` : ""}`
|
|
2195
|
-
);
|
|
2196
|
-
console.log("\nGitHub connection OK.");
|
|
2197
|
-
} catch (error) {
|
|
2198
|
-
console.error(`
|
|
2199
|
-
API connection failed: ${error.message}`);
|
|
2200
|
-
process.exit(1);
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
2078
|
// src/cli/index.ts
|
|
2205
2079
|
function collect(value, previous) {
|
|
2206
2080
|
return previous.concat([value]);
|
|
@@ -2209,24 +2083,20 @@ function createProgram() {
|
|
|
2209
2083
|
const program2 = new Command();
|
|
2210
2084
|
program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version("0.1.1").option(
|
|
2211
2085
|
"-m, --mode <mode>",
|
|
2212
|
-
"Scoring mode: binary, authorship,
|
|
2086
|
+
"Scoring mode: binary, authorship, weighted",
|
|
2213
2087
|
"binary"
|
|
2214
2088
|
).option(
|
|
2215
2089
|
"-u, --user <user>",
|
|
2216
2090
|
"Git user name or email (repeatable for comparison)",
|
|
2217
2091
|
collect,
|
|
2218
2092
|
[]
|
|
2219
|
-
).option(
|
|
2220
|
-
"-f, --filter <filter>",
|
|
2221
|
-
"Filter mode: all, written, reviewed",
|
|
2222
|
-
"all"
|
|
2223
2093
|
).option(
|
|
2224
2094
|
"-e, --expiration <policy>",
|
|
2225
2095
|
"Expiration policy: never, time:180d, change:50%, combined:365d:50%",
|
|
2226
2096
|
"never"
|
|
2227
2097
|
).option("--html", "Generate HTML treemap report", false).option(
|
|
2228
2098
|
"-w, --weights <weights>",
|
|
2229
|
-
'Weights for weighted mode: blame,commit
|
|
2099
|
+
'Weights for weighted mode: blame,commit (e.g., "0.5,0.5")'
|
|
2230
2100
|
).option("--team", "Compare all contributors", false).option(
|
|
2231
2101
|
"--team-coverage",
|
|
2232
2102
|
"Show team coverage map (bus factor analysis)",
|
|
@@ -2234,21 +2104,10 @@ function createProgram() {
|
|
|
2234
2104
|
).option("--hotspot [mode]", "Hotspot analysis: personal (default) or team").option(
|
|
2235
2105
|
"--window <days>",
|
|
2236
2106
|
"Time window for hotspot analysis in days (default: 90)"
|
|
2237
|
-
).option(
|
|
2238
|
-
"--github-url <hostname>",
|
|
2239
|
-
"GitHub Enterprise hostname (e.g. ghe.example.com). Auto-detected from git remote if omitted."
|
|
2240
|
-
).option(
|
|
2241
|
-
"--check-github",
|
|
2242
|
-
"Verify GitHub API connectivity and show connection info",
|
|
2243
|
-
false
|
|
2244
2107
|
).action(async (rawOptions) => {
|
|
2245
2108
|
try {
|
|
2246
2109
|
const repoPath = process.cwd();
|
|
2247
2110
|
const options = parseOptions(rawOptions, repoPath);
|
|
2248
|
-
if (options.checkGithub) {
|
|
2249
|
-
await checkGitHubConnection(repoPath, options.githubUrl);
|
|
2250
|
-
return;
|
|
2251
|
-
}
|
|
2252
2111
|
if (options.hotspot) {
|
|
2253
2112
|
const result2 = await computeHotspots(options);
|
|
2254
2113
|
if (options.html) {
|