Skip to content

Commit 0e07635

Browse files
committed
feat: allow limiting forensic analyses by commit cound and time
1 parent 9c240f5 commit 0e07635

7 files changed

Lines changed: 162 additions & 127 deletions

File tree

backend/src/express.ts

Lines changed: 124 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -9,118 +9,131 @@ import { calcTeamAlignment } from "./services/team-alignment";
99
import { aggregateHotspots, findHotspotFiles } from "./services/hotspot";
1010
import { calcChangeCoupling } from "./services/change-coupling";
1111
import { Options } from "./options/options";
12+
import { Limits } from "./model/limit";
1213

1314
export function setupExpress(options: Options) {
14-
const app = express();
15-
16-
app.use(express.json());
17-
18-
app.get("/api/config", (req, res) => {
19-
res.sendFile(path.join(cwd(), options.config));
20-
});
21-
22-
app.post("/api/config", (req, res) => {
23-
const newConfig = req.body;
24-
const configPath = path.join(cwd(), options.config);
25-
26-
fs.writeFile(
27-
configPath,
28-
JSON.stringify(newConfig, null, 2),
29-
"utf8",
30-
(error) => {
31-
if (error) {
32-
return res.status(500).json({ error });
33-
}
34-
res.json({});
15+
const app = express();
16+
17+
app.use(express.json());
18+
19+
app.get("/api/config", (req, res) => {
20+
res.sendFile(path.join(cwd(), options.config));
21+
});
22+
23+
app.post("/api/config", (req, res) => {
24+
const newConfig = req.body;
25+
const configPath = path.join(cwd(), options.config);
26+
27+
fs.writeFile(
28+
configPath,
29+
JSON.stringify(newConfig, null, 2),
30+
"utf8",
31+
(error) => {
32+
if (error) {
33+
return res.status(500).json({ error });
3534
}
36-
);
37-
});
38-
39-
app.get("/api/modules", (req, res) => {
40-
try {
41-
const result = calcModuleInfo(options);
42-
res.json(result);
43-
} catch (e: any) {
44-
console.log("error", e);
45-
res.status(500).json({ message: e.message });
46-
}
47-
});
48-
49-
app.get("/api/folders", (req, res) => {
50-
try {
51-
const result = inferFolders(options);
52-
res.json(result);
53-
} catch (e: any) {
54-
console.log("error", e);
55-
res.status(500).json({ message: e.message });
56-
}
57-
});
58-
59-
app.get("/api/coupling", (req, res) => {
60-
try {
61-
const result = calcCoupling(options);
62-
res.json(result);
63-
} catch (e: any) {
64-
console.log("error", e);
65-
res.status(500).json({ message: e.message });
66-
}
67-
});
68-
69-
app.get("/api/change-coupling", async (req, res) => {
70-
try {
71-
const result = await calcChangeCoupling(options);
72-
res.json(result);
73-
} catch (e: any) {
74-
console.log("error", e);
75-
res.status(500).json({ message: e.message });
35+
res.json({});
7636
}
77-
});
78-
79-
app.get("/api/team-alignment", async (req, res) => {
80-
const byUser = Boolean(req.query.byUser);
81-
82-
try {
83-
const result = await calcTeamAlignment(byUser, options);
84-
res.json(result);
85-
} catch (e: any) {
86-
console.log("error", e);
87-
res.status(500).json({ message: e.message });
88-
}
89-
});
90-
91-
app.get("/api/hotspots/aggregated", async (req, res) => {
92-
const minScore = Number(req.query.minScore) || -1;
93-
const criteria = { minScore, module: "" };
94-
95-
try {
96-
const result = await aggregateHotspots(criteria, options);
97-
res.json(result);
98-
} catch (e: any) {
99-
console.log("error", e);
100-
res.status(500).json({ message: e.message });
101-
}
102-
});
103-
104-
app.get("/api/hotspots", async (req, res) => {
105-
const minScore = Number(req.query.minScore) || -1;
106-
const module = req.query.module ? String(req.query.module) : "";
107-
108-
const criteria = { minScore, module };
109-
110-
try {
111-
const result = await findHotspotFiles(criteria, options);
112-
res.json(result);
113-
} catch (e: any) {
114-
console.log("error", e);
115-
res.status(500).json({ message: e.message });
116-
}
117-
});
118-
119-
app.use(express.static(path.join(__dirname, "..", "public")));
120-
121-
app.get("*", (req, res) => {
122-
res.sendFile(path.join(__dirname, "..", "public", "index.html"));
123-
});
124-
return app;
125-
}
126-
37+
);
38+
});
39+
40+
app.get("/api/modules", (req, res) => {
41+
try {
42+
const result = calcModuleInfo(options);
43+
res.json(result);
44+
} catch (e: any) {
45+
console.log("error", e);
46+
res.status(500).json({ message: e.message });
47+
}
48+
});
49+
50+
app.get("/api/folders", (req, res) => {
51+
try {
52+
const result = inferFolders(options);
53+
res.json(result);
54+
} catch (e: any) {
55+
console.log("error", e);
56+
res.status(500).json({ message: e.message });
57+
}
58+
});
59+
60+
app.get("/api/coupling", (req, res) => {
61+
try {
62+
const result = calcCoupling(options);
63+
res.json(result);
64+
} catch (e: any) {
65+
console.log("error", e);
66+
res.status(500).json({ message: e.message });
67+
}
68+
});
69+
70+
app.get("/api/change-coupling", async (req, res) => {
71+
const limits = getLimits(req);
72+
73+
try {
74+
const result = await calcChangeCoupling(limits, options);
75+
res.json(result);
76+
} catch (e: any) {
77+
console.log("error", e);
78+
res.status(500).json({ message: e.message });
79+
}
80+
});
81+
82+
app.get("/api/team-alignment", async (req, res) => {
83+
const byUser = Boolean(req.query.byUser);
84+
const limits = getLimits(req);
85+
86+
try {
87+
const result = await calcTeamAlignment(byUser, limits, options);
88+
res.json(result);
89+
} catch (e: any) {
90+
console.log("error", e);
91+
res.status(500).json({ message: e.message });
92+
}
93+
});
94+
95+
app.get("/api/hotspots/aggregated", async (req, res) => {
96+
const minScore = Number(req.query.minScore) || -1;
97+
const criteria = { minScore, module: "" };
98+
99+
const limits = getLimits(req);
100+
101+
try {
102+
const result = await aggregateHotspots(criteria, limits, options);
103+
res.json(result);
104+
} catch (e: any) {
105+
console.log("error", e);
106+
res.status(500).json({ message: e.message });
107+
}
108+
});
109+
110+
app.get("/api/hotspots", async (req, res) => {
111+
const minScore = Number(req.query.minScore) || -1;
112+
const module = req.query.module ? String(req.query.module) : "";
113+
const criteria = { minScore, module };
114+
115+
const limits = getLimits(req);
116+
117+
try {
118+
const result = await findHotspotFiles(criteria, limits, options);
119+
res.json(result);
120+
} catch (e: any) {
121+
console.log("error", e);
122+
res.status(500).json({ message: e.message });
123+
}
124+
});
125+
126+
app.use(express.static(path.join(__dirname, "..", "public")));
127+
128+
app.get("*", (req, res) => {
129+
res.sendFile(path.join(__dirname, "..", "public", "index.html"));
130+
});
131+
return app;
132+
}
133+
134+
function getLimits(req: express.Request): Limits {
135+
return {
136+
limitCommits: parseInt("" + req.query.limitCommits) || null,
137+
limitMonths: parseInt("" + req.query.limitMonths) || null,
138+
};
139+
}

backend/src/infrastructure/git.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
import * as fs from 'fs';
2+
import { Limits } from '../model/limit';
23
const { spawn } = require("child_process");
34

45
export function isRepo(): boolean {
56
return fs.existsSync('.git');
67
}
78

8-
export function getGitLog(): Promise<string> {
9+
export function getGitLog(limits: Limits): Promise<string> {
910
return new Promise<string>((resolve, reject) => {
1011
try {
11-
const subprocess = spawn("git", ["log", '--numstat', '--pretty=format:"%an <%ae>,%ad"']);
12+
const args = ["log", '--numstat', '--pretty=format:"%an <%ae>,%ad"'];
13+
14+
if (limits.limitCommits) {
15+
args.push('-n ' + limits.limitCommits);
16+
}
17+
18+
if (limits.limitMonths) {
19+
args.push(`--since="${limits.limitMonths} months ago"`);
20+
}
21+
22+
const subprocess = spawn("git", args);
1223

1324
let text = "";
1425
let error = "";

backend/src/model/limit.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type Limits = {
2+
limitCommits: number | null;
3+
limitMonths: number | null;
4+
}
5+
6+
export const NoLimits: Limits = {
7+
limitCommits: 0,
8+
limitMonths: 0,
9+
};

backend/src/services/change-coupling.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { loadConfig } from "../infrastructure/config";
2+
import { Limits } from "../model/limit";
23
import { Options } from "../options/options";
34
import { parseGitLog } from "../utils/git-parser";
45
import { getEmptyMatrix } from "../utils/matrix";
@@ -13,7 +14,7 @@ export type ChangeCouplingResult = {
1314
cohesion: number[];
1415
}
1516

16-
export async function calcChangeCoupling(options: Options): Promise<ChangeCouplingResult> {
17+
export async function calcChangeCoupling(limits: Limits, options: Options): Promise<ChangeCouplingResult> {
1718
const config = loadConfig(options);
1819

1920
const displayModules = config.scopes;
@@ -32,7 +33,7 @@ export async function calcChangeCoupling(options: Options): Promise<ChangeCoupli
3233
}
3334
}
3435
addToMatrix(touchedModules, matrix);
35-
});
36+
}, limits);
3637

3738
return {
3839
matrix,

backend/src/services/hotspot.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as fs from 'fs';
55
import { calcComplexity } from "../utils/complexity";
66
import { loadConfig } from "../infrastructure/config";
77
import { normalizeFolder, toDisplayFolder } from "../utils/normalize-folder";
8+
import { Limits } from "../model/limit";
89

910
export type Hotspot = {
1011
commits: number,
@@ -27,9 +28,6 @@ export type HotspotResult = {
2728
};
2829

2930
export type HotspotCriteria = {
30-
// minCommits: number;
31-
// minChangedLines: number;
32-
// minComplexity: number;
3331
module: string,
3432
minScore: number,
3533
};
@@ -43,8 +41,8 @@ export type AggregatedHotspotsResult = {
4341
aggregated: AggregatedHotspot[];
4442
}
4543

46-
export async function findHotspotFiles(criteria: HotspotCriteria, options: Options): Promise<HotspotResult> {
47-
const hotspots: Record<string, Hotspot> = await analyzeLogs(criteria, options);
44+
export async function findHotspotFiles(criteria: HotspotCriteria, limits: Limits, options: Options): Promise<HotspotResult> {
45+
const hotspots: Record<string, Hotspot> = await analyzeLogs(criteria, limits, options);
4846

4947
const filtered: FlatHotspot[] = [];
5048
for (const fileName of Object.keys(hotspots)) {
@@ -60,8 +58,8 @@ export async function findHotspotFiles(criteria: HotspotCriteria, options: Optio
6058
return { hotspots: filtered };
6159
}
6260

63-
export async function aggregateHotspots(criteria: HotspotCriteria, options: Options): Promise<AggregatedHotspotsResult> {
64-
const hotspots = (await findHotspotFiles(criteria, options)).hotspots;
61+
export async function aggregateHotspots(criteria: HotspotCriteria, limits: Limits, options: Options): Promise<AggregatedHotspotsResult> {
62+
const hotspots = (await findHotspotFiles(criteria, limits, options)).hotspots;
6563
const config = loadConfig(options);
6664

6765
const modules = config.scopes.map(m => normalizeFolder(m));
@@ -81,7 +79,7 @@ export async function aggregateHotspots(criteria: HotspotCriteria, options: Opti
8179
return { aggregated: result };
8280
}
8381

84-
async function analyzeLogs(criteria: HotspotCriteria, options: Options) {
82+
async function analyzeLogs(criteria: HotspotCriteria, limits: Limits, options: Options) {
8583
const hotspots: Record<string, Hotspot> = {};
8684

8785
await parseGitLog((entry) => {
@@ -115,6 +113,6 @@ async function analyzeLogs(criteria: HotspotCriteria, options: Options) {
115113
hotspot.commits++;
116114
hotspot.changedLines += change.linesAdded + change.linesRemoved;
117115
}
118-
});
116+
}, limits);
119117
return hotspots;
120118
}

backend/src/services/team-alignment.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { loadConfig } from "../infrastructure/config";
2+
import { Limits } from "../model/limit";
23
import { Options } from "../options/options";
34
import { parseGitLog } from "../utils/git-parser";
45
import { normalizeFolder } from "../utils/normalize-folder";
@@ -16,6 +17,7 @@ export type TeamAlignmentResult = {
1617

1718
export async function calcTeamAlignment(
1819
byUser = false,
20+
limits: Limits,
1921
options: Options
2022
): Promise<TeamAlignmentResult> {
2123
const config = loadConfig(options);
@@ -59,7 +61,7 @@ export async function calcTeamAlignment(
5961
}
6062
}
6163
}
62-
});
64+
}, limits);
6365

6466
console.log("users", Array.from(users));
6567

0 commit comments

Comments
 (0)