@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.27.0",
3
+ "version": "3.28.0",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [