code-complexity
Version:
Measure the churn/complexity score. Higher values mean hotspots where refactorings should happen.
96 lines (80 loc) • 2.63 kB
text/typescript
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import * as micromatch from "micromatch";
import { buildDebugger, withDuration } from "../../utils";
import { Options, Path } from "../types";
const internal = { debug: buildDebugger("churn") };
const PER_LINE = "\n";
export default {
compute: (...args: any[]): Promise<Map<Path, number>> =>
withDuration(compute, args, internal.debug),
};
async function compute(options: Options): Promise<Map<Path, number>> {
const gitLogCommand = buildGitLogCommand(options);
const singleStringWithAllChurns = executeGitLogCommand(gitLogCommand);
return computeChurnsPerFiles(
singleStringWithAllChurns,
options.directory,
options.filter
);
}
function executeGitLogCommand(gitLogCommand: string): string {
return execSync(gitLogCommand, { encoding: "utf8", maxBuffer: 32_000_000 });
}
function buildGitLogCommand(options: Options): string {
const isWindows = process.platform === "win32";
return [
"git",
`-C ${options.directory}`,
`log`,
`--follow`,
// Windows CMD handle quotes differently than linux, this is why we should put empty string as said in:
// https://github.com/git-for-windows/git/issues/3131
`--format=${isWindows ? "" : "''"}`,
`--name-only`,
options.since ? `--since="${options.since}"` : "",
options.until ? `--until="${options.until}"` : "",
// Windows CMD handle quotes differently
isWindows ? "*" : "'*'",
]
.filter((s) => s.length > 0)
.join(" ");
}
function computeChurnsPerFiles(
gitLogOutput: string,
directory: string,
filters: string[] | undefined
): Map<Path, number> {
const changedFiles = gitLogOutput
.split(PER_LINE)
.filter((line) => line !== "")
.sort();
return changedFiles.reduce((map: Map<Path, number>, path) => {
applyFiltersAndExcludeObsoletePath(path, map);
return map;
}, new Map());
function applyFiltersAndExcludeObsoletePath(
path: string,
map: Map<Path, number>
) {
if (!filters || !filters.length) {
if (pathStillExists(path)) {
addOrIncrement(map, path);
}
} else {
const pathHasAMatch = filters.every((f) => micromatch.isMatch(path, f));
if (pathHasAMatch) {
if (pathStillExists(path)) {
addOrIncrement(map, path);
}
}
}
}
function addOrIncrement(map: Map<Path, number>, path: string) {
map.set(path, (map.get(path) ?? 0) + 1);
}
function pathStillExists(fileName: string) {
return existsSync(resolve(directory, fileName));
}
}