@0xbigboss/gh-pulse-core 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache.cjs +27 -0
- package/dist/cache.d.cts +6 -0
- package/dist/cache.d.ts +6 -0
- package/dist/cache.js +27 -0
- package/dist/config.cjs +4 -0
- package/dist/config.d.cts +10 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +4 -0
- package/dist/reports/exec.cjs +25 -4
- package/dist/reports/exec.js +25 -4
- package/dist/reports/personal.cjs +16 -8
- package/dist/reports/personal.js +16 -8
- package/dist/reports/team.cjs +54 -8
- package/dist/reports/team.js +54 -8
- package/dist/reports/types.d.cts +1 -0
- package/dist/reports/types.d.ts +1 -0
- package/dist/repos.cjs +50 -5
- package/dist/repos.d.cts +3 -0
- package/dist/repos.d.ts +3 -0
- package/dist/repos.js +50 -5
- package/dist/team.cjs +12 -1
- package/dist/team.js +12 -1
- package/package.json +2 -1
package/dist/cache.cjs
CHANGED
|
@@ -86,6 +86,12 @@ class Cache {
|
|
|
86
86
|
last_sync INTEGER NOT NULL,
|
|
87
87
|
cursor TEXT
|
|
88
88
|
);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS repo_lists (
|
|
91
|
+
owner TEXT PRIMARY KEY,
|
|
92
|
+
repos JSON NOT NULL,
|
|
93
|
+
fetched_at INTEGER NOT NULL
|
|
94
|
+
);
|
|
89
95
|
`);
|
|
90
96
|
}
|
|
91
97
|
close() {
|
|
@@ -235,6 +241,27 @@ class Cache {
|
|
|
235
241
|
isFresh(fetchedAt, ttlHours) {
|
|
236
242
|
return fetchedAt >= this.now() - ttlHours * 60 * 60 * 1000;
|
|
237
243
|
}
|
|
244
|
+
getRepoList(owner) {
|
|
245
|
+
const row = this.db
|
|
246
|
+
.query('SELECT repos, fetched_at FROM repo_lists WHERE owner = ?')
|
|
247
|
+
.get(owner);
|
|
248
|
+
if (!row) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
repos: JSON.parse(row.repos),
|
|
253
|
+
fetched_at: row.fetched_at,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
upsertRepoList(owner, repos, fetchedAt = this.now()) {
|
|
257
|
+
this.db.run(`INSERT INTO repo_lists (owner, repos, fetched_at)
|
|
258
|
+
VALUES (?, ?, ?)
|
|
259
|
+
ON CONFLICT(owner) DO UPDATE SET repos = excluded.repos, fetched_at = excluded.fetched_at`, [owner, JSON.stringify(repos), fetchedAt]);
|
|
260
|
+
}
|
|
261
|
+
getCachedRepos() {
|
|
262
|
+
const rows = this.db.query('SELECT DISTINCT repo FROM sync_state').all();
|
|
263
|
+
return rows.map((row) => row.repo);
|
|
264
|
+
}
|
|
238
265
|
eventExists(event, timestamp) {
|
|
239
266
|
switch (event.type) {
|
|
240
267
|
case 'pr_opened':
|
package/dist/cache.d.cts
CHANGED
|
@@ -52,6 +52,12 @@ export declare class Cache {
|
|
|
52
52
|
upsertSyncState(repo: RepoFullName, lastSync: Timestamp, cursor?: string | null): void;
|
|
53
53
|
getFreshness(repos: RepoFullName[]): CacheFreshness;
|
|
54
54
|
isFresh(fetchedAt: Timestamp, ttlHours: number): boolean;
|
|
55
|
+
getRepoList(owner: string): {
|
|
56
|
+
repos: RepoFullName[];
|
|
57
|
+
fetched_at: Timestamp;
|
|
58
|
+
} | null;
|
|
59
|
+
upsertRepoList(owner: string, repos: RepoFullName[], fetchedAt?: Timestamp): void;
|
|
60
|
+
getCachedRepos(): RepoFullName[];
|
|
55
61
|
private eventExists;
|
|
56
62
|
}
|
|
57
63
|
export declare function getEventTimestamp(event: Event): Timestamp;
|
package/dist/cache.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export declare class Cache {
|
|
|
52
52
|
upsertSyncState(repo: RepoFullName, lastSync: Timestamp, cursor?: string | null): void;
|
|
53
53
|
getFreshness(repos: RepoFullName[]): CacheFreshness;
|
|
54
54
|
isFresh(fetchedAt: Timestamp, ttlHours: number): boolean;
|
|
55
|
+
getRepoList(owner: string): {
|
|
56
|
+
repos: RepoFullName[];
|
|
57
|
+
fetched_at: Timestamp;
|
|
58
|
+
} | null;
|
|
59
|
+
upsertRepoList(owner: string, repos: RepoFullName[], fetchedAt?: Timestamp): void;
|
|
60
|
+
getCachedRepos(): RepoFullName[];
|
|
55
61
|
private eventExists;
|
|
56
62
|
}
|
|
57
63
|
export declare function getEventTimestamp(event: Event): Timestamp;
|
package/dist/cache.js
CHANGED
|
@@ -78,6 +78,12 @@ export class Cache {
|
|
|
78
78
|
last_sync INTEGER NOT NULL,
|
|
79
79
|
cursor TEXT
|
|
80
80
|
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS repo_lists (
|
|
83
|
+
owner TEXT PRIMARY KEY,
|
|
84
|
+
repos JSON NOT NULL,
|
|
85
|
+
fetched_at INTEGER NOT NULL
|
|
86
|
+
);
|
|
81
87
|
`);
|
|
82
88
|
}
|
|
83
89
|
close() {
|
|
@@ -227,6 +233,27 @@ export class Cache {
|
|
|
227
233
|
isFresh(fetchedAt, ttlHours) {
|
|
228
234
|
return fetchedAt >= this.now() - ttlHours * 60 * 60 * 1000;
|
|
229
235
|
}
|
|
236
|
+
getRepoList(owner) {
|
|
237
|
+
const row = this.db
|
|
238
|
+
.query('SELECT repos, fetched_at FROM repo_lists WHERE owner = ?')
|
|
239
|
+
.get(owner);
|
|
240
|
+
if (!row) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
repos: JSON.parse(row.repos),
|
|
245
|
+
fetched_at: row.fetched_at,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
upsertRepoList(owner, repos, fetchedAt = this.now()) {
|
|
249
|
+
this.db.run(`INSERT INTO repo_lists (owner, repos, fetched_at)
|
|
250
|
+
VALUES (?, ?, ?)
|
|
251
|
+
ON CONFLICT(owner) DO UPDATE SET repos = excluded.repos, fetched_at = excluded.fetched_at`, [owner, JSON.stringify(repos), fetchedAt]);
|
|
252
|
+
}
|
|
253
|
+
getCachedRepos() {
|
|
254
|
+
const rows = this.db.query('SELECT DISTINCT repo FROM sync_state').all();
|
|
255
|
+
return rows.map((row) => row.repo);
|
|
256
|
+
}
|
|
230
257
|
eventExists(event, timestamp) {
|
|
231
258
|
switch (event.type) {
|
|
232
259
|
case 'pr_opened':
|
package/dist/config.cjs
CHANGED
|
@@ -33,11 +33,15 @@ const ThresholdsSchema = zod_1.z
|
|
|
33
33
|
stale_days: zod_1.z.number().int().positive().default(2),
|
|
34
34
|
stuck_days: zod_1.z.number().int().positive().default(4),
|
|
35
35
|
large_pr_lines: zod_1.z.number().int().positive().default(500),
|
|
36
|
+
// Max age for blockers - PRs older than this are excluded from blocker list
|
|
37
|
+
// Set to 0 to disable (show all blockers regardless of age)
|
|
38
|
+
blocker_max_age_days: zod_1.z.number().int().nonnegative().default(90),
|
|
36
39
|
})
|
|
37
40
|
.default({
|
|
38
41
|
stale_days: 2,
|
|
39
42
|
stuck_days: 4,
|
|
40
43
|
large_pr_lines: 500,
|
|
44
|
+
blocker_max_age_days: 90,
|
|
41
45
|
});
|
|
42
46
|
const CacheSchema = zod_1.z
|
|
43
47
|
.object({
|
package/dist/config.d.cts
CHANGED
|
@@ -33,14 +33,17 @@ declare const ResolvedConfigSchema: z.ZodObject<{
|
|
|
33
33
|
stale_days: z.ZodDefault<z.ZodNumber>;
|
|
34
34
|
stuck_days: z.ZodDefault<z.ZodNumber>;
|
|
35
35
|
large_pr_lines: z.ZodDefault<z.ZodNumber>;
|
|
36
|
+
blocker_max_age_days: z.ZodDefault<z.ZodNumber>;
|
|
36
37
|
}, "strip", z.ZodTypeAny, {
|
|
37
38
|
stale_days: number;
|
|
38
39
|
stuck_days: number;
|
|
39
40
|
large_pr_lines: number;
|
|
41
|
+
blocker_max_age_days: number;
|
|
40
42
|
}, {
|
|
41
43
|
stale_days?: number | undefined;
|
|
42
44
|
stuck_days?: number | undefined;
|
|
43
45
|
large_pr_lines?: number | undefined;
|
|
46
|
+
blocker_max_age_days?: number | undefined;
|
|
44
47
|
}>>;
|
|
45
48
|
timezone: z.ZodDefault<z.ZodString>;
|
|
46
49
|
cache: z.ZodDefault<z.ZodObject<{
|
|
@@ -79,6 +82,7 @@ declare const ResolvedConfigSchema: z.ZodObject<{
|
|
|
79
82
|
stale_days: number;
|
|
80
83
|
stuck_days: number;
|
|
81
84
|
large_pr_lines: number;
|
|
85
|
+
blocker_max_age_days: number;
|
|
82
86
|
};
|
|
83
87
|
timezone: string;
|
|
84
88
|
cache: {
|
|
@@ -107,6 +111,7 @@ declare const ResolvedConfigSchema: z.ZodObject<{
|
|
|
107
111
|
stale_days?: number | undefined;
|
|
108
112
|
stuck_days?: number | undefined;
|
|
109
113
|
large_pr_lines?: number | undefined;
|
|
114
|
+
blocker_max_age_days?: number | undefined;
|
|
110
115
|
} | undefined;
|
|
111
116
|
timezone?: string | undefined;
|
|
112
117
|
cache?: {
|
|
@@ -164,14 +169,17 @@ export declare const CONFIG_SCHEMA: z.ZodObject<{
|
|
|
164
169
|
stale_days: z.ZodDefault<z.ZodNumber>;
|
|
165
170
|
stuck_days: z.ZodDefault<z.ZodNumber>;
|
|
166
171
|
large_pr_lines: z.ZodDefault<z.ZodNumber>;
|
|
172
|
+
blocker_max_age_days: z.ZodDefault<z.ZodNumber>;
|
|
167
173
|
}, "strip", z.ZodTypeAny, {
|
|
168
174
|
stale_days: number;
|
|
169
175
|
stuck_days: number;
|
|
170
176
|
large_pr_lines: number;
|
|
177
|
+
blocker_max_age_days: number;
|
|
171
178
|
}, {
|
|
172
179
|
stale_days?: number | undefined;
|
|
173
180
|
stuck_days?: number | undefined;
|
|
174
181
|
large_pr_lines?: number | undefined;
|
|
182
|
+
blocker_max_age_days?: number | undefined;
|
|
175
183
|
}>>;
|
|
176
184
|
timezone: z.ZodDefault<z.ZodString>;
|
|
177
185
|
cache: z.ZodDefault<z.ZodObject<{
|
|
@@ -207,6 +215,7 @@ export declare const CONFIG_SCHEMA: z.ZodObject<{
|
|
|
207
215
|
stale_days: number;
|
|
208
216
|
stuck_days: number;
|
|
209
217
|
large_pr_lines: number;
|
|
218
|
+
blocker_max_age_days: number;
|
|
210
219
|
};
|
|
211
220
|
timezone: string;
|
|
212
221
|
cache: {
|
|
@@ -236,6 +245,7 @@ export declare const CONFIG_SCHEMA: z.ZodObject<{
|
|
|
236
245
|
stale_days?: number | undefined;
|
|
237
246
|
stuck_days?: number | undefined;
|
|
238
247
|
large_pr_lines?: number | undefined;
|
|
248
|
+
blocker_max_age_days?: number | undefined;
|
|
239
249
|
} | undefined;
|
|
240
250
|
timezone?: string | undefined;
|
|
241
251
|
cache?: {
|
package/dist/config.d.ts
CHANGED
|
@@ -33,14 +33,17 @@ declare const ResolvedConfigSchema: z.ZodObject<{
|
|
|
33
33
|
stale_days: z.ZodDefault<z.ZodNumber>;
|
|
34
34
|
stuck_days: z.ZodDefault<z.ZodNumber>;
|
|
35
35
|
large_pr_lines: z.ZodDefault<z.ZodNumber>;
|
|
36
|
+
blocker_max_age_days: z.ZodDefault<z.ZodNumber>;
|
|
36
37
|
}, "strip", z.ZodTypeAny, {
|
|
37
38
|
stale_days: number;
|
|
38
39
|
stuck_days: number;
|
|
39
40
|
large_pr_lines: number;
|
|
41
|
+
blocker_max_age_days: number;
|
|
40
42
|
}, {
|
|
41
43
|
stale_days?: number | undefined;
|
|
42
44
|
stuck_days?: number | undefined;
|
|
43
45
|
large_pr_lines?: number | undefined;
|
|
46
|
+
blocker_max_age_days?: number | undefined;
|
|
44
47
|
}>>;
|
|
45
48
|
timezone: z.ZodDefault<z.ZodString>;
|
|
46
49
|
cache: z.ZodDefault<z.ZodObject<{
|
|
@@ -79,6 +82,7 @@ declare const ResolvedConfigSchema: z.ZodObject<{
|
|
|
79
82
|
stale_days: number;
|
|
80
83
|
stuck_days: number;
|
|
81
84
|
large_pr_lines: number;
|
|
85
|
+
blocker_max_age_days: number;
|
|
82
86
|
};
|
|
83
87
|
timezone: string;
|
|
84
88
|
cache: {
|
|
@@ -107,6 +111,7 @@ declare const ResolvedConfigSchema: z.ZodObject<{
|
|
|
107
111
|
stale_days?: number | undefined;
|
|
108
112
|
stuck_days?: number | undefined;
|
|
109
113
|
large_pr_lines?: number | undefined;
|
|
114
|
+
blocker_max_age_days?: number | undefined;
|
|
110
115
|
} | undefined;
|
|
111
116
|
timezone?: string | undefined;
|
|
112
117
|
cache?: {
|
|
@@ -164,14 +169,17 @@ export declare const CONFIG_SCHEMA: z.ZodObject<{
|
|
|
164
169
|
stale_days: z.ZodDefault<z.ZodNumber>;
|
|
165
170
|
stuck_days: z.ZodDefault<z.ZodNumber>;
|
|
166
171
|
large_pr_lines: z.ZodDefault<z.ZodNumber>;
|
|
172
|
+
blocker_max_age_days: z.ZodDefault<z.ZodNumber>;
|
|
167
173
|
}, "strip", z.ZodTypeAny, {
|
|
168
174
|
stale_days: number;
|
|
169
175
|
stuck_days: number;
|
|
170
176
|
large_pr_lines: number;
|
|
177
|
+
blocker_max_age_days: number;
|
|
171
178
|
}, {
|
|
172
179
|
stale_days?: number | undefined;
|
|
173
180
|
stuck_days?: number | undefined;
|
|
174
181
|
large_pr_lines?: number | undefined;
|
|
182
|
+
blocker_max_age_days?: number | undefined;
|
|
175
183
|
}>>;
|
|
176
184
|
timezone: z.ZodDefault<z.ZodString>;
|
|
177
185
|
cache: z.ZodDefault<z.ZodObject<{
|
|
@@ -207,6 +215,7 @@ export declare const CONFIG_SCHEMA: z.ZodObject<{
|
|
|
207
215
|
stale_days: number;
|
|
208
216
|
stuck_days: number;
|
|
209
217
|
large_pr_lines: number;
|
|
218
|
+
blocker_max_age_days: number;
|
|
210
219
|
};
|
|
211
220
|
timezone: string;
|
|
212
221
|
cache: {
|
|
@@ -236,6 +245,7 @@ export declare const CONFIG_SCHEMA: z.ZodObject<{
|
|
|
236
245
|
stale_days?: number | undefined;
|
|
237
246
|
stuck_days?: number | undefined;
|
|
238
247
|
large_pr_lines?: number | undefined;
|
|
248
|
+
blocker_max_age_days?: number | undefined;
|
|
239
249
|
} | undefined;
|
|
240
250
|
timezone?: string | undefined;
|
|
241
251
|
cache?: {
|
package/dist/config.js
CHANGED
|
@@ -23,11 +23,15 @@ const ThresholdsSchema = z
|
|
|
23
23
|
stale_days: z.number().int().positive().default(2),
|
|
24
24
|
stuck_days: z.number().int().positive().default(4),
|
|
25
25
|
large_pr_lines: z.number().int().positive().default(500),
|
|
26
|
+
// Max age for blockers - PRs older than this are excluded from blocker list
|
|
27
|
+
// Set to 0 to disable (show all blockers regardless of age)
|
|
28
|
+
blocker_max_age_days: z.number().int().nonnegative().default(90),
|
|
26
29
|
})
|
|
27
30
|
.default({
|
|
28
31
|
stale_days: 2,
|
|
29
32
|
stuck_days: 4,
|
|
30
33
|
large_pr_lines: 500,
|
|
34
|
+
blocker_max_age_days: 90,
|
|
31
35
|
});
|
|
32
36
|
const CacheSchema = z
|
|
33
37
|
.object({
|
package/dist/reports/exec.cjs
CHANGED
|
@@ -114,19 +114,40 @@ function buildHighlights(mergedPrs, largePrThreshold, now) {
|
|
|
114
114
|
})
|
|
115
115
|
.filter((entry) => Boolean(entry));
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Case-insensitive username comparison.
|
|
119
|
+
*/
|
|
120
|
+
function usernameMatches(a, b) {
|
|
121
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Deduplicate team members case-insensitively, preserving first occurrence.
|
|
125
|
+
*/
|
|
126
|
+
function dedupeMembers(members) {
|
|
127
|
+
const seen = new Map();
|
|
128
|
+
for (const member of members) {
|
|
129
|
+
const key = member.toLowerCase();
|
|
130
|
+
if (!seen.has(key)) {
|
|
131
|
+
seen.set(key, member);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Array.from(seen.values());
|
|
135
|
+
}
|
|
117
136
|
function buildTeamBreakdown(teamMembers, pullRequests, commits, events, exclude, excludeBots, since, until) {
|
|
118
137
|
const reviewEvents = events.filter((event) => event.type === 'review_submitted');
|
|
119
|
-
|
|
138
|
+
// Dedupe members case-insensitively
|
|
139
|
+
const uniqueMembers = dedupeMembers(teamMembers);
|
|
140
|
+
return uniqueMembers
|
|
120
141
|
.filter((member) => !(0, filters_1.isExcludedAuthor)(member, exclude, excludeBots))
|
|
121
142
|
.map((member) => {
|
|
122
|
-
const authored = pullRequests.filter((pr) => pr.author
|
|
143
|
+
const authored = pullRequests.filter((pr) => usernameMatches(pr.author, member) &&
|
|
123
144
|
pr.created_at >= since &&
|
|
124
145
|
pr.created_at <= until &&
|
|
125
146
|
!(0, filters_1.isExcludedAuthor)(pr.author, exclude, excludeBots));
|
|
126
147
|
const reviewed = reviewEvents.filter((event) => event.type === 'review_submitted' &&
|
|
127
|
-
event.reviewer
|
|
148
|
+
usernameMatches(event.reviewer, member) &&
|
|
128
149
|
!(0, filters_1.isExcludedAuthor)(event.reviewer, exclude, excludeBots));
|
|
129
|
-
const memberCommits = commits.filter((commit) => commit.author
|
|
150
|
+
const memberCommits = commits.filter((commit) => usernameMatches(commit.author, member) &&
|
|
130
151
|
commit.committed_at >= since &&
|
|
131
152
|
commit.committed_at <= until &&
|
|
132
153
|
!(0, filters_1.isExcludedAuthor)(commit.author, exclude, excludeBots));
|
package/dist/reports/exec.js
CHANGED
|
@@ -111,19 +111,40 @@ function buildHighlights(mergedPrs, largePrThreshold, now) {
|
|
|
111
111
|
})
|
|
112
112
|
.filter((entry) => Boolean(entry));
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Case-insensitive username comparison.
|
|
116
|
+
*/
|
|
117
|
+
function usernameMatches(a, b) {
|
|
118
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Deduplicate team members case-insensitively, preserving first occurrence.
|
|
122
|
+
*/
|
|
123
|
+
function dedupeMembers(members) {
|
|
124
|
+
const seen = new Map();
|
|
125
|
+
for (const member of members) {
|
|
126
|
+
const key = member.toLowerCase();
|
|
127
|
+
if (!seen.has(key)) {
|
|
128
|
+
seen.set(key, member);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return Array.from(seen.values());
|
|
132
|
+
}
|
|
114
133
|
function buildTeamBreakdown(teamMembers, pullRequests, commits, events, exclude, excludeBots, since, until) {
|
|
115
134
|
const reviewEvents = events.filter((event) => event.type === 'review_submitted');
|
|
116
|
-
|
|
135
|
+
// Dedupe members case-insensitively
|
|
136
|
+
const uniqueMembers = dedupeMembers(teamMembers);
|
|
137
|
+
return uniqueMembers
|
|
117
138
|
.filter((member) => !isExcludedAuthor(member, exclude, excludeBots))
|
|
118
139
|
.map((member) => {
|
|
119
|
-
const authored = pullRequests.filter((pr) => pr.author
|
|
140
|
+
const authored = pullRequests.filter((pr) => usernameMatches(pr.author, member) &&
|
|
120
141
|
pr.created_at >= since &&
|
|
121
142
|
pr.created_at <= until &&
|
|
122
143
|
!isExcludedAuthor(pr.author, exclude, excludeBots));
|
|
123
144
|
const reviewed = reviewEvents.filter((event) => event.type === 'review_submitted' &&
|
|
124
|
-
event.reviewer
|
|
145
|
+
usernameMatches(event.reviewer, member) &&
|
|
125
146
|
!isExcludedAuthor(event.reviewer, exclude, excludeBots));
|
|
126
|
-
const memberCommits = commits.filter((commit) => commit.author
|
|
147
|
+
const memberCommits = commits.filter((commit) => usernameMatches(commit.author, member) &&
|
|
127
148
|
commit.committed_at >= since &&
|
|
128
149
|
commit.committed_at <= until &&
|
|
129
150
|
!isExcludedAuthor(commit.author, exclude, excludeBots));
|
|
@@ -4,6 +4,12 @@ exports.buildPersonalReport = buildPersonalReport;
|
|
|
4
4
|
const utils_1 = require("./utils.cjs");
|
|
5
5
|
const context_1 = require("./context.cjs");
|
|
6
6
|
const filters_1 = require("../filters.cjs");
|
|
7
|
+
/**
|
|
8
|
+
* Case-insensitive username comparison.
|
|
9
|
+
*/
|
|
10
|
+
function usernameMatches(a, b) {
|
|
11
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
12
|
+
}
|
|
7
13
|
function buildPersonalReport(cache, options) {
|
|
8
14
|
const { pullRequests, commits } = (0, context_1.loadReportData)(cache, options.repos);
|
|
9
15
|
const now = options.meta.generated_at;
|
|
@@ -12,7 +18,8 @@ function buildPersonalReport(cache, options) {
|
|
|
12
18
|
const events = cache
|
|
13
19
|
.getEvents({ since: options.timeRange.start, until: options.timeRange.end })
|
|
14
20
|
.filter((event) => options.repos.includes(event.repo));
|
|
15
|
-
const authoredPrs = pullRequests.filter((pr) => pr.author
|
|
21
|
+
const authoredPrs = pullRequests.filter((pr) => usernameMatches(pr.author, options.user) &&
|
|
22
|
+
!(0, filters_1.isExcludedAuthor)(pr.author, exclude, options.excludeBots));
|
|
16
23
|
const authoredSummaries = authoredPrs
|
|
17
24
|
.filter((pr) => pr.created_at >= options.timeRange.start && pr.created_at <= options.timeRange.end)
|
|
18
25
|
.map((pr) => (0, utils_1.buildPullRequestSummary)(pr, now));
|
|
@@ -23,19 +30,20 @@ function buildPersonalReport(cache, options) {
|
|
|
23
30
|
const requested = pullRequests
|
|
24
31
|
.filter((pr) => pr.state === 'open')
|
|
25
32
|
.filter((pr) => options.includeDrafts || !pr.draft)
|
|
26
|
-
.filter((pr) => pr.requested_reviewers.
|
|
33
|
+
.filter((pr) => pr.requested_reviewers.some((reviewer) => usernameMatches(reviewer, options.user)))
|
|
27
34
|
.filter((pr) => !(0, filters_1.isExcludedAuthor)(pr.author, exclude, options.excludeBots))
|
|
28
35
|
.map((pr) => (0, utils_1.buildPullRequestSummary)(pr, now));
|
|
29
|
-
|
|
36
|
+
// Use lowercase keys for case-insensitive membership checks
|
|
37
|
+
const teamMembers = new Set(options.teamMembers.map((m) => m.toLowerCase()));
|
|
30
38
|
const teamPrs = pullRequests
|
|
31
39
|
.filter((pr) => pr.state === 'open')
|
|
32
40
|
.filter((pr) => options.includeDrafts || !pr.draft)
|
|
33
|
-
.filter((pr) => pr.author
|
|
34
|
-
.filter((pr) => teamMembers.has(pr.author))
|
|
41
|
+
.filter((pr) => !usernameMatches(pr.author, options.user))
|
|
42
|
+
.filter((pr) => teamMembers.has(pr.author.toLowerCase()))
|
|
35
43
|
.filter((pr) => !(0, filters_1.isExcludedAuthor)(pr.author, exclude, options.excludeBots))
|
|
36
44
|
.map((pr) => (0, utils_1.buildPullRequestSummary)(pr, now));
|
|
37
45
|
const commitsForUser = commits
|
|
38
|
-
.filter((commit) => commit.author
|
|
46
|
+
.filter((commit) => usernameMatches(commit.author, options.user))
|
|
39
47
|
.filter((commit) => commit.committed_at >= options.timeRange.start)
|
|
40
48
|
.filter((commit) => commit.committed_at <= options.timeRange.end)
|
|
41
49
|
.filter((commit) => !(0, filters_1.isExcludedAuthor)(commit.author, exclude, options.excludeBots));
|
|
@@ -67,7 +75,7 @@ function buildPersonalReport(cache, options) {
|
|
|
67
75
|
};
|
|
68
76
|
}
|
|
69
77
|
function buildPersonalSummary(events, prLookup, user, commits) {
|
|
70
|
-
const opened = events.filter((event) => event.type === 'pr_opened' && event.author
|
|
78
|
+
const opened = events.filter((event) => event.type === 'pr_opened' && usernameMatches(event.author, user));
|
|
71
79
|
const merged = events.filter((event) => event.type === 'pr_merged' && prMatchesAuthor(prLookup, event.repo, event.pr_number, user));
|
|
72
80
|
const closed = events.filter((event) => event.type === 'pr_closed' && prMatchesAuthor(prLookup, event.repo, event.pr_number, user));
|
|
73
81
|
const reposTouched = new Set(commits.map((commit) => commit.repo));
|
|
@@ -83,7 +91,7 @@ function buildPersonalSummary(events, prLookup, user, commits) {
|
|
|
83
91
|
}
|
|
84
92
|
function prMatchesAuthor(lookup, repo, number, user) {
|
|
85
93
|
const pr = lookup.get(`${repo}#${number}`);
|
|
86
|
-
return pr ? pr.author
|
|
94
|
+
return pr ? usernameMatches(pr.author, user) : false;
|
|
87
95
|
}
|
|
88
96
|
function buildPullRequestLookup(pullRequests) {
|
|
89
97
|
return new Map(pullRequests.map((pr) => [`${pr.repo}#${pr.number}`, pr]));
|
package/dist/reports/personal.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { buildPullRequestSummary } from "./utils.js";
|
|
2
2
|
import { loadReportData } from "./context.js";
|
|
3
3
|
import { isExcludedAuthor } from "../filters.js";
|
|
4
|
+
/**
|
|
5
|
+
* Case-insensitive username comparison.
|
|
6
|
+
*/
|
|
7
|
+
function usernameMatches(a, b) {
|
|
8
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
9
|
+
}
|
|
4
10
|
export function buildPersonalReport(cache, options) {
|
|
5
11
|
const { pullRequests, commits } = loadReportData(cache, options.repos);
|
|
6
12
|
const now = options.meta.generated_at;
|
|
@@ -9,7 +15,8 @@ export function buildPersonalReport(cache, options) {
|
|
|
9
15
|
const events = cache
|
|
10
16
|
.getEvents({ since: options.timeRange.start, until: options.timeRange.end })
|
|
11
17
|
.filter((event) => options.repos.includes(event.repo));
|
|
12
|
-
const authoredPrs = pullRequests.filter((pr) => pr.author
|
|
18
|
+
const authoredPrs = pullRequests.filter((pr) => usernameMatches(pr.author, options.user) &&
|
|
19
|
+
!isExcludedAuthor(pr.author, exclude, options.excludeBots));
|
|
13
20
|
const authoredSummaries = authoredPrs
|
|
14
21
|
.filter((pr) => pr.created_at >= options.timeRange.start && pr.created_at <= options.timeRange.end)
|
|
15
22
|
.map((pr) => buildPullRequestSummary(pr, now));
|
|
@@ -20,19 +27,20 @@ export function buildPersonalReport(cache, options) {
|
|
|
20
27
|
const requested = pullRequests
|
|
21
28
|
.filter((pr) => pr.state === 'open')
|
|
22
29
|
.filter((pr) => options.includeDrafts || !pr.draft)
|
|
23
|
-
.filter((pr) => pr.requested_reviewers.
|
|
30
|
+
.filter((pr) => pr.requested_reviewers.some((reviewer) => usernameMatches(reviewer, options.user)))
|
|
24
31
|
.filter((pr) => !isExcludedAuthor(pr.author, exclude, options.excludeBots))
|
|
25
32
|
.map((pr) => buildPullRequestSummary(pr, now));
|
|
26
|
-
|
|
33
|
+
// Use lowercase keys for case-insensitive membership checks
|
|
34
|
+
const teamMembers = new Set(options.teamMembers.map((m) => m.toLowerCase()));
|
|
27
35
|
const teamPrs = pullRequests
|
|
28
36
|
.filter((pr) => pr.state === 'open')
|
|
29
37
|
.filter((pr) => options.includeDrafts || !pr.draft)
|
|
30
|
-
.filter((pr) => pr.author
|
|
31
|
-
.filter((pr) => teamMembers.has(pr.author))
|
|
38
|
+
.filter((pr) => !usernameMatches(pr.author, options.user))
|
|
39
|
+
.filter((pr) => teamMembers.has(pr.author.toLowerCase()))
|
|
32
40
|
.filter((pr) => !isExcludedAuthor(pr.author, exclude, options.excludeBots))
|
|
33
41
|
.map((pr) => buildPullRequestSummary(pr, now));
|
|
34
42
|
const commitsForUser = commits
|
|
35
|
-
.filter((commit) => commit.author
|
|
43
|
+
.filter((commit) => usernameMatches(commit.author, options.user))
|
|
36
44
|
.filter((commit) => commit.committed_at >= options.timeRange.start)
|
|
37
45
|
.filter((commit) => commit.committed_at <= options.timeRange.end)
|
|
38
46
|
.filter((commit) => !isExcludedAuthor(commit.author, exclude, options.excludeBots));
|
|
@@ -64,7 +72,7 @@ export function buildPersonalReport(cache, options) {
|
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
74
|
function buildPersonalSummary(events, prLookup, user, commits) {
|
|
67
|
-
const opened = events.filter((event) => event.type === 'pr_opened' && event.author
|
|
75
|
+
const opened = events.filter((event) => event.type === 'pr_opened' && usernameMatches(event.author, user));
|
|
68
76
|
const merged = events.filter((event) => event.type === 'pr_merged' && prMatchesAuthor(prLookup, event.repo, event.pr_number, user));
|
|
69
77
|
const closed = events.filter((event) => event.type === 'pr_closed' && prMatchesAuthor(prLookup, event.repo, event.pr_number, user));
|
|
70
78
|
const reposTouched = new Set(commits.map((commit) => commit.repo));
|
|
@@ -80,7 +88,7 @@ function buildPersonalSummary(events, prLookup, user, commits) {
|
|
|
80
88
|
}
|
|
81
89
|
function prMatchesAuthor(lookup, repo, number, user) {
|
|
82
90
|
const pr = lookup.get(`${repo}#${number}`);
|
|
83
|
-
return pr ? pr.author
|
|
91
|
+
return pr ? usernameMatches(pr.author, user) : false;
|
|
84
92
|
}
|
|
85
93
|
function buildPullRequestLookup(pullRequests) {
|
|
86
94
|
return new Map(pullRequests.map((pr) => [`${pr.repo}#${pr.number}`, pr]));
|
package/dist/reports/team.cjs
CHANGED
|
@@ -8,7 +8,8 @@ function buildTeamReport(cache, options) {
|
|
|
8
8
|
const { pullRequests, commits } = (0, context_1.loadReportData)(cache, options.repos);
|
|
9
9
|
const now = options.meta.generated_at;
|
|
10
10
|
const exclude = options.excludeAuthors;
|
|
11
|
-
|
|
11
|
+
// Use lowercase keys for case-insensitive membership checks
|
|
12
|
+
const teamSet = new Set(options.teamMembers.map((m) => m.toLowerCase()));
|
|
12
13
|
const prLookup = buildPullRequestLookup(pullRequests);
|
|
13
14
|
const events = cache
|
|
14
15
|
.getEvents({ since: options.timeRange.start, until: options.timeRange.end })
|
|
@@ -43,7 +44,7 @@ function buildTeamReport(cache, options) {
|
|
|
43
44
|
return { pr: (0, utils_1.buildPullRequestSummary)(pr, now), metrics };
|
|
44
45
|
});
|
|
45
46
|
const memberBreakdown = buildMemberBreakdown(options.teamMembers, pullRequests, commits, reviewEvents, exclude, options.excludeBots, options.timeRange.start, options.timeRange.end);
|
|
46
|
-
const blockers = findBlockers(pullRequests, options.includeDrafts, exclude, options.excludeBots, options.thresholds, now);
|
|
47
|
+
const blockers = findBlockers(pullRequests, options.includeDrafts, exclude, options.excludeBots, options.thresholds, now, options.timeRange);
|
|
47
48
|
return {
|
|
48
49
|
type: 'team',
|
|
49
50
|
meta: options.meta,
|
|
@@ -63,18 +64,39 @@ function buildTeamReport(cache, options) {
|
|
|
63
64
|
review_engagement: reviewEngagement,
|
|
64
65
|
};
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Case-insensitive username comparison.
|
|
69
|
+
*/
|
|
70
|
+
function usernameMatches(a, b) {
|
|
71
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Deduplicate team members case-insensitively, preserving first occurrence.
|
|
75
|
+
*/
|
|
76
|
+
function dedupeMembers(members) {
|
|
77
|
+
const seen = new Map();
|
|
78
|
+
for (const member of members) {
|
|
79
|
+
const key = member.toLowerCase();
|
|
80
|
+
if (!seen.has(key)) {
|
|
81
|
+
seen.set(key, member);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return Array.from(seen.values());
|
|
85
|
+
}
|
|
66
86
|
function buildMemberBreakdown(teamMembers, pullRequests, commits, reviewEvents, exclude, excludeBots, since, until) {
|
|
67
|
-
|
|
87
|
+
// Dedupe members case-insensitively
|
|
88
|
+
const uniqueMembers = dedupeMembers(teamMembers);
|
|
89
|
+
return uniqueMembers
|
|
68
90
|
.filter((member) => !(0, filters_1.isExcludedAuthor)(member, exclude, excludeBots))
|
|
69
91
|
.map((member) => {
|
|
70
|
-
const authored = pullRequests.filter((pr) => pr.author
|
|
92
|
+
const authored = pullRequests.filter((pr) => usernameMatches(pr.author, member) &&
|
|
71
93
|
pr.created_at >= since &&
|
|
72
94
|
pr.created_at <= until &&
|
|
73
95
|
!(0, filters_1.isExcludedAuthor)(pr.author, exclude, excludeBots));
|
|
74
96
|
const reviewed = reviewEvents.filter((event) => event.type === 'review_submitted' &&
|
|
75
|
-
event.reviewer
|
|
97
|
+
usernameMatches(event.reviewer, member) &&
|
|
76
98
|
!(0, filters_1.isExcludedAuthor)(event.reviewer, exclude, excludeBots));
|
|
77
|
-
const memberCommits = commits.filter((commit) => commit.author
|
|
99
|
+
const memberCommits = commits.filter((commit) => usernameMatches(commit.author, member) &&
|
|
78
100
|
commit.committed_at >= since &&
|
|
79
101
|
commit.committed_at <= until &&
|
|
80
102
|
!(0, filters_1.isExcludedAuthor)(commit.author, exclude, excludeBots));
|
|
@@ -89,11 +111,32 @@ function buildMemberBreakdown(teamMembers, pullRequests, commits, reviewEvents,
|
|
|
89
111
|
})
|
|
90
112
|
.toSorted((a, b) => a.user.localeCompare(b.user));
|
|
91
113
|
}
|
|
92
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Find blockers (stale/stuck PRs) with configurable age filtering.
|
|
116
|
+
*
|
|
117
|
+
* A PR is included in the blocker list if:
|
|
118
|
+
* - It had activity (was updated) within the time range, OR
|
|
119
|
+
* - It was created within the time range, OR
|
|
120
|
+
* - blocker_max_age_days is 0 (disabled), OR
|
|
121
|
+
* - The PR is younger than blocker_max_age_days
|
|
122
|
+
*/
|
|
123
|
+
function findBlockers(pullRequests, includeDrafts, exclude, excludeBots, thresholds, now, timeRange) {
|
|
124
|
+
const maxAgeDays = thresholds.blocker_max_age_days;
|
|
93
125
|
return pullRequests
|
|
94
126
|
.filter((pr) => pr.state === 'open')
|
|
95
127
|
.filter((pr) => includeDrafts || !pr.draft)
|
|
96
128
|
.filter((pr) => !(0, filters_1.isExcludedAuthor)(pr.author, exclude, excludeBots))
|
|
129
|
+
.filter((pr) => {
|
|
130
|
+
// If max age is disabled (0), include all
|
|
131
|
+
if (maxAgeDays === 0)
|
|
132
|
+
return true;
|
|
133
|
+
const prAgeDays = diffDays(pr.created_at, now);
|
|
134
|
+
const hadActivityInRange = pr.updated_at >= timeRange.start && pr.updated_at <= timeRange.end;
|
|
135
|
+
const createdInRange = pr.created_at >= timeRange.start && pr.created_at <= timeRange.end;
|
|
136
|
+
const withinMaxAge = prAgeDays <= maxAgeDays;
|
|
137
|
+
// Include if: had recent activity, was created in range, or is within max age
|
|
138
|
+
return hadActivityInRange || createdInRange || withinMaxAge;
|
|
139
|
+
})
|
|
97
140
|
.flatMap((pr) => {
|
|
98
141
|
const blockers = [];
|
|
99
142
|
const summary = (0, utils_1.buildPullRequestSummary)(pr, now);
|
|
@@ -112,8 +155,11 @@ function findBlockers(pullRequests, includeDrafts, exclude, excludeBots, thresho
|
|
|
112
155
|
function diffDays(start, end) {
|
|
113
156
|
return Math.floor((end - start) / (24 * 60 * 60 * 1000));
|
|
114
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Case-insensitive check if author is in team.
|
|
160
|
+
*/
|
|
115
161
|
function teamHasAuthor(team, author) {
|
|
116
|
-
return team.has(author);
|
|
162
|
+
return team.has(author.toLowerCase());
|
|
117
163
|
}
|
|
118
164
|
function prAuthorInTeam(lookup, team, repo, number, exclude, excludeBots) {
|
|
119
165
|
const pr = lookup.get(`${repo}#${number}`);
|
package/dist/reports/team.js
CHANGED
|
@@ -5,7 +5,8 @@ export function buildTeamReport(cache, options) {
|
|
|
5
5
|
const { pullRequests, commits } = loadReportData(cache, options.repos);
|
|
6
6
|
const now = options.meta.generated_at;
|
|
7
7
|
const exclude = options.excludeAuthors;
|
|
8
|
-
|
|
8
|
+
// Use lowercase keys for case-insensitive membership checks
|
|
9
|
+
const teamSet = new Set(options.teamMembers.map((m) => m.toLowerCase()));
|
|
9
10
|
const prLookup = buildPullRequestLookup(pullRequests);
|
|
10
11
|
const events = cache
|
|
11
12
|
.getEvents({ since: options.timeRange.start, until: options.timeRange.end })
|
|
@@ -40,7 +41,7 @@ export function buildTeamReport(cache, options) {
|
|
|
40
41
|
return { pr: buildPullRequestSummary(pr, now), metrics };
|
|
41
42
|
});
|
|
42
43
|
const memberBreakdown = buildMemberBreakdown(options.teamMembers, pullRequests, commits, reviewEvents, exclude, options.excludeBots, options.timeRange.start, options.timeRange.end);
|
|
43
|
-
const blockers = findBlockers(pullRequests, options.includeDrafts, exclude, options.excludeBots, options.thresholds, now);
|
|
44
|
+
const blockers = findBlockers(pullRequests, options.includeDrafts, exclude, options.excludeBots, options.thresholds, now, options.timeRange);
|
|
44
45
|
return {
|
|
45
46
|
type: 'team',
|
|
46
47
|
meta: options.meta,
|
|
@@ -60,18 +61,39 @@ export function buildTeamReport(cache, options) {
|
|
|
60
61
|
review_engagement: reviewEngagement,
|
|
61
62
|
};
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Case-insensitive username comparison.
|
|
66
|
+
*/
|
|
67
|
+
function usernameMatches(a, b) {
|
|
68
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Deduplicate team members case-insensitively, preserving first occurrence.
|
|
72
|
+
*/
|
|
73
|
+
function dedupeMembers(members) {
|
|
74
|
+
const seen = new Map();
|
|
75
|
+
for (const member of members) {
|
|
76
|
+
const key = member.toLowerCase();
|
|
77
|
+
if (!seen.has(key)) {
|
|
78
|
+
seen.set(key, member);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return Array.from(seen.values());
|
|
82
|
+
}
|
|
63
83
|
function buildMemberBreakdown(teamMembers, pullRequests, commits, reviewEvents, exclude, excludeBots, since, until) {
|
|
64
|
-
|
|
84
|
+
// Dedupe members case-insensitively
|
|
85
|
+
const uniqueMembers = dedupeMembers(teamMembers);
|
|
86
|
+
return uniqueMembers
|
|
65
87
|
.filter((member) => !isExcludedAuthor(member, exclude, excludeBots))
|
|
66
88
|
.map((member) => {
|
|
67
|
-
const authored = pullRequests.filter((pr) => pr.author
|
|
89
|
+
const authored = pullRequests.filter((pr) => usernameMatches(pr.author, member) &&
|
|
68
90
|
pr.created_at >= since &&
|
|
69
91
|
pr.created_at <= until &&
|
|
70
92
|
!isExcludedAuthor(pr.author, exclude, excludeBots));
|
|
71
93
|
const reviewed = reviewEvents.filter((event) => event.type === 'review_submitted' &&
|
|
72
|
-
event.reviewer
|
|
94
|
+
usernameMatches(event.reviewer, member) &&
|
|
73
95
|
!isExcludedAuthor(event.reviewer, exclude, excludeBots));
|
|
74
|
-
const memberCommits = commits.filter((commit) => commit.author
|
|
96
|
+
const memberCommits = commits.filter((commit) => usernameMatches(commit.author, member) &&
|
|
75
97
|
commit.committed_at >= since &&
|
|
76
98
|
commit.committed_at <= until &&
|
|
77
99
|
!isExcludedAuthor(commit.author, exclude, excludeBots));
|
|
@@ -86,11 +108,32 @@ function buildMemberBreakdown(teamMembers, pullRequests, commits, reviewEvents,
|
|
|
86
108
|
})
|
|
87
109
|
.toSorted((a, b) => a.user.localeCompare(b.user));
|
|
88
110
|
}
|
|
89
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Find blockers (stale/stuck PRs) with configurable age filtering.
|
|
113
|
+
*
|
|
114
|
+
* A PR is included in the blocker list if:
|
|
115
|
+
* - It had activity (was updated) within the time range, OR
|
|
116
|
+
* - It was created within the time range, OR
|
|
117
|
+
* - blocker_max_age_days is 0 (disabled), OR
|
|
118
|
+
* - The PR is younger than blocker_max_age_days
|
|
119
|
+
*/
|
|
120
|
+
function findBlockers(pullRequests, includeDrafts, exclude, excludeBots, thresholds, now, timeRange) {
|
|
121
|
+
const maxAgeDays = thresholds.blocker_max_age_days;
|
|
90
122
|
return pullRequests
|
|
91
123
|
.filter((pr) => pr.state === 'open')
|
|
92
124
|
.filter((pr) => includeDrafts || !pr.draft)
|
|
93
125
|
.filter((pr) => !isExcludedAuthor(pr.author, exclude, excludeBots))
|
|
126
|
+
.filter((pr) => {
|
|
127
|
+
// If max age is disabled (0), include all
|
|
128
|
+
if (maxAgeDays === 0)
|
|
129
|
+
return true;
|
|
130
|
+
const prAgeDays = diffDays(pr.created_at, now);
|
|
131
|
+
const hadActivityInRange = pr.updated_at >= timeRange.start && pr.updated_at <= timeRange.end;
|
|
132
|
+
const createdInRange = pr.created_at >= timeRange.start && pr.created_at <= timeRange.end;
|
|
133
|
+
const withinMaxAge = prAgeDays <= maxAgeDays;
|
|
134
|
+
// Include if: had recent activity, was created in range, or is within max age
|
|
135
|
+
return hadActivityInRange || createdInRange || withinMaxAge;
|
|
136
|
+
})
|
|
94
137
|
.flatMap((pr) => {
|
|
95
138
|
const blockers = [];
|
|
96
139
|
const summary = buildPullRequestSummary(pr, now);
|
|
@@ -109,8 +152,11 @@ function findBlockers(pullRequests, includeDrafts, exclude, excludeBots, thresho
|
|
|
109
152
|
function diffDays(start, end) {
|
|
110
153
|
return Math.floor((end - start) / (24 * 60 * 60 * 1000));
|
|
111
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Case-insensitive check if author is in team.
|
|
157
|
+
*/
|
|
112
158
|
function teamHasAuthor(team, author) {
|
|
113
|
-
return team.has(author);
|
|
159
|
+
return team.has(author.toLowerCase());
|
|
114
160
|
}
|
|
115
161
|
function prAuthorInTeam(lookup, team, repo, number, exclude, excludeBots) {
|
|
116
162
|
const pr = lookup.get(`${repo}#${number}`);
|
package/dist/reports/types.d.cts
CHANGED
package/dist/reports/types.d.ts
CHANGED
package/dist/repos.cjs
CHANGED
|
@@ -8,9 +8,25 @@ const picomatch_1 = __importDefault(require("picomatch"));
|
|
|
8
8
|
async function resolveRepos(options) {
|
|
9
9
|
const orgs = uniqueStrings([...(options.config.orgs ?? []), ...(options.orgs ?? [])]);
|
|
10
10
|
const repoPatterns = options.repoPatterns ?? [];
|
|
11
|
+
const { cache, forceSync } = options;
|
|
12
|
+
const cacheTtlHours = options.config.cache.ttl_hours;
|
|
11
13
|
const repoSet = new Set();
|
|
12
14
|
// orgs can include GitHub orgs OR usernames - we try org first, fall back to user
|
|
13
15
|
for (const [index, org] of orgs.entries()) {
|
|
16
|
+
// Check cache first (unless forceSync)
|
|
17
|
+
if (cache && !forceSync) {
|
|
18
|
+
const cached = cache.getRepoList(org);
|
|
19
|
+
if (cached && cache.isFresh(cached.fetched_at, cacheTtlHours)) {
|
|
20
|
+
options.onProgress?.({
|
|
21
|
+
phase: 'resolve',
|
|
22
|
+
message: `Using cached repos for ${org}`,
|
|
23
|
+
current: index + 1,
|
|
24
|
+
total: orgs.length,
|
|
25
|
+
});
|
|
26
|
+
cached.repos.forEach((repo) => repoSet.add(repo));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
14
30
|
options.onProgress?.({
|
|
15
31
|
phase: 'resolve',
|
|
16
32
|
message: `Resolving repos for ${org}`,
|
|
@@ -19,6 +35,10 @@ async function resolveRepos(options) {
|
|
|
19
35
|
});
|
|
20
36
|
const repos = await listOwnerRepos(options.github, org);
|
|
21
37
|
repos.forEach((repo) => repoSet.add(repo));
|
|
38
|
+
// Cache the result
|
|
39
|
+
if (cache) {
|
|
40
|
+
cache.upsertRepoList(org, repos);
|
|
41
|
+
}
|
|
22
42
|
}
|
|
23
43
|
for (const pattern of options.config.repos ?? []) {
|
|
24
44
|
if (pattern.includes('*')) {
|
|
@@ -26,11 +46,36 @@ async function resolveRepos(options) {
|
|
|
26
46
|
if (!owner) {
|
|
27
47
|
throw new Error(`Invalid repo pattern: ${pattern}`);
|
|
28
48
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
49
|
+
// Check cache first (unless forceSync)
|
|
50
|
+
let repos;
|
|
51
|
+
if (cache && !forceSync) {
|
|
52
|
+
const cached = cache.getRepoList(owner);
|
|
53
|
+
if (cached && cache.isFresh(cached.fetched_at, cacheTtlHours)) {
|
|
54
|
+
options.onProgress?.({
|
|
55
|
+
phase: 'resolve',
|
|
56
|
+
message: `Using cached repos for ${owner}`,
|
|
57
|
+
});
|
|
58
|
+
repos = cached.repos;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
options.onProgress?.({
|
|
62
|
+
phase: 'resolve',
|
|
63
|
+
message: `Resolving repos for ${owner}`,
|
|
64
|
+
});
|
|
65
|
+
repos = await listOwnerRepos(options.github, owner);
|
|
66
|
+
cache.upsertRepoList(owner, repos);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
options.onProgress?.({
|
|
71
|
+
phase: 'resolve',
|
|
72
|
+
message: `Resolving repos for ${owner}`,
|
|
73
|
+
});
|
|
74
|
+
repos = await listOwnerRepos(options.github, owner);
|
|
75
|
+
if (cache) {
|
|
76
|
+
cache.upsertRepoList(owner, repos);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
34
79
|
const matcher = (0, picomatch_1.default)(pattern);
|
|
35
80
|
repos.filter((repo) => matcher(repo)).forEach((repo) => repoSet.add(repo));
|
|
36
81
|
continue;
|
package/dist/repos.d.cts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import type { Cache } from "./cache.cjs";
|
|
1
2
|
import type { Config } from "./config.cjs";
|
|
2
3
|
import type { GitHubClient } from "./github.cjs";
|
|
3
4
|
import type { ProgressEvent, RepoFullName } from "./types.cjs";
|
|
4
5
|
export interface ResolveReposOptions {
|
|
5
6
|
config: Config;
|
|
6
7
|
github: GitHubClient;
|
|
8
|
+
cache?: Cache;
|
|
7
9
|
orgs?: string[];
|
|
8
10
|
repoPatterns?: string[];
|
|
9
11
|
strict?: boolean;
|
|
12
|
+
forceSync?: boolean;
|
|
10
13
|
onProgress?: (event: ProgressEvent) => void;
|
|
11
14
|
}
|
|
12
15
|
export declare function resolveRepos(options: ResolveReposOptions): Promise<RepoFullName[]>;
|
package/dist/repos.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import type { Cache } from "./cache.js";
|
|
1
2
|
import type { Config } from "./config.js";
|
|
2
3
|
import type { GitHubClient } from "./github.js";
|
|
3
4
|
import type { ProgressEvent, RepoFullName } from "./types.js";
|
|
4
5
|
export interface ResolveReposOptions {
|
|
5
6
|
config: Config;
|
|
6
7
|
github: GitHubClient;
|
|
8
|
+
cache?: Cache;
|
|
7
9
|
orgs?: string[];
|
|
8
10
|
repoPatterns?: string[];
|
|
9
11
|
strict?: boolean;
|
|
12
|
+
forceSync?: boolean;
|
|
10
13
|
onProgress?: (event: ProgressEvent) => void;
|
|
11
14
|
}
|
|
12
15
|
export declare function resolveRepos(options: ResolveReposOptions): Promise<RepoFullName[]>;
|
package/dist/repos.js
CHANGED
|
@@ -2,9 +2,25 @@ import picomatch from 'picomatch';
|
|
|
2
2
|
export async function resolveRepos(options) {
|
|
3
3
|
const orgs = uniqueStrings([...(options.config.orgs ?? []), ...(options.orgs ?? [])]);
|
|
4
4
|
const repoPatterns = options.repoPatterns ?? [];
|
|
5
|
+
const { cache, forceSync } = options;
|
|
6
|
+
const cacheTtlHours = options.config.cache.ttl_hours;
|
|
5
7
|
const repoSet = new Set();
|
|
6
8
|
// orgs can include GitHub orgs OR usernames - we try org first, fall back to user
|
|
7
9
|
for (const [index, org] of orgs.entries()) {
|
|
10
|
+
// Check cache first (unless forceSync)
|
|
11
|
+
if (cache && !forceSync) {
|
|
12
|
+
const cached = cache.getRepoList(org);
|
|
13
|
+
if (cached && cache.isFresh(cached.fetched_at, cacheTtlHours)) {
|
|
14
|
+
options.onProgress?.({
|
|
15
|
+
phase: 'resolve',
|
|
16
|
+
message: `Using cached repos for ${org}`,
|
|
17
|
+
current: index + 1,
|
|
18
|
+
total: orgs.length,
|
|
19
|
+
});
|
|
20
|
+
cached.repos.forEach((repo) => repoSet.add(repo));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
8
24
|
options.onProgress?.({
|
|
9
25
|
phase: 'resolve',
|
|
10
26
|
message: `Resolving repos for ${org}`,
|
|
@@ -13,6 +29,10 @@ export async function resolveRepos(options) {
|
|
|
13
29
|
});
|
|
14
30
|
const repos = await listOwnerRepos(options.github, org);
|
|
15
31
|
repos.forEach((repo) => repoSet.add(repo));
|
|
32
|
+
// Cache the result
|
|
33
|
+
if (cache) {
|
|
34
|
+
cache.upsertRepoList(org, repos);
|
|
35
|
+
}
|
|
16
36
|
}
|
|
17
37
|
for (const pattern of options.config.repos ?? []) {
|
|
18
38
|
if (pattern.includes('*')) {
|
|
@@ -20,11 +40,36 @@ export async function resolveRepos(options) {
|
|
|
20
40
|
if (!owner) {
|
|
21
41
|
throw new Error(`Invalid repo pattern: ${pattern}`);
|
|
22
42
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
// Check cache first (unless forceSync)
|
|
44
|
+
let repos;
|
|
45
|
+
if (cache && !forceSync) {
|
|
46
|
+
const cached = cache.getRepoList(owner);
|
|
47
|
+
if (cached && cache.isFresh(cached.fetched_at, cacheTtlHours)) {
|
|
48
|
+
options.onProgress?.({
|
|
49
|
+
phase: 'resolve',
|
|
50
|
+
message: `Using cached repos for ${owner}`,
|
|
51
|
+
});
|
|
52
|
+
repos = cached.repos;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
options.onProgress?.({
|
|
56
|
+
phase: 'resolve',
|
|
57
|
+
message: `Resolving repos for ${owner}`,
|
|
58
|
+
});
|
|
59
|
+
repos = await listOwnerRepos(options.github, owner);
|
|
60
|
+
cache.upsertRepoList(owner, repos);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
options.onProgress?.({
|
|
65
|
+
phase: 'resolve',
|
|
66
|
+
message: `Resolving repos for ${owner}`,
|
|
67
|
+
});
|
|
68
|
+
repos = await listOwnerRepos(options.github, owner);
|
|
69
|
+
if (cache) {
|
|
70
|
+
cache.upsertRepoList(owner, repos);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
28
73
|
const matcher = picomatch(pattern);
|
|
29
74
|
repos.filter((repo) => matcher(repo)).forEach((repo) => repoSet.add(repo));
|
|
30
75
|
continue;
|
package/dist/team.cjs
CHANGED
|
@@ -21,8 +21,19 @@ async function resolveTeamMembers(options) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Deduplicate usernames case-insensitively.
|
|
26
|
+
* Preserves the first occurrence's casing (typically the canonical GitHub casing).
|
|
27
|
+
*/
|
|
24
28
|
function unique(values) {
|
|
25
|
-
|
|
29
|
+
const seen = new Map();
|
|
30
|
+
for (const value of values) {
|
|
31
|
+
const key = value.toLowerCase();
|
|
32
|
+
if (!seen.has(key)) {
|
|
33
|
+
seen.set(key, value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return Array.from(seen.values());
|
|
26
37
|
}
|
|
27
38
|
async function listOwnerMembers(github, owner) {
|
|
28
39
|
try {
|
package/dist/team.js
CHANGED
|
@@ -18,8 +18,19 @@ export async function resolveTeamMembers(options) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Deduplicate usernames case-insensitively.
|
|
23
|
+
* Preserves the first occurrence's casing (typically the canonical GitHub casing).
|
|
24
|
+
*/
|
|
21
25
|
function unique(values) {
|
|
22
|
-
|
|
26
|
+
const seen = new Map();
|
|
27
|
+
for (const value of values) {
|
|
28
|
+
const key = value.toLowerCase();
|
|
29
|
+
if (!seen.has(key)) {
|
|
30
|
+
seen.set(key, value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Array.from(seen.values());
|
|
23
34
|
}
|
|
24
35
|
async function listOwnerMembers(github, owner) {
|
|
25
36
|
try {
|