Skip to content

Commit cede1f1

Browse files
author
Simon Renoult
committed
refactor: do all filters in a single loop + add missing test on obsolete files
1 parent af5cf92 commit cede1f1

File tree

5 files changed

+134
-93
lines changed

5 files changed

+134
-93
lines changed

src/io/index.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
11
import Cli from "./cli";
22
import Output from "./output";
3-
import { Path } from "../lib/types";
3+
import { Options, Path } from "../lib/types";
44
import Statistics from "../lib";
5+
import { execSync } from "child_process";
6+
import { existsSync } from "fs";
57

68
export default async function main(): Promise<void> {
79
const options = await Cli.parse();
810

11+
warnIfUsingComplexityWithIncompatibleFileTypes(options);
12+
assertGitIsInstalled();
13+
assertIsGitRootDirectory(options.directory);
14+
15+
const statistics: Map<Path, Statistics> = await Statistics.compute(options);
16+
Cli.cleanup(options);
17+
Output.render(statistics, options);
18+
}
19+
20+
function warnIfUsingComplexityWithIncompatibleFileTypes(options: Options) {
921
if (options.complexityStrategy !== "sloc") {
1022
console.warn(
1123
"Beware, the 'halstead' and 'cyclomatic' strategies are only available for JavaScript/TypeScript."
1224
);
1325
}
26+
}
27+
28+
function assertGitIsInstalled(): void {
29+
try {
30+
execSync("which git");
31+
} catch (error) {
32+
throw new Error("Program 'git' must be installed");
33+
}
34+
}
1435

15-
const statistics: Map<Path, Statistics> = await Statistics.compute(options);
16-
Cli.cleanup(options);
17-
Output.render(statistics, options);
36+
function assertIsGitRootDirectory(directory: string): void {
37+
if (!existsSync(`${directory}/.git`)) {
38+
throw new Error(`Argument 'dir' must be the git root directory.`);
39+
}
1840
}

src/lib/churn/churn.ts

Lines changed: 39 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { execSync } from "child_process";
2-
import { existsSync } from "fs";
32
import * as micromatch from "micromatch";
4-
import { resolve } from "path";
53

64
import { Options, Path } from "../types";
75
import { buildDebugger, withDuration } from "../../utils";
6+
import { resolve } from "path";
7+
import { existsSync } from "fs";
88

9-
type ParsedLine = { relativePath: string; commitCount: string };
109
const internal = { debug: buildDebugger("churn") };
1110
const PER_LINE = "\n";
1211

@@ -16,67 +15,19 @@ export default {
1615
};
1716

1817
async function compute(options: Options): Promise<Map<Path, number>> {
19-
assertGitIsInstalled();
20-
assertIsGitRootDirectory(options.directory);
21-
2218
const gitLogCommand = buildGitLogCommand(options);
23-
const rawStringOfAllChurns = executeGitLogCommand(gitLogCommand);
24-
const arrayOfAllChurns = computeChurnsPerFiles(
25-
rawStringOfAllChurns,
26-
options.directory
27-
);
28-
const mapOfAllChurns = createMapOfChurnsPerFile(arrayOfAllChurns);
29-
return applyUserFilters(mapOfAllChurns, options.filter);
30-
}
31-
32-
function applyUserFilters(
33-
allChurns: Map<Path, number>,
34-
filter: string[] | undefined
35-
): Map<Path, number> {
36-
const filteredChurns: Map<Path, number> = new Map();
37-
allChurns.forEach((churn: number, path: Path) => {
38-
const patchIsAMatch =
39-
filter && filter.length > 0
40-
? filter.every((f) => micromatch.isMatch(path, f))
41-
: true;
42-
43-
if (patchIsAMatch) filteredChurns.set(path, churn);
44-
});
45-
return filteredChurns;
46-
}
47-
48-
function createMapOfChurnsPerFile(
49-
numberOfTimesFilesChanged: ParsedLine[]
50-
): Map<Path, number> {
51-
return numberOfTimesFilesChanged.reduce(
52-
(map: Map<Path, number>, { relativePath, commitCount }: ParsedLine) => {
53-
const path: Path = relativePath;
54-
const churn = parseInt(commitCount, 10);
55-
map.set(path, churn);
56-
return map;
57-
},
58-
new Map()
19+
const singleStringWithAllChurns = executeGitLogCommand(gitLogCommand);
20+
return computeChurnsPerFiles(
21+
singleStringWithAllChurns,
22+
options.directory,
23+
options.filter
5924
);
6025
}
6126

6227
function executeGitLogCommand(gitLogCommand: string): string {
6328
return execSync(gitLogCommand, { encoding: "utf8", maxBuffer: 32_000_000 });
6429
}
6530

66-
function assertGitIsInstalled(): void {
67-
try {
68-
execSync("which git");
69-
} catch (error) {
70-
throw new Error("Program 'git' must be installed");
71-
}
72-
}
73-
74-
function assertIsGitRootDirectory(directory: string): void {
75-
if (!existsSync(`${directory}/.git`)) {
76-
throw new Error(`Argument 'dir' must be the git root directory.`);
77-
}
78-
}
79-
8031
function buildGitLogCommand(options: Options): string {
8132
const isWindows = process.platform === "win32";
8233

@@ -102,30 +53,42 @@ function buildGitLogCommand(options: Options): string {
10253

10354
function computeChurnsPerFiles(
10455
gitLogOutput: string,
105-
directory: string
106-
): ParsedLine[] {
56+
directory: string,
57+
filters: string[] | undefined
58+
): Map<Path, number> {
10759
const changedFiles = gitLogOutput
10860
.split(PER_LINE)
10961
.filter((line) => line !== "")
11062
.sort();
11163

112-
const changedFilesCount = changedFiles.reduce(
113-
(fileAndTimeChanged: { [fileName: string]: number }, fileName) => {
114-
fileAndTimeChanged[fileName] = (fileAndTimeChanged[fileName] || 0) + 1;
115-
return fileAndTimeChanged;
116-
},
117-
{}
118-
);
64+
return changedFiles.reduce((map: Map<Path, number>, path) => {
65+
applyFiltersAndExcludeObsoletePath(path, map);
66+
return map;
67+
}, new Map());
68+
69+
function applyFiltersAndExcludeObsoletePath(
70+
path: string,
71+
map: Map<Path, number>
72+
) {
73+
if (!filters || !filters.length) {
74+
if (pathStillExists(path)) {
75+
addOrIncrement(map, path);
76+
}
77+
} else {
78+
const pathHasAMatch = filters.every((f) => micromatch.isMatch(path, f));
79+
if (pathHasAMatch) {
80+
if (pathStillExists(path)) {
81+
addOrIncrement(map, path);
82+
}
83+
}
84+
}
85+
}
11986

120-
return Object.keys(changedFilesCount)
121-
.map(
122-
(changedFileName) =>
123-
({
124-
relativePath: changedFileName,
125-
commitCount: changedFilesCount[changedFileName].toString(),
126-
} as ParsedLine)
127-
)
128-
.filter((parsedLine: ParsedLine) => {
129-
return existsSync(resolve(directory, parsedLine.relativePath));
130-
});
87+
function addOrIncrement(map: Map<Path, number>, path: string) {
88+
map.set(path, (map.get(path) ?? 0) + 1);
89+
}
90+
91+
function pathStillExists(fileName: string) {
92+
return existsSync(resolve(directory, fileName));
93+
}
13194
}

test/fixtures/test-repository.fixture.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default class TestRepositoryFixture {
1515
commits?: number;
1616
lines?: number;
1717
date?: string;
18+
removed?: boolean;
1819
}[] = [];
1920

2021
constructor() {
@@ -27,6 +28,7 @@ export default class TestRepositoryFixture {
2728
commits?: number;
2829
lines?: number;
2930
date?: string;
31+
removed?: boolean;
3032
}): this {
3133
this.files.push(args);
3234
return this;
@@ -44,6 +46,7 @@ export default class TestRepositoryFixture {
4446
.withName(file.name)
4547
.containing(file.content ?? { lines: file.lines ?? 1 })
4648
.committed({ times: file.commits ?? 1, date: file.date })
49+
.isRemoved(file.removed ?? false)
4750
.writeOnDisk();
4851
});
4952

test/fixtures/versioned-file.fixture.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default class VersionedFileFixture {
1010
private numberOfCommitsForFile = 10;
1111
private content?: string;
1212
private commitDate?: string;
13+
private removed = false;
1314

1415
withName(name: string): VersionedFileFixture {
1516
this.name = name;
@@ -31,6 +32,11 @@ export default class VersionedFileFixture {
3132
return this;
3233
}
3334

35+
isRemoved(value: boolean): VersionedFileFixture {
36+
this.removed = value;
37+
return this;
38+
}
39+
3440
writeOnDisk(): void {
3541
for (let i = 0; i < this.numberOfCommitsForFile; i++) {
3642
if (i === 0) {
@@ -41,6 +47,10 @@ export default class VersionedFileFixture {
4147
}
4248
this.commitFile(i);
4349
}
50+
51+
if (this.removed) {
52+
this.removeAndCommit();
53+
}
4454
}
4555

4656
private commitFile(commitNumber: number): void {
@@ -58,7 +68,7 @@ export default class VersionedFileFixture {
5868

5969
private modifyFileWithoutChangingItsLength(commitNumber: number): void {
6070
appendFileSync(
61-
`${this.repositoryLocation}${sep}${this.name}`,
71+
`${this.getFileLocation()}`,
6272
`// change for commit #${commitNumber + 1} `
6373
);
6474
}
@@ -71,10 +81,28 @@ export default class VersionedFileFixture {
7181
.map((value, index) => `console.log(${index});`)
7282
.join("\n");
7383

74-
writeFileSync(`${this.repositoryLocation}${sep}${this.name}`, fileContent);
84+
writeFileSync(`${this.getFileLocation()}`, fileContent);
7585
}
7686

7787
private addFileToRepository(): void {
7888
execSync(`git -C ${this.repositoryLocation} add --all`);
7989
}
90+
91+
private removeAndCommit() {
92+
const message = `"${this.name}: removed"`;
93+
const commands = [
94+
`git -C ${this.repositoryLocation} rm ${this.getFileLocation()}`,
95+
`git -C ${this.repositoryLocation} commit --message=${message}`,
96+
].join("&&");
97+
try {
98+
execSync(commands);
99+
} catch (e: any) {
100+
console.log(e.stdout.toString());
101+
throw e;
102+
}
103+
}
104+
105+
private getFileLocation(): string {
106+
return `${this.repositoryLocation}${sep}${this.name}`;
107+
}
80108
}

0 commit comments

Comments
 (0)