UNPKG

@atomist/sdm-pack-aspect

Version:

an Atomist SDM Extension Pack for visualizing drift across an organization

329 lines (310 loc) 11 kB
/* * Copyright © 2019 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { sha256 } from "@atomist/sdm-pack-fingerprint"; import { Language } from "@atomist/sdm-pack-sloc/lib/slocReport"; import * as _ from "lodash"; import { RepositoryScorer } from "../aspect/AspectRegistry"; import { isCodeMetricsFingerprint } from "../aspect/common/codeMetrics"; import { findReviewCommentCountFingerprint } from "../aspect/common/reviewerAspect"; import { CodeOfConductType } from "../aspect/community/codeOfConduct"; import { hasNoLicense, isLicenseFingerprint, } from "../aspect/community/license"; import { countGlobMatches, isGlobMatchFingerprint, } from "../aspect/compose/globAspect"; import { BranchCountType } from "../aspect/git/branchCount"; import { daysSince } from "../aspect/git/dateUtils"; import { GitRecencyType } from "../aspect/git/gitActivity"; import { AlwaysIncludeCategory, FiveStar, } from "./Score"; import { adjustBy } from "./scoring"; export const CommunityCategory: string = "community"; export const CodeCategory: string = "code"; /** * Use to anchor scores to penalize repositories about which we know little. * Typically weighted > default x1 */ export function anchorScoreAt(score: FiveStar): RepositoryScorer { const scoreFingerprints = async () => { return { reason: `Weight to ${score} stars to penalize repositories about which we know little`, score, }; }; return { name: "anchor", category: AlwaysIncludeCategory, scoreFingerprints, }; } /** * Penalize repositories for not having a recent commit. * days is the number of days since the latest commit to the default branch * that will cost 1 star. */ export function requireRecentCommit(opts: { days: number }): RepositoryScorer { const scoreFingerprints = async repo => { const grt = repo.analysis.fingerprints.find(fp => fp.type === GitRecencyType); if (!grt) { return undefined; } if (!grt.data.lastCommitTime) { return undefined; } const date = new Date(grt.data.lastCommitTime); const days = daysSince(date); return { score: adjustBy((1 - days) / (opts.days || 1)), reason: `Last commit ${days} days ago`, }; }; return { name: "recency", scoreFingerprints, baseOnly: true, }; } /** * Limit languages used in a project */ export function limitLanguages(opts: { limit: number, baseOnly?: boolean }): RepositoryScorer { const scoreFingerprints = async repo => { const cm = repo.analysis.fingerprints.find(isCodeMetricsFingerprint); if (!cm) { return undefined; } return { score: adjustBy(opts.limit - cm.data.languages.length), reason: `Found ${cm.data.languages.length} languages: ${cm.data.languages.map(l => l.language.name).join(",")}`, }; }; return { name: "multi-language", scoreFingerprints, baseOnly: opts.baseOnly, }; } /** * Penalize repositories for having too many lines of code. * The limit is the number of lines of code required to drop one star. * The chosen limit will depend on team preferences: For example, * are we trying to do microservices? */ export function limitLinesOfCode(opts: { limit: number, baseOnly?: boolean }): RepositoryScorer { const scoreFingerprints = async repo => { const cm = repo.analysis.fingerprints.find(isCodeMetricsFingerprint); if (!cm) { return undefined; } return { score: adjustBy(-cm.data.lines / opts.limit), reason: `Found ${cm.data.lines} total lines of code`, }; }; return { name: "total-loc", category: CodeCategory, scoreFingerprints, baseOnly: opts.baseOnly, }; } /** * Penalize repositories for having too many lines of code in the given language */ export function limitLinesOfCodeIn(opts: { limit: number, language: Language, freeAmount?: number }): RepositoryScorer { const scoreFingerprints = async repo => { const cm = repo.analysis.fingerprints.find(isCodeMetricsFingerprint); if (!cm) { return undefined; } const target = cm.data.languages.find(l => l.language.name === opts.language.name); const targetLoc = target ? target.total : 0; return { score: adjustBy(((opts.freeAmount || 0) - targetLoc) / opts.limit), reason: `Found ${targetLoc} lines of ${opts.language.name}`, }; }; return { name: `limit-${opts.language.name} (${opts.limit})`, scoreFingerprints, }; } /** * Penalize repositories for having an excessive number of git branches */ export function penalizeForExcessiveBranches(opts: { branchLimit: number }): RepositoryScorer { const scoreFingerprints = async repo => { const branchCount = repo.analysis.fingerprints.find(f => f.type === BranchCountType); if (!branchCount) { return undefined; } // You get the first 2 branches for free. After that they start to cost const score = adjustBy(-(branchCount.data.count - 2) / opts.branchLimit); return branchCount ? { score, reason: `${branchCount.data.count} branches: Should not have more than ${opts.branchLimit}`, } : undefined; }; return { name: BranchCountType, scoreFingerprints, baseOnly: true, }; } /** * Penalize repositories for having more than 1 virtual project, * as identified by a VirtualProjectFinder */ export const PenalizeMonorepos: RepositoryScorer = { scoreFingerprints: async repo => { const distinctPaths = _.uniq(repo.analysis.fingerprints.map(t => t.path)).length; return { score: adjustBy(1 - distinctPaths / 2), reason: distinctPaths > 1 ? `${distinctPaths} virtual projects: Prefer one project per repository` : "Single project in repository", }; }, name: "monorepo", scoreAll: true, }; /** * Penalize repositories without a license file */ export const PenalizeNoLicense: RepositoryScorer = { name: "require-license", category: CommunityCategory, baseOnly: true, scoreFingerprints: async repo => { const license = repo.analysis.fingerprints.find(isLicenseFingerprint); const bad = !license || hasNoLicense(license.data); return { score: bad ? 1 : 5, reason: bad ? "Repositories should have a license" : "Repository has a license", }; }, }; /** * Penalize repositories without a code of conduct file */ export const PenalizeNoCodeOfConduct: RepositoryScorer = requireAspectOfType({ type: CodeOfConductType, category: CommunityCategory, reason: "Repos should have a code of conduct", baseOnly: true, }); /** * Penalize repositories that don't have this type of aspect. * If data is provided, check that the sha matches the default sha-ing of this * data payload */ export function requireAspectOfType(opts: { type: string, reason: string, data?: any, category?: string, baseOnly?: boolean, }): RepositoryScorer { const scoreFingerprints = async repo => { const found = repo.analysis.fingerprints.find(fp => fp.type === opts.type && (opts.data ? fp.sha === sha256(JSON.stringify(opts.data)) : true)); const score: FiveStar = !!found ? 5 : 1; return { score, reason: !found ? opts.reason : "Satisfactory", }; }; return { name: `${opts.type}-required`, category: opts.category, scoreFingerprints, baseOnly: opts.baseOnly, }; } /** * Penalize repositories without matches for the glob pattern. * Depends on globAspect */ export function requireGlobAspect(opts: { glob: string, category?: string, baseOnly?: boolean }): RepositoryScorer { const scoreFingerprints = async repo => { const globs = repo.analysis.fingerprints.filter(isGlobMatchFingerprint); const found = globs .filter(gf => gf.data.glob === opts.glob) .filter(f => f.data.matches.length > 0); const score: FiveStar = !!found ? 5 : 1; return { score, reason: !found ? `Should have file for ${opts.glob}` : "Satisfactory", }; }; return { name: `${opts.glob}-required`, category: opts.category, scoreFingerprints, baseOnly: opts.baseOnly, }; } /** * Penalize for each point lost in these reviewers */ export function penalizeForReviewViolations(opts: { reviewerName: string, violationsPerPointLost: number }): RepositoryScorer { return { name: opts.reviewerName, scoreFingerprints: async repo => { const found = findReviewCommentCountFingerprint(opts.reviewerName, repo.analysis.fingerprints); if (!found) { return undefined; } const score = adjustBy(-found.data.count / opts.violationsPerPointLost); return { score, reason: `${found.data.count} review comments found for ${opts.reviewerName}`, }; }, }; } /** * Convenient function to emit scorers for all reviewers */ export function penalizeForAllReviewViolations(opts: { reviewerNames: string[], violationsPerPointLost: number }): RepositoryScorer[] { return opts.reviewerNames.map(reviewerName => penalizeForReviewViolations({ reviewerName, violationsPerPointLost: opts.violationsPerPointLost, })); } /** * Use for a file pattern or something within files we don't want */ export function penalizeGlobMatches(opts: { name?: string, type: string, pointsLostPerMatch: number}): RepositoryScorer { const scoreFingerprints = async repo => { const count = countGlobMatches(repo.analysis.fingerprints, opts.type); const score = adjustBy(-count * opts.pointsLostPerMatch); return { score, reason: `${count} matches for glob typed ${opts.type}: Should have none`, }; }; return { name: opts.name || opts.type, scoreFingerprints, }; }