@rtorcato/repo-tooling 3.27.0 → 3.28.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.
|
@@ -149,3 +149,72 @@ export async function checkGitIdentity(dir, exec) {
|
|
|
149
149
|
hint: HINT,
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
|
+
const HISTORY_CHECK = 'Git author history';
|
|
153
|
+
/**
|
|
154
|
+
* Bounded so doctor stays cheap on every invocation; the output always names
|
|
155
|
+
* how many commits were actually examined so "0 found" is never read as "all
|
|
156
|
+
* history is clean".
|
|
157
|
+
*/
|
|
158
|
+
export const HISTORY_SCAN_LIMIT = 200;
|
|
159
|
+
/** How many offending commits to name; the count carries the rest. */
|
|
160
|
+
const HISTORY_SAMPLE = 3;
|
|
161
|
+
const HISTORY_HINT = 'These commits are already made: the address can never be verified as a secondary email, so they cannot be re-linked to a forge account without a history rewrite (#327). Fix the identity going forward (see the Git identity check) and treat the history as recorded loss.';
|
|
162
|
+
/**
|
|
163
|
+
* The retrospective half of `checkGitIdentity` (#557): that check reads the
|
|
164
|
+
* identity the *next* commit will use, this one scans recent history for
|
|
165
|
+
* commits already made with a bad one. Same classifier — `classifyGitEmail` —
|
|
166
|
+
* so the two can never drift. Same STATUS reasoning too: history is not
|
|
167
|
+
* something the repo's author can fix by editing the repo, so a finding is
|
|
168
|
+
* `optional-missing`, never `drift`/`missing`, and the exit code is untouched.
|
|
169
|
+
* Detection only — there is deliberately no fixer, because the only "fix" is a
|
|
170
|
+
* history rewrite no tool should offer unprompted.
|
|
171
|
+
*/
|
|
172
|
+
export async function checkGitIdentityHistory(dir, exec) {
|
|
173
|
+
if (!(await fs.pathExists(path.join(dir, '.git')))) {
|
|
174
|
+
return { check: HISTORY_CHECK, status: 'ok', detail: 'not a git repository' };
|
|
175
|
+
}
|
|
176
|
+
if (process.env.CI) {
|
|
177
|
+
return { check: HISTORY_CHECK, status: 'ok', detail: 'skipped on CI' };
|
|
178
|
+
}
|
|
179
|
+
const git = exec ?? ((args) => realGitExec(args, dir));
|
|
180
|
+
// A shallow clone has almost no history; scanning its stub and reporting
|
|
181
|
+
// "0 found" would be a false all-clear.
|
|
182
|
+
if ((await git(['rev-parse', '--is-shallow-repository'])) === 'true') {
|
|
183
|
+
return {
|
|
184
|
+
check: HISTORY_CHECK,
|
|
185
|
+
status: 'ok',
|
|
186
|
+
detail: 'shallow clone — commit history is not available to scan',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// %h %ae: abbreviated hash + author email, one commit per line.
|
|
190
|
+
const log = await git(['log', `-${HISTORY_SCAN_LIMIT}`, '--format=%h %ae']);
|
|
191
|
+
if (log === null || log === '') {
|
|
192
|
+
return { check: HISTORY_CHECK, status: 'ok', detail: 'no commits to scan' };
|
|
193
|
+
}
|
|
194
|
+
const commits = log.split('\n').map((line) => {
|
|
195
|
+
const sp = line.indexOf(' ');
|
|
196
|
+
return { hash: line.slice(0, sp), email: line.slice(sp + 1) };
|
|
197
|
+
});
|
|
198
|
+
const bad = commits.filter(({ email }) => {
|
|
199
|
+
const verdict = classifyGitEmail(email);
|
|
200
|
+
return verdict === 'placeholder' || verdict === 'generated';
|
|
201
|
+
});
|
|
202
|
+
const scanned = `the last ${commits.length} commit${commits.length === 1 ? '' : 's'}`;
|
|
203
|
+
if (bad.length === 0) {
|
|
204
|
+
return {
|
|
205
|
+
check: HISTORY_CHECK,
|
|
206
|
+
status: 'ok',
|
|
207
|
+
detail: `no placeholder or machine-derived author emails in ${scanned}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const sample = bad
|
|
211
|
+
.slice(0, HISTORY_SAMPLE)
|
|
212
|
+
.map(({ hash, email }) => `${hash} (${email})`)
|
|
213
|
+
.join(', ');
|
|
214
|
+
return {
|
|
215
|
+
check: HISTORY_CHECK,
|
|
216
|
+
status: 'optional-missing',
|
|
217
|
+
detail: `${bad.length} of ${scanned} carry a placeholder or machine-derived author email — e.g. ${sample}`,
|
|
218
|
+
hint: HISTORY_HINT,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -16,7 +16,7 @@ import { checkAgentUser } from '../../base/agent-user.js';
|
|
|
16
16
|
import { checkGitHubSettings } from '../../base/github-settings.js';
|
|
17
17
|
import { checkLoopLabels } from '../../base/labels.js';
|
|
18
18
|
import { checkMilestones } from '../../base/milestones.js';
|
|
19
|
-
import { checkGitIdentity } from '../../base/git-identity.js';
|
|
19
|
+
import { checkGitIdentity, checkGitIdentityHistory } from '../../base/git-identity.js';
|
|
20
20
|
import { checkCopiedAssets } from '../utils/copied-assets.js';
|
|
21
21
|
import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
|
|
22
22
|
import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
|
|
@@ -202,6 +202,8 @@ async function runBaseChecks(dir, lock, opts) {
|
|
|
202
202
|
results.push(await checkCopiedAssets(dir));
|
|
203
203
|
results.push(await checkNestedLanguages(dir, opts.language));
|
|
204
204
|
results.push(await checkGitIdentity(dir));
|
|
205
|
+
// The retrospective half (#557): commits already made with a bad identity.
|
|
206
|
+
results.push(await checkGitIdentityHistory(dir));
|
|
205
207
|
results.push(await checkEditorConfig(dir));
|
|
206
208
|
results.push(await checkFile(dir, COMMITLINT_FILE_CHECK));
|
|
207
209
|
if (opts.hooks) {
|
package/package.json
CHANGED