Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: scmStats #31959

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Expand Up @@ -19,10 +19,12 @@ 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';
import { getCache } from '../cache/repository';
import { hash } from '../hash';
import { newlineRegex, regEx } from '../regex';
import { parseGitAuthor } from './author';
import {
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) => hash(email))
.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
Loading