workproof 0.1.0 → 0.1.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 +25 -1
- package/README.tr.md +188 -0
- package/dist/src/analyse.d.ts +7 -1
- package/dist/src/analyse.js +11 -4
- package/dist/src/cli.js +10 -2
- package/dist/src/figures/identity.js +14 -4
- package/dist/src/git.d.ts +4 -0
- package/dist/src/git.js +19 -0
- package/dist/src/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<p align="center"><img src="assets/wordmark.svg" width="480" alt="workproof"></p>
|
|
2
2
|
|
|
3
|
+
<p align="center">English | <a href="README.tr.md">Türkçe</a></p>
|
|
4
|
+
|
|
3
5
|
<p align="center"><em>Your best work is in private repos. Prove it anyway.</em></p>
|
|
4
6
|
|
|
5
7
|
<p align="center">
|
|
@@ -119,6 +121,7 @@ workproof verify <report.json> [--repo <dir>]...
|
|
|
119
121
|
--repo <dir> repository to analyse (repeatable; several produce one combined report)
|
|
120
122
|
--since / --until override the tenure window
|
|
121
123
|
--sample <n> blame every n-th file (default: 1; 7 for very large repositories)
|
|
124
|
+
--max-commits <n> read only the newest n commits (escape hatch for enormous histories)
|
|
122
125
|
--depth <n> directory depth for ownership (default: 2)
|
|
123
126
|
--paths include directory paths
|
|
124
127
|
--emails include author emails
|
|
@@ -127,7 +130,28 @@ workproof verify <report.json> [--repo <dir>]...
|
|
|
127
130
|
--json print the JSON to stdout instead of writing files
|
|
128
131
|
```
|
|
129
132
|
|
|
130
|
-
A `.mailmap` in the repository merges an author's several addresses.
|
|
133
|
+
A `.mailmap` in the repository merges an author's several addresses. Progress lines go to
|
|
134
|
+
stderr while history is read and files are blamed, so a long run is visibly alive; on a
|
|
135
|
+
history of hundreds of thousands of commits, `--max-commits` bounds the read and the report
|
|
136
|
+
records that it did.
|
|
137
|
+
|
|
138
|
+
## Can it be gamed?
|
|
139
|
+
|
|
140
|
+
Partly, and the report is built so the gaming shows.
|
|
141
|
+
|
|
142
|
+
- Commit spam moves commit share and cadence, and nothing else. Surviving lines come from
|
|
143
|
+
`git blame` at HEAD, so a thousand empty commits add zero surviving lines, and the gap
|
|
144
|
+
between the two shares is printed side by side.
|
|
145
|
+
- Vendoring a library inflates lines added. The languages figure and the owned-directory
|
|
146
|
+
list (with `--paths`) show where those lines landed, and a reviewer sees a directory
|
|
147
|
+
named `vendor` or `node_modules` owning most of them.
|
|
148
|
+
- Rewriting history to change authorship changes the root commit or HEAD, so the
|
|
149
|
+
fingerprint and HEAD in an older report stop matching.
|
|
150
|
+
- The verifier runs against the same repository. A report that does not reproduce is worse
|
|
151
|
+
than no report, which is the incentive the tool relies on.
|
|
152
|
+
|
|
153
|
+
What it cannot catch: a genuinely large, low-value contribution. That is what references
|
|
154
|
+
are for.
|
|
131
155
|
|
|
132
156
|
## For candidates
|
|
133
157
|
|
package/README.tr.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<p align="center"><img src="assets/wordmark.svg" width="480" alt="workproof"></p>
|
|
2
|
+
|
|
3
|
+
<p align="center"><a href="README.md">English</a> | Türkçe</p>
|
|
4
|
+
|
|
5
|
+
<p align="center"><em>En iyi işin özel repolarda. Yine de kanıtla.</em></p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://img.shields.io/npm/v/workproof?style=flat-square&color=111111&label=npm" alt="npm">
|
|
9
|
+
<img src="https://img.shields.io/github/stars/Bubblegunn/workproof?style=flat-square&color=111111" alt="stars">
|
|
10
|
+
<img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
workproof, bir git deposunu tek bir yazar için doğrulanabilir bir mühendislik raporuna
|
|
14
|
+
çevirir; hiçbir kod göstermeden. Paylaşamadığınız depoda çalıştırırsınız. Okuyan kişi altı
|
|
15
|
+
figür, her birinin arkasındaki tam komutu, her birinin neyi gösteremediğini ve bir hash alır.
|
|
16
|
+
Aynı depoya sahip herkes `verify` çalıştırıp sayıların yeniden üretilip üretilmediğini görür.
|
|
17
|
+
|
|
18
|
+
## 30 saniye
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
cd your-private-repo
|
|
22
|
+
npx workproof
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Bu, `workproof-report.md` (bir özgeçmişe, portfolyoya, vize başvurusuna yapıştırın) ve
|
|
26
|
+
`workproof-report.json` (araçlar ve doğrulama için) dosyalarını yazar. Birinin raporunu
|
|
27
|
+
kontrol etmek için:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
npx workproof verify workproof-report.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Bir rapor neye benzer
|
|
34
|
+
|
|
35
|
+
Bu, [langchain-ai/openwiki](https://github.com/langchain-ai/openwiki) deposunun `1e6d54c`
|
|
36
|
+
sürümünde bir bakımcı için 5 Eylül 2026'da `--author "Colin Francis" --sample 5` ile alınan
|
|
37
|
+
gerçek çıktıdır; yollar ve e-postalar gizli (varsayılanlar):
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
## openwiki
|
|
41
|
+
|
|
42
|
+
HEAD 1e6d54cdfeec · fingerprint 82aa401bbba056f1 · identities: Colin Francis
|
|
43
|
+
|
|
44
|
+
### Tenure window
|
|
45
|
+
2026-07-06 to 2026-09-03 (60 days)
|
|
46
|
+
|
|
47
|
+
### Share of commits in tenure
|
|
48
|
+
71 of 295 non-merge commits, 24.1%
|
|
49
|
+
|
|
50
|
+
### Cadence
|
|
51
|
+
9 active weeks of 9, 7.9 commits per active week, longest streak 9 weeks
|
|
52
|
+
1 of 21 release tags in tenure
|
|
53
|
+
|
|
54
|
+
### Footprint
|
|
55
|
+
694 files touched
|
|
56
|
+
16 directories with a commit share at or above the threshold (paths hidden; run with --paths)
|
|
57
|
+
languages by lines added: TypeScript 80.4%, JSON 9.6%, Markdown 7.0%, JavaScript 1.6%, YAML 1.4%
|
|
58
|
+
|
|
59
|
+
### Tests and documentation
|
|
60
|
+
393 of 657 test-file changes, 59.8%
|
|
61
|
+
116 documents authored
|
|
62
|
+
|
|
63
|
+
### Surviving lines at HEAD
|
|
64
|
+
23,317 of 33,038 surviving lines, 70.6% (files 123/548, sample 1 in 5)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Her figürün altında rapor iki satır daha basar: onu üreten git komutuyla `How:` ve
|
|
68
|
+
`What this cannot show:`. Son bölüm `Integrity`: rapor hash'i ve depo parmak izi.
|
|
69
|
+
|
|
70
|
+
İki payı birlikte okuyun. Bu kişi penceresindeki commit'lerin %24,1'ini ve hâlâ yaşayan
|
|
71
|
+
satırların %70,6'sını yazmış. Bir commit sayısı onu küçük bir katkıcı olarak adlandırırdı. Bu
|
|
72
|
+
boşluk, iki yönde de, bir raporun birinin işi hakkında söyleyebileceği en dürüst şeydir.
|
|
73
|
+
|
|
74
|
+
## Ne ölçer
|
|
75
|
+
|
|
76
|
+
Altı figürün hepsi git'ten gelir, başka hiçbir şeyden değil.
|
|
77
|
+
|
|
78
|
+
| figür | ne | neyi gösteremez |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Görev penceresi | yazarın ilk ve son commit'i, ya da `--since/--until` | ilk commit'ten önceki ya da sonuncudan sonraki iş |
|
|
81
|
+
| Commit payı | penceredeki merge dışı commit'ler içinde yazarın merge dışı commit'leri | neyin sağ kaldığı; bir yazım hatası ile bir alt sistem aynı sayılır |
|
|
82
|
+
| Tempo | aktif haftalar, aktif hafta başına commit, en uzun seri, penceredeki sürüm etiketleri ve yazarınkiler | bir commit'lik hafta ile kırk commit'lik hafta ikisi de aktif sayılır |
|
|
83
|
+
| Ayak izi | dokunulan dosyalar, commit payı eşiğin üstündeki dizinler, eklenen satıra göre diller | üretilmiş ve vendored dosyalar commit'leyeni şişirir |
|
|
84
|
+
| Testler ve dokümanlar | test dosyası değişikliklerinin payı, yazılan dokümanlar | test vakaları, kapsam ya da bir dokümanın kalitesi |
|
|
85
|
+
| Hayatta kalan satırlar | HEAD'de yaşayan satırların payı, deterministik dosya örneği üzerinde `git blame -w -M`, [surviving-lines](https://github.com/Bubblegunn/surviving-lines) ile | liyakat; bilerek silinen kod kimseye sayılmaz |
|
|
86
|
+
|
|
87
|
+
## Doğrulama nasıl çalışır
|
|
88
|
+
|
|
89
|
+
- JSON deponun HEAD'ini, bir **parmak izi** (kök commit ve normalleştirilmiş remote adresinin
|
|
90
|
+
sha256'sı; depo adı verilmeden tanımlanır), kullanılan kimlik adlarını, `surviving-lines`
|
|
91
|
+
sürümünü, her parametreyi ve parametreler ile figürlerin bir **hash**'ini taşır.
|
|
92
|
+
- `workproof verify report.json` her figürü gösterdiğiniz depoda yeniden hesaplar ve bir
|
|
93
|
+
eşleşme tablosu basar. HEAD rapordan beri ilerlediyse bunu söyler ve hangi figürlerin
|
|
94
|
+
değiştiğini gösterir.
|
|
95
|
+
- Bir işe alım yöneticisinin iki şeye ihtiyacı vardır: rapor ve depoya okuma erişimi (ya da
|
|
96
|
+
şirket içinde tek komutu çalıştıracak bir çalışan). Depodan hiçbir şey çıkmaz.
|
|
97
|
+
|
|
98
|
+
## Gizlilik
|
|
99
|
+
|
|
100
|
+
- Kod içeriği, asla. Araç `git log --numstat` ve `git blame` okur ve sayılar üretir.
|
|
101
|
+
- Varsayılan olarak dosya yolu yok. `--paths`, yapılandırılan `--depth` (varsayılan 2)
|
|
102
|
+
derinliğinde dizin adları ekler, dosya asla.
|
|
103
|
+
- Varsayılan olarak e-posta adresi yok. `--emails` ekler; onsuz, yazdığınız `--author`
|
|
104
|
+
bile saklanan parametrelerde `(email hidden)` ile değiştirilir.
|
|
105
|
+
- İsteğe bağlı anlatı (`--narrate`) figürleri, yalnızca figürleri, seçtiğiniz bir model uç
|
|
106
|
+
noktasına gönderir (`WORKPROOF_API_URL`, `WORKPROOF_API_KEY`, `WORKPROOF_MODEL`;
|
|
107
|
+
OpenAI uyumlu ya da Anthropic). Paragraf "Generated narrative (not verified)" başlığı
|
|
108
|
+
altına eklenir ve hash'in dışındadır.
|
|
109
|
+
|
|
110
|
+
## Seçenekler
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
workproof [options] [--repo <dir>]...
|
|
114
|
+
workproof verify <report.json> [--repo <dir>]...
|
|
115
|
+
|
|
116
|
+
--author <email|name> identity to report on (repeatable; default: git config user.email)
|
|
117
|
+
--repo <dir> repository to analyse (repeatable; several produce one combined report)
|
|
118
|
+
--since / --until override the tenure window
|
|
119
|
+
--sample <n> blame every n-th file (default: 1; 7 for very large repositories)
|
|
120
|
+
--max-commits <n> read only the newest n commits (escape hatch for enormous histories)
|
|
121
|
+
--depth <n> directory depth for ownership (default: 2)
|
|
122
|
+
--paths include directory paths
|
|
123
|
+
--emails include author emails
|
|
124
|
+
--narrate append a model-written paragraph
|
|
125
|
+
--out <basename> output basename (default: workproof-report)
|
|
126
|
+
--json print the JSON to stdout instead of writing files
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Depodaki bir `.mailmap`, bir yazarın birden çok adresini birleştirir. Geçmiş okunurken ve
|
|
130
|
+
dosyalar blame'lenirken ilerleme satırları stderr'e gider, uzun bir çalıştırma canlı
|
|
131
|
+
görünür; yüz binlerce commit'lik bir geçmişte `--max-commits` okumayı sınırlar ve rapor bunu
|
|
132
|
+
kaydeder.
|
|
133
|
+
|
|
134
|
+
## Oyunlanabilir mi?
|
|
135
|
+
|
|
136
|
+
Kısmen; rapor, oyunun görünmesi için tasarlandı.
|
|
137
|
+
|
|
138
|
+
- Commit spam'i commit payını ve tempoyu oynatır, başka bir şeyi değil. Hayatta kalan satırlar
|
|
139
|
+
HEAD'deki `git blame`'den gelir; bin boş commit sıfır hayatta kalan satır ekler ve iki pay
|
|
140
|
+
yan yana basılır.
|
|
141
|
+
- Bir kütüphaneyi vendor'lamak eklenen satırları şişirir. Diller figürü ve (`--paths` ile)
|
|
142
|
+
sahiplenilen dizin listesi o satırların nereye düştüğünü gösterir; inceleyen, satırların
|
|
143
|
+
çoğunu `vendor` ya da `node_modules` adlı bir dizinin sahiplendiğini görür.
|
|
144
|
+
- Yazarlığı değiştirmek için geçmişi yeniden yazmak kök commit'i ya da HEAD'i değiştirir;
|
|
145
|
+
eski bir rapordaki parmak izi ve HEAD eşleşmez olur.
|
|
146
|
+
- Doğrulayıcı aynı depoya karşı çalışır. Yeniden üretilemeyen bir rapor, hiç rapor
|
|
147
|
+
olmamasından kötüdür; aracın dayandığı teşvik budur.
|
|
148
|
+
|
|
149
|
+
Yakalayamadığı: gerçekten büyük ama düşük değerli bir katkı. Referanslar bunun içindir.
|
|
150
|
+
|
|
151
|
+
## Adaylar için
|
|
152
|
+
|
|
153
|
+
Gurur duyduğunuz ve gösteremediğiniz her depoda çalıştırın. Markdown'ı portfolyonuza, zaten
|
|
154
|
+
yazacağınız cümlenin ("frontend'i ben yaptım") yanına koyun ve sayılar cümleyi taşısın. JSON'u
|
|
155
|
+
saklayın; inceleyenin doğruladığı odur.
|
|
156
|
+
|
|
157
|
+
## İşe alanlar için
|
|
158
|
+
|
|
159
|
+
JSON'u ve adayın eski şirketinden birinin üzerinde `npx workproof verify` çalıştırmasını
|
|
160
|
+
isteyin. Tablo ya yeniden üretilir ya üretilmez. Depo ilerlediyse araç hangi figürlerin
|
|
161
|
+
değiştiğini ve bunun neden beklendiğini söyler.
|
|
162
|
+
|
|
163
|
+
## Vize ve göç kanıtı için
|
|
164
|
+
|
|
165
|
+
workproof, en güçlü işin özel depolarda olduğu ve "bana güven"in kanıt sayılmadığı bir
|
|
166
|
+
Birleşik Krallık Global Talent başvurusu için yapıldı. Bir rapor, yöntemi ekli bir ölçümdür,
|
|
167
|
+
bir tavsiye değil; orada bulunmuş insanların mektuplarıyla birlikte kullanın.
|
|
168
|
+
|
|
169
|
+
## Bunu yapmaz
|
|
170
|
+
|
|
171
|
+
Hayatta kalmayı ve etkinliği ölçer; kaliteyi, incelemeyi, tasarımı ya da mentorluğu değil.
|
|
172
|
+
İnsanları sıralamaz. Referansların yerine geçmez. Hukuki bir belge değildir.
|
|
173
|
+
|
|
174
|
+
## Nereden geliyor
|
|
175
|
+
|
|
176
|
+
Yöntem
|
|
177
|
+
[How to show engineering ownership when the repositories are private](https://efe-genc-portfolio.vercel.app/writing/showing-ownership-private-repositories/)
|
|
178
|
+
yazısında anlatılıyor. Blame örneklemesi workproof'un tek bağımlılığı olan
|
|
179
|
+
[surviving-lines](https://github.com/Bubblegunn/surviving-lines) paketidir.
|
|
180
|
+
|
|
181
|
+
## Geliştirme
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
npm ci
|
|
185
|
+
npm test # tsc build, then node:test over the compiled tests (fixture repositories built in a temp dir)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
MIT.
|
package/dist/src/analyse.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface Params {
|
|
|
9
9
|
minCommits: number;
|
|
10
10
|
paths: boolean;
|
|
11
11
|
emails: boolean;
|
|
12
|
+
/** Escape hatch for enormous histories: read only the newest n commits. */
|
|
13
|
+
maxCommits?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface AnalyseHooks {
|
|
16
|
+
/** Called with short status lines while history is read and files are blamed. */
|
|
17
|
+
progress?: (message: string) => void;
|
|
12
18
|
}
|
|
13
19
|
export interface RepoReport {
|
|
14
20
|
name: string;
|
|
@@ -23,4 +29,4 @@ export interface RepoReport {
|
|
|
23
29
|
}
|
|
24
30
|
/** sha256 of the root commit and the normalised remote: identifies a repository without naming it. */
|
|
25
31
|
export declare function fingerprint(root: string, remote: string): string;
|
|
26
|
-
export declare function analyseRepo(cwd: string, params: Params): Promise<RepoReport>;
|
|
32
|
+
export declare function analyseRepo(cwd: string, params: Params, hooks?: AnalyseHooks): Promise<RepoReport>;
|
package/dist/src/analyse.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { listCommits, listTags, rootCommit, headSha, remoteUrl } from "./git.js";
|
|
4
|
+
import { listCommits, listTags, rootCommit, headSha, remoteUrl, assertRepository } from "./git.js";
|
|
5
5
|
import { resolveIdentity } from "./figures/identity.js";
|
|
6
6
|
import { tenure, commitShare } from "./figures/commits.js";
|
|
7
7
|
import { cadence } from "./figures/cadence.js";
|
|
@@ -22,8 +22,12 @@ const survivingVersion = () => {
|
|
|
22
22
|
return "unknown";
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
|
-
export async function analyseRepo(cwd, params) {
|
|
26
|
-
const
|
|
25
|
+
export async function analyseRepo(cwd, params, hooks = {}) {
|
|
26
|
+
const say = hooks.progress ?? (() => { });
|
|
27
|
+
await assertRepository(cwd);
|
|
28
|
+
say(`${basename(cwd)}: reading history${params.maxCommits ? ` (newest ${params.maxCommits} commits)` : ""}...`);
|
|
29
|
+
const all = await listCommits(cwd, params.maxCommits ? { max: params.maxCommits } : {});
|
|
30
|
+
say(`${basename(cwd)}: ${all.length.toLocaleString("en-US")} commits read`);
|
|
27
31
|
const id = await resolveIdentity(all, params.author, cwd);
|
|
28
32
|
const t = tenure(all, id, { ...(params.since ? { since: params.since } : {}), ...(params.until ? { until: params.until } : {}) });
|
|
29
33
|
const start = new Date(t.value.first + "T00:00:00Z");
|
|
@@ -41,8 +45,11 @@ export async function analyseRepo(cwd, params) {
|
|
|
41
45
|
cadence(inTenure, tags, id, { first: t.value.first, last: t.value.last }),
|
|
42
46
|
fp,
|
|
43
47
|
testsAndDocs(inTenure, id),
|
|
44
|
-
await survivingLines(cwd, id, { sample, version: survivingVersion() }),
|
|
45
48
|
];
|
|
49
|
+
say(`${basename(cwd)}: blaming files (1 in ${sample} sample)...`);
|
|
50
|
+
const surviving = await survivingLines(cwd, id, { sample, version: survivingVersion() });
|
|
51
|
+
say(`${basename(cwd)}: blamed ${surviving.value.filesSampled} of ${surviving.value.filesTotal} files`);
|
|
52
|
+
figures.push(surviving);
|
|
46
53
|
return {
|
|
47
54
|
name: basename(cwd),
|
|
48
55
|
head: await headSha(cwd),
|
package/dist/src/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ Turn a git repository into a verifiable engineering report for one author, witho
|
|
|
13
13
|
--repo <dir> repository to analyse (repeatable; default: current directory)
|
|
14
14
|
--since / --until override the tenure window (dates git understands)
|
|
15
15
|
--sample <n> blame every n-th file (default: 1, or 7 for very large repositories)
|
|
16
|
+
--max-commits <n> read only the newest n commits (escape hatch for enormous histories)
|
|
16
17
|
--depth <n> directory depth for ownership (default: 2)
|
|
17
18
|
--paths include directory paths in the report (off by default)
|
|
18
19
|
--emails include author emails in the report (off by default)
|
|
@@ -52,6 +53,11 @@ export function parse(argv) {
|
|
|
52
53
|
params.until = next();
|
|
53
54
|
else if (a === "--sample")
|
|
54
55
|
params.sample = Number(next());
|
|
56
|
+
else if (a === "--max-commits") {
|
|
57
|
+
params.maxCommits = Number(next());
|
|
58
|
+
if (!Number.isInteger(params.maxCommits) || params.maxCommits < 1)
|
|
59
|
+
throw new Error("--max-commits must be an integer >= 1");
|
|
60
|
+
}
|
|
55
61
|
else if (a === "--depth")
|
|
56
62
|
params.depth = Number(next());
|
|
57
63
|
else if (a === "--paths")
|
|
@@ -78,7 +84,9 @@ export function parse(argv) {
|
|
|
78
84
|
return { params, repos, out, json, doNarrate, verifyFile };
|
|
79
85
|
}
|
|
80
86
|
async function main() {
|
|
87
|
+
const started = Date.now();
|
|
81
88
|
const { params, repos, out, json, doNarrate, verifyFile } = parse(process.argv.slice(2));
|
|
89
|
+
const progress = (m) => process.stderr.write(`${m}\n`);
|
|
82
90
|
if (verifyFile) {
|
|
83
91
|
const report = JSON.parse(await readFile(verifyFile, "utf8"));
|
|
84
92
|
const result = await verifyReport(report, repos);
|
|
@@ -93,7 +101,7 @@ async function main() {
|
|
|
93
101
|
const version = createRequire(import.meta.url)("../../package.json").version;
|
|
94
102
|
const repositories = [];
|
|
95
103
|
for (const dir of repos)
|
|
96
|
-
repositories.push(await analyseRepo(dir, params));
|
|
104
|
+
repositories.push(await analyseRepo(dir, params, { progress }));
|
|
97
105
|
const report = buildReport(repositories, params, { version, generatedAt: new Date().toISOString() });
|
|
98
106
|
let narrative;
|
|
99
107
|
if (doNarrate) {
|
|
@@ -110,7 +118,7 @@ async function main() {
|
|
|
110
118
|
}
|
|
111
119
|
await writeFile(`${out}.json`, JSON.stringify(report, null, 2));
|
|
112
120
|
await writeFile(`${out}.md`, renderMarkdown(report, narrative));
|
|
113
|
-
console.log(`wrote ${out}.md and ${out}.json`);
|
|
121
|
+
console.log(`wrote ${out}.md and ${out}.json in ${((Date.now() - started) / 1000).toFixed(1)}s`);
|
|
114
122
|
}
|
|
115
123
|
const entry = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
|
|
116
124
|
if (entry === import.meta.url || entry.endsWith("/workproof")) {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { configuredEmail } from "../git.js";
|
|
1
|
+
import { configuredEmail, configuredName } from "../git.js";
|
|
2
2
|
/**
|
|
3
3
|
* Resolve the author to report on. `author` entries match mailmapped emails or
|
|
4
4
|
* names, case-insensitively; with none given, the repository's configured
|
|
5
5
|
* user.email is used.
|
|
6
6
|
*/
|
|
7
7
|
export async function resolveIdentity(commits, author, cwd) {
|
|
8
|
-
const
|
|
8
|
+
const explicit = author && author.length ? author : [];
|
|
9
|
+
// Without --author, try the configured email, then the configured name.
|
|
10
|
+
const wanted = (explicit.length ? explicit : [await configuredEmail(cwd), await configuredName(cwd)]).map((a) => a.toLowerCase()).filter(Boolean);
|
|
9
11
|
if (!wanted.length)
|
|
10
|
-
throw new Error(
|
|
12
|
+
throw new Error(`no author given and git config has no user.email or user.name; pass --author. ${authorsHint(commits)}`);
|
|
11
13
|
const emails = new Set();
|
|
12
14
|
const names = new Set();
|
|
13
15
|
for (const c of commits) {
|
|
@@ -17,7 +19,15 @@ export async function resolveIdentity(commits, author, cwd) {
|
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
if (!emails.size)
|
|
20
|
-
throw new Error(`no commits by ${wanted.join("
|
|
22
|
+
throw new Error(`no commits by ${wanted.join(" or ")} in this repository. ${authorsHint(commits)}`);
|
|
21
23
|
return { emails: [...emails].sort(), names: [...names].sort() };
|
|
22
24
|
}
|
|
25
|
+
/** The most frequent author names, so the error tells the user what to pass. */
|
|
26
|
+
function authorsHint(commits) {
|
|
27
|
+
const counts = new Map();
|
|
28
|
+
for (const c of commits)
|
|
29
|
+
counts.set(c.name, (counts.get(c.name) ?? 0) + 1);
|
|
30
|
+
const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, n]) => `"${name}" (${n})`);
|
|
31
|
+
return top.length ? `Authors here: ${top.join(", ")}. Pass one with --author.` : "The repository has no commits.";
|
|
32
|
+
}
|
|
23
33
|
export const isMine = (c, id) => id.emails.includes(c.email);
|
package/dist/src/git.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface Commit {
|
|
|
16
16
|
export declare function listCommits(cwd: string, opts: {
|
|
17
17
|
since?: string;
|
|
18
18
|
until?: string;
|
|
19
|
+
max?: number;
|
|
19
20
|
}): Promise<Commit[]>;
|
|
20
21
|
export interface Tag {
|
|
21
22
|
name: string;
|
|
@@ -26,4 +27,7 @@ export declare function listTags(cwd: string): Promise<Tag[]>;
|
|
|
26
27
|
export declare const rootCommit: (cwd: string) => Promise<string>;
|
|
27
28
|
export declare const headSha: (cwd: string) => Promise<string>;
|
|
28
29
|
export declare function remoteUrl(cwd: string): Promise<string>;
|
|
30
|
+
export declare function configuredName(cwd: string): Promise<string>;
|
|
31
|
+
/** Throws a plain sentence when the directory is not inside a git repository. */
|
|
32
|
+
export declare function assertRepository(cwd: string): Promise<void>;
|
|
29
33
|
export declare function configuredEmail(cwd: string): Promise<string>;
|
package/dist/src/git.js
CHANGED
|
@@ -12,6 +12,8 @@ export async function listCommits(cwd, opts) {
|
|
|
12
12
|
args.push(`--since=${opts.since}`);
|
|
13
13
|
if (opts.until)
|
|
14
14
|
args.push(`--until=${opts.until}`);
|
|
15
|
+
if (opts.max)
|
|
16
|
+
args.push(`--max-count=${opts.max}`);
|
|
15
17
|
const out = await git(args, cwd);
|
|
16
18
|
const commits = [];
|
|
17
19
|
for (const block of out.split("\x1e")) {
|
|
@@ -66,6 +68,23 @@ export async function remoteUrl(cwd) {
|
|
|
66
68
|
return "";
|
|
67
69
|
}
|
|
68
70
|
}
|
|
71
|
+
export async function configuredName(cwd) {
|
|
72
|
+
try {
|
|
73
|
+
return (await git(["config", "--get", "user.name"], cwd)).trim();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Throws a plain sentence when the directory is not inside a git repository. */
|
|
80
|
+
export async function assertRepository(cwd) {
|
|
81
|
+
try {
|
|
82
|
+
await git(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new Error(`${cwd} is not inside a git repository (use --repo to point at one)`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
69
88
|
export async function configuredEmail(cwd) {
|
|
70
89
|
try {
|
|
71
90
|
return (await git(["config", "--get", "user.email"], cwd)).trim().toLowerCase();
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { analyseRepo, fingerprint } from "./analyse.js";
|
|
2
|
-
export type { Params, RepoReport } from "./analyse.js";
|
|
2
|
+
export type { Params, RepoReport, AnalyseHooks } from "./analyse.js";
|
|
3
3
|
export { buildReport, renderMarkdown } from "./report.js";
|
|
4
4
|
export type { Report } from "./report.js";
|
|
5
5
|
export { verifyReport } from "./verify.js";
|
package/package.json
CHANGED