Skip to content

Commit

Permalink
feat: scmStats
Browse files Browse the repository at this point in the history
  • Loading branch information
rarkins committed Oct 14, 2024
1 parent 9bd101c commit 8ad3ad6
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 3 deletions.
6 changes: 5 additions & 1 deletion lib/modules/platform/default-scm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as git from '../../util/git';
import type { CommitFilesConfig, LongCommitSha } from '../../util/git/types';
import type { PlatformScm } from './types';
import type { PlatformScm, ScmStats } from './types';

export class DefaultGitScm implements PlatformScm {
branchExists(branchName: string): Promise<boolean> {
Expand Down Expand Up @@ -48,4 +48,8 @@ export class DefaultGitScm implements PlatformScm {
mergeToLocal(branchName: string): Promise<void> {
return git.mergeToLocal(branchName);
}

getStats(): Promise<ScmStats | null> {
return git.getStats();
}
}
6 changes: 5 additions & 1 deletion lib/modules/platform/local/scm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { execSync } from 'node:child_process';
import { glob } from 'glob';
import { logger } from '../../../logger';
import type { CommitFilesConfig, LongCommitSha } from '../../../util/git/types';
import type { PlatformScm } from '../types';
import type { PlatformScm, ScmStats } from '../types';

let fileList: string[] | undefined;
export class LocalFs implements PlatformScm {
Expand Down Expand Up @@ -59,4 +59,8 @@ export class LocalFs implements PlatformScm {
mergeToLocal(branchName: string): Promise<void> {
return Promise.resolve();
}

getStats(): Promise<ScmStats | null> {
return Promise.resolve(null);
}
}
9 changes: 9 additions & 0 deletions lib/modules/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,14 @@ export interface Platform {
maxBodyLength(): number;
}

export interface ScmStats {
defaultBranchSha: string;
last90Days: {
committerHashList: string[];
renovateCommitCount: number;
};
}

export interface PlatformScm {
isBranchBehindBase(branchName: string, baseBranch: string): Promise<boolean>;
isBranchModified(branchName: string, baseBranch: string): Promise<boolean>;
Expand All @@ -294,4 +302,5 @@ export interface PlatformScm {
checkoutBranch(branchName: string): Promise<LongCommitSha>;
mergeToLocal(branchName: string): Promise<void>;
mergeAndPush(branchName: string): Promise<void>;
getStats(): Promise<ScmStats | null>;
}
17 changes: 17 additions & 0 deletions lib/util/cache/repository/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
UpdateType,
} from '../../../config/types';
import type { PackageFile } from '../../../modules/manager/types';
import type { ScmStats } from '../../../modules/platform';
import type { RepoInitConfig } from '../../../workers/repository/init/types';
import type { PrBlockedBy } from '../../../workers/types';

Expand Down Expand Up @@ -42,6 +43,21 @@ export interface OnboardingBranchCache {
configFileParsed?: string;
}

export interface RepoStats {
scm: ScmStats;
/*
renovatePrs?: {
counts: {
open: number;
closed: number;
merged: number;
};
lastMerged?: string;
};
stargazerCount?: number;
*/
}

export interface ReconfigureBranchCache {
reconfigureBranchSha: string;
isConfigValid: boolean;
Expand Down Expand Up @@ -149,6 +165,7 @@ export interface RepoCacheData {
prComments?: Record<number, Record<string, string>>;
onboardingBranchCache?: OnboardingBranchCache;
reconfigureBranchCache?: ReconfigureBranchCache;
repoStats?: RepoStats;
}

export interface RepoCache {
Expand Down
79 changes: 79 additions & 0 deletions lib/util/git/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import crypto from 'crypto';
import URL from 'node:url';
import { setTimeout } from 'timers/promises';
import is from '@sindresorhus/is';
Expand All @@ -19,6 +20,7 @@ import {
TEMPORARY_ERROR,
} from '../../constants/error-messages';
import { logger } from '../../logger';
import type { ScmStats } from '../../modules/platform/types';
import { ExternalHostError } from '../../types/errors/external-host-error';
import type { GitProtocol } from '../../types/git';
import { incLimitedValue } from '../../workers/global/limits';
Expand Down Expand Up @@ -1370,3 +1372,80 @@ export async function listCommitTree(
}
return result;
}

function parseGitShortlog(inputText: string): Map<string, number> {
const emailMap = new Map();

if (!inputText.trim().length) {
return emailMap;
}

// Split the input text into individual lines
const lines = inputText.trim().split('\n');

// Iterate over each line using "for...of"
for (const line of lines) {
// Match the pattern "<count> <name> <email>" or "<count> <email>"
const match = line.match(/^\s*(\d+)\s+.*?<(.+?)>|^\s*(\d+)\s+(.+@.+)$/);

if (match) {
// If the line matches, extract the count and the email
const count = parseInt(match[1] || match[3], 10);
const email = match[2] || match[4];

// Add or update the email's commit count in the map
if (emailMap.has(email)) {
emailMap.set(email, emailMap.get(email) + count);
} else {
emailMap.set(email, count);
}
} else {
logger.once.warn('Failed to parse shortlog line');
}
}

return emailMap;
}

export async function getStats(): Promise<ScmStats | null> {
if (!config.defaultBranch) {
logger.warn('No default branch found');
return null;
}
const defaultBranchSha = getBranchCommit(config.defaultBranch);
if (!defaultBranchSha) {
logger.warn('No default branch sha found');
return null;
}
await syncGit();
await resetToBranch(config.defaultBranch);
logger.debug('Checking git committers in last 90 days');
const rawCommitters = await git.raw([
'shortlog',
'-sne',
'--since="last 90 days"',
'HEAD',
]);
logger.trace({ rawCommitters }, 'rawCommitters');
const emailMap = parseGitShortlog(rawCommitters);
// Find Renovate's email address and commit count
let renovateCommitCount = 0;
if (config.gitAuthorEmail) {
renovateCommitCount = emailMap.get(config.gitAuthorEmail) ?? 0;
}
// Convert each email to a base64-encoded SHA256 hash and produce a sorted list,
// excluding Renovate's email address
const committerHashList = Array.from(emailMap.keys())
.filter((email) => email !== config.gitAuthorEmail)
.map((email) => crypto.createHash('sha256').update(email).digest('base64'))
.sort();

const scmStats: ScmStats = {
defaultBranchSha,
last90Days: {
committerHashList,
renovateCommitCount,
},
};
return scmStats;
}
24 changes: 23 additions & 1 deletion lib/workers/repository/finalize/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { RenovateConfig } from '../../../config/types';
import { logger } from '../../../logger';
import { platform } from '../../../modules/platform';
import { scm } from '../../../modules/platform/scm';
import * as repositoryCache from '../../../util/cache/repository';
import { clearRenovateRefs } from '../../../util/git';
import { clearRenovateRefs, getBranchCommit } from '../../../util/git';
import { PackageFiles } from '../package-files';
import { validateReconfigureBranch } from '../reconfigure';
import { pruneStaleBranches } from './prune';
Expand All @@ -11,12 +12,33 @@ import {
runRenovateRepoStats,
} from './repository-statistics';

export async function calculateScmStats(config: RenovateConfig): Promise<void> {
const defaultBranchSha = getBranchCommit(config.defaultBranch!);
// istanbul ignore if: shouldn't happen
if (!defaultBranchSha) {
logger.debug('No default branch sha found');
}
const repoCache = repositoryCache.getCache();
if (repoCache.repoStats?.scm.defaultBranchSha === defaultBranchSha) {
logger.debug('Default branch sha unchanged - scm stats are up to date');
} else {
logger.debug('Recalculating repo scm stats');
const repoStats = await scm.getStats();
if (repoStats) {
repoCache.repoStats = { scm: repoStats };
} else {
logger.debug(`Could not calcualte repo stats`);
}
}
}

// istanbul ignore next
export async function finalizeRepo(
config: RenovateConfig,
branchList: string[],
): Promise<void> {
await validateReconfigureBranch(config);
await calculateScmStats(config);
await repositoryCache.saveCache();
await pruneStaleBranches(config, branchList);
await ensureIssuesClosing();
Expand Down
7 changes: 7 additions & 0 deletions lib/workers/repository/finalize/repository-statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export function runRenovateRepoStats(
}
}
logger.debug({ stats: prStats }, `Renovate repository PR statistics`);
const repoCache = getCache();
const scmStats = repoCache?.repoStats?.scm?.last90Days;
if (scmStats) {
logger.debug(
`Repository has ${scmStats.renovateCommitCount} Renovate commits in the last 90 days plus ${scmStats.committerHashList?.length} other committers`,
);
}
}

function branchCacheToMetadata({
Expand Down

0 comments on commit 8ad3ad6

Please sign in to comment.