@atomist/sdm-pack-aspect
Version:
an Atomist SDM Extension Pack for visualizing drift across an organization
315 lines • 12 kB
JavaScript
;
/*
* 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.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const sdm_pack_fingerprint_1 = require("@atomist/sdm-pack-fingerprint");
const _ = require("lodash");
const codeMetrics_1 = require("../aspect/common/codeMetrics");
const reviewerAspect_1 = require("../aspect/common/reviewerAspect");
const codeOfConduct_1 = require("../aspect/community/codeOfConduct");
const license_1 = require("../aspect/community/license");
const globAspect_1 = require("../aspect/compose/globAspect");
const branchCount_1 = require("../aspect/git/branchCount");
const dateUtils_1 = require("../aspect/git/dateUtils");
const gitActivity_1 = require("../aspect/git/gitActivity");
const Score_1 = require("./Score");
const scoring_1 = require("./scoring");
exports.CommunityCategory = "community";
exports.CodeCategory = "code";
/**
* Use to anchor scores to penalize repositories about which we know little.
* Typically weighted > default x1
*/
function anchorScoreAt(score) {
const scoreFingerprints = () => __awaiter(this, void 0, void 0, function* () {
return {
reason: `Weight to ${score} stars to penalize repositories about which we know little`,
score,
};
});
return {
name: "anchor",
category: Score_1.AlwaysIncludeCategory,
scoreFingerprints,
};
}
exports.anchorScoreAt = anchorScoreAt;
/**
* 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.
*/
function requireRecentCommit(opts) {
const scoreFingerprints = (repo) => __awaiter(this, void 0, void 0, function* () {
const grt = repo.analysis.fingerprints.find(fp => fp.type === gitActivity_1.GitRecencyType);
if (!grt) {
return undefined;
}
if (!grt.data.lastCommitTime) {
return undefined;
}
const date = new Date(grt.data.lastCommitTime);
const days = dateUtils_1.daysSince(date);
return {
score: scoring_1.adjustBy((1 - days) / (opts.days || 1)),
reason: `Last commit ${days} days ago`,
};
});
return {
name: "recency",
scoreFingerprints,
baseOnly: true,
};
}
exports.requireRecentCommit = requireRecentCommit;
/**
* Limit languages used in a project
*/
function limitLanguages(opts) {
const scoreFingerprints = (repo) => __awaiter(this, void 0, void 0, function* () {
const cm = repo.analysis.fingerprints.find(codeMetrics_1.isCodeMetricsFingerprint);
if (!cm) {
return undefined;
}
return {
score: scoring_1.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,
};
}
exports.limitLanguages = limitLanguages;
/**
* 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?
*/
function limitLinesOfCode(opts) {
const scoreFingerprints = (repo) => __awaiter(this, void 0, void 0, function* () {
const cm = repo.analysis.fingerprints.find(codeMetrics_1.isCodeMetricsFingerprint);
if (!cm) {
return undefined;
}
return {
score: scoring_1.adjustBy(-cm.data.lines / opts.limit),
reason: `Found ${cm.data.lines} total lines of code`,
};
});
return {
name: "total-loc",
category: exports.CodeCategory,
scoreFingerprints,
baseOnly: opts.baseOnly,
};
}
exports.limitLinesOfCode = limitLinesOfCode;
/**
* Penalize repositories for having too many lines of code in the given language
*/
function limitLinesOfCodeIn(opts) {
const scoreFingerprints = (repo) => __awaiter(this, void 0, void 0, function* () {
const cm = repo.analysis.fingerprints.find(codeMetrics_1.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: scoring_1.adjustBy(((opts.freeAmount || 0) - targetLoc) / opts.limit),
reason: `Found ${targetLoc} lines of ${opts.language.name}`,
};
});
return {
name: `limit-${opts.language.name} (${opts.limit})`,
scoreFingerprints,
};
}
exports.limitLinesOfCodeIn = limitLinesOfCodeIn;
/**
* Penalize repositories for having an excessive number of git branches
*/
function penalizeForExcessiveBranches(opts) {
const scoreFingerprints = (repo) => __awaiter(this, void 0, void 0, function* () {
const branchCount = repo.analysis.fingerprints.find(f => f.type === branchCount_1.BranchCountType);
if (!branchCount) {
return undefined;
}
// You get the first 2 branches for free. After that they start to cost
const score = scoring_1.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: branchCount_1.BranchCountType,
scoreFingerprints,
baseOnly: true,
};
}
exports.penalizeForExcessiveBranches = penalizeForExcessiveBranches;
/**
* Penalize repositories for having more than 1 virtual project,
* as identified by a VirtualProjectFinder
*/
exports.PenalizeMonorepos = {
scoreFingerprints: (repo) => __awaiter(void 0, void 0, void 0, function* () {
const distinctPaths = _.uniq(repo.analysis.fingerprints.map(t => t.path)).length;
return {
score: scoring_1.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
*/
exports.PenalizeNoLicense = {
name: "require-license",
category: exports.CommunityCategory,
baseOnly: true,
scoreFingerprints: (repo) => __awaiter(void 0, void 0, void 0, function* () {
const license = repo.analysis.fingerprints.find(license_1.isLicenseFingerprint);
const bad = !license || license_1.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
*/
exports.PenalizeNoCodeOfConduct = requireAspectOfType({
type: codeOfConduct_1.CodeOfConductType,
category: exports.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
*/
function requireAspectOfType(opts) {
const scoreFingerprints = (repo) => __awaiter(this, void 0, void 0, function* () {
const found = repo.analysis.fingerprints.find(fp => fp.type === opts.type &&
(opts.data ? fp.sha === sdm_pack_fingerprint_1.sha256(JSON.stringify(opts.data)) : true));
const score = !!found ? 5 : 1;
return {
score,
reason: !found ? opts.reason : "Satisfactory",
};
});
return {
name: `${opts.type}-required`,
category: opts.category,
scoreFingerprints,
baseOnly: opts.baseOnly,
};
}
exports.requireAspectOfType = requireAspectOfType;
/**
* Penalize repositories without matches for the glob pattern.
* Depends on globAspect
*/
function requireGlobAspect(opts) {
const scoreFingerprints = (repo) => __awaiter(this, void 0, void 0, function* () {
const globs = repo.analysis.fingerprints.filter(globAspect_1.isGlobMatchFingerprint);
const found = globs
.filter(gf => gf.data.glob === opts.glob)
.filter(f => f.data.matches.length > 0);
const score = !!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,
};
}
exports.requireGlobAspect = requireGlobAspect;
/**
* Penalize for each point lost in these reviewers
*/
function penalizeForReviewViolations(opts) {
return {
name: opts.reviewerName,
scoreFingerprints: (repo) => __awaiter(this, void 0, void 0, function* () {
const found = reviewerAspect_1.findReviewCommentCountFingerprint(opts.reviewerName, repo.analysis.fingerprints);
if (!found) {
return undefined;
}
const score = scoring_1.adjustBy(-found.data.count / opts.violationsPerPointLost);
return {
score,
reason: `${found.data.count} review comments found for ${opts.reviewerName}`,
};
}),
};
}
exports.penalizeForReviewViolations = penalizeForReviewViolations;
/**
* Convenient function to emit scorers for all reviewers
*/
function penalizeForAllReviewViolations(opts) {
return opts.reviewerNames.map(reviewerName => penalizeForReviewViolations({
reviewerName,
violationsPerPointLost: opts.violationsPerPointLost,
}));
}
exports.penalizeForAllReviewViolations = penalizeForAllReviewViolations;
/**
* Use for a file pattern or something within files we don't want
*/
function penalizeGlobMatches(opts) {
const scoreFingerprints = (repo) => __awaiter(this, void 0, void 0, function* () {
const count = globAspect_1.countGlobMatches(repo.analysis.fingerprints, opts.type);
const score = scoring_1.adjustBy(-count * opts.pointsLostPerMatch);
return {
score,
reason: `${count} matches for glob typed ${opts.type}: Should have none`,
};
});
return {
name: opts.name || opts.type,
scoreFingerprints,
};
}
exports.penalizeGlobMatches = penalizeGlobMatches;
//# sourceMappingURL=commonScorers.js.map