Skip to content

Commit 214555e

Browse files
committed
Add --exclude-template-repositories option
Adds a new CLI flag that filters out GitHub template repositories from search results. Template repos are marked with is_template in the GitHub API response. - Add isTemplate: boolean to CodeMatch in types.ts - Map is_template from GitHub API in api.ts - Add excludeTemplates param to aggregate() with guard - Thread excludeTemplates through output.ts (ReplayOptions, buildReplayCommand, buildOutput) - Add excludeTemplates param to runInteractive() in tui.ts - Register --exclude-template-repositories CLI flag and wire through searchAction in github-code-search.ts - Add shell completion entry in completions.ts - Add unit tests for the new filter in aggregate.test.ts - Update docs: filtering.md, cli-options.md, README.md Closes #116
1 parent ed80a1f commit 214555e

File tree

11 files changed

+79
-5
lines changed

11 files changed

+79
-5
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ github-code-search query "useFeatureFlag" --org my-org --group-by-team-prefix pl
8383

8484
Get a team-scoped view of every usage site before refactoring a shared hook or utility.
8585

86+
**Skip template repositories**
87+
88+
```bash
89+
github-code-search query "TODO" --org my-org --exclude-template-repositories
90+
```
91+
92+
Exclude repositories that are marked as GitHub templates, so boilerplate repos don't clutter your results.
93+
8694
**Regex search — pattern-based code audit**
8795

8896
```bash

docs/reference/cli-options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ github-code-search completions [--shell <shell>]
4040
| `--format <format>` | `markdown` \| `json` || `markdown` | Output format. See [Output formats](/usage/output-formats). |
4141
| `--output-type <type>` | `repo-and-matches` \| `repo-only` || `repo-and-matches` | Controls output detail level. `repo-only` lists repository names only, without individual extracts. |
4242
| `--include-archived` | boolean (flag) || `false` | Include archived repositories in results (excluded by default). |
43+
| `--exclude-template-repositories` | boolean (flag) || `false` | Exclude template repositories from results (included by default). See [Filtering](/usage/filtering#--exclude-template-repositories). |
4344
| `--group-by-team-prefix <prefixes>` | string || `""` | Comma-separated team-name prefixes for grouping result repos by GitHub team (e.g. `squad-,chapter-`). Requires `read:org` scope. |
4445
| `--no-cache` | boolean (flag) || `true` (on) | Bypass the 24 h team-list cache and re-fetch teams from GitHub. Cache is **on** by default; pass this flag to disable it. Only applies with `--group-by-team-prefix`. |
4546
| `--regex-hint <term>` | string ||| Override the API search term used when the query is a regex (`/pattern/`). Useful when auto-extraction produces a term that is too broad or too narrow. See [Regex queries](/usage/search-syntax#regex-queries). |

docs/usage/filtering.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,26 @@ github-code-search "useFeatureFlag" --org fulll --include-archived
6464
Archived repos are silently filtered out in the aggregation step before the TUI is shown. This flag overrides that behaviour.
6565
:::
6666

67+
## `--exclude-template-repositories`
68+
69+
By default, template repositories are included in results. Pass `--exclude-template-repositories` to filter them out:
70+
71+
```bash
72+
github-code-search "useFeatureFlag" --org fulll --exclude-template-repositories
73+
```
74+
75+
::: info
76+
Template repositories (marked as templates on GitHub) are filtered out in the aggregation step before the TUI is shown. Useful when your organisation uses template repos for boilerplate that should not appear in search results.
77+
:::
78+
6779
## Combining filters
6880

69-
All three flags can be combined freely:
81+
All four flags can be combined freely:
7082

7183
```bash
7284
github-code-search "useFeatureFlag" --org fulll \
7385
--include-archived \
86+
--exclude-template-repositories \
7487
--exclude-repositories legacy-monolith \
7588
--exclude-extracts billing-api:src/flags.ts:0
7689
```

github-code-search.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ function addSearchOptions(cmd: Command): Command {
166166
"Include archived repositories in results (default: false)",
167167
false,
168168
)
169+
.option(
170+
"--exclude-template-repositories",
171+
"Exclude template repositories from results (default: false)",
172+
false,
173+
)
169174
.option(
170175
"--group-by-team-prefix <prefixes>",
171176
[
@@ -203,6 +208,7 @@ async function searchAction(
203208
format: string;
204209
outputType: string;
205210
includeArchived: boolean;
211+
excludeTemplateRepositories: boolean;
206212
groupByTeamPrefix: string;
207213
cache: boolean;
208214
regexHint?: string;
@@ -219,6 +225,7 @@ async function searchAction(
219225
const format: OutputFormat = opts.format === "json" ? "json" : "markdown";
220226
const outputType: OutputType = opts.outputType === "repo-only" ? "repo-only" : "repo-and-matches";
221227
const includeArchived = Boolean(opts.includeArchived);
228+
const excludeTemplates = Boolean(opts.excludeTemplateRepositories);
222229

223230
const excludedRepos = new Set(
224231
opts.excludeRepositories
@@ -305,6 +312,7 @@ async function searchAction(
305312
excludedExtractRefs,
306313
includeArchived,
307314
regexFilter,
315+
excludeTemplates,
308316
);
309317

310318
// ─── Team-prefix grouping ─────────────────────────────────────────────────
@@ -327,6 +335,7 @@ async function searchAction(
327335
console.log(
328336
buildOutput(groups, query, org, excludedRepos, excludedExtractRefs, format, outputType, {
329337
includeArchived,
338+
excludeTemplates,
330339
groupByTeamPrefix: opts.groupByTeamPrefix,
331340
regexHint: opts.regexHint,
332341
}),
@@ -378,6 +387,7 @@ async function searchAction(
378387
format,
379388
outputType,
380389
includeArchived,
390+
excludeTemplates,
381391
opts.groupByTeamPrefix,
382392
opts.regexHint ?? "",
383393
);

src/aggregate.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ describe("extractRef", () => {
4040

4141
// ─── aggregate ───────────────────────────────────────────────────────────────
4242

43-
function makeMatch(repo: string, path: string, archived = false): CodeMatch {
43+
function makeMatch(repo: string, path: string, archived = false, isTemplate = false): CodeMatch {
4444
return {
4545
path,
4646
repoFullName: repo,
4747
htmlUrl: `https://github.com/${repo}/blob/main/${path}`,
4848
archived,
49+
isTemplate,
4950
textMatches: [],
5051
};
5152
}
@@ -136,6 +137,26 @@ describe("aggregate", () => {
136137
const groups = aggregate(matches, new Set(), new Set());
137138
expect(groups).toHaveLength(0);
138139
});
140+
141+
it("excludes template repos when excludeTemplates = true", () => {
142+
const matches: CodeMatch[] = [
143+
makeMatch("myorg/repoA", "src/a.ts", false, false),
144+
makeMatch("myorg/templateRepo", "src/b.ts", false, true),
145+
];
146+
const groups = aggregate(matches, new Set(), new Set(), false, null, true);
147+
expect(groups).toHaveLength(1);
148+
expect(groups[0].repoFullName).toBe("myorg/repoA");
149+
});
150+
151+
it("includes template repos when excludeTemplates = false (default)", () => {
152+
const matches: CodeMatch[] = [
153+
makeMatch("myorg/repoA", "src/a.ts", false, false),
154+
makeMatch("myorg/templateRepo", "src/b.ts", false, true),
155+
];
156+
const groups = aggregate(matches, new Set(), new Set());
157+
expect(groups).toHaveLength(2);
158+
expect(groups.map((g) => g.repoFullName)).toContain("myorg/templateRepo");
159+
});
139160
});
140161

141162
// ─── aggregate with regexFilter ───────────────────────────────────────────────
@@ -146,6 +167,7 @@ function makeMatchWithFragments(repo: string, path: string, fragments: string[])
146167
repoFullName: repo,
147168
htmlUrl: `https://github.com/${repo}/blob/main/${path}`,
148169
archived: false,
170+
isTemplate: false,
149171
textMatches: fragments.map((fragment) => ({ fragment, matches: [] })),
150172
};
151173
}

src/aggregate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export function aggregate(
8585
excludedExtractRefs: Set<string>,
8686
includeArchived = false,
8787
regexFilter?: RegExp | null,
88+
excludeTemplates = false,
8889
): RepoGroup[] {
8990
// Compile the global regex once per aggregate() call rather than once per
9091
// fragment inside recomputeSegments — avoids repeated RegExp construction
@@ -97,6 +98,7 @@ export function aggregate(
9798
for (const m of matches) {
9899
if (excludedRepos.has(m.repoFullName)) continue;
99100
if (!includeArchived && m.archived) continue;
101+
if (excludeTemplates && m.isTemplate) continue;
100102
// Fix: when a regex filter is active, replace each TextMatch's API-provided
101103
// segments (which point at the literal search term) with segments derived
102104
// from the actual regex match positions — see issue #111 / fix highlight bug

src/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface RawTextMatch {
1818
interface RawCodeItem {
1919
path: string;
2020
html_url: string;
21-
repository: { full_name: string; archived?: boolean };
21+
repository: { full_name: string; archived?: boolean; is_template?: boolean };
2222
text_matches?: RawTextMatch[];
2323
}
2424

@@ -277,6 +277,7 @@ export async function fetchAllResults(
277277
repoFullName: item.repository.full_name,
278278
htmlUrl: item.html_url,
279279
archived: item.repository.archived === true,
280+
isTemplate: item.repository.is_template === true,
280281
textMatches: (item.text_matches ?? []).map((m) => {
281282
const fragment: string = m.fragment ?? "";
282283
const fragmentStartLine = fileContent ? computeFragmentStartLine(fileContent, fragment) : 1;

src/completions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ const OPTIONS = [
5757
takesArg: false,
5858
values: [],
5959
},
60+
{
61+
flag: "exclude-template-repositories",
62+
description: "Exclude template repositories",
63+
takesArg: false,
64+
values: [],
65+
},
6066
{
6167
flag: "no-cache",
6268
description: "Bypass the 24 h team-list cache",

src/output.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface ReplayOptions {
3232
format?: OutputFormat;
3333
outputType?: OutputType;
3434
includeArchived?: boolean;
35+
excludeTemplates?: boolean;
3536
groupByTeamPrefix?: string;
3637
/** When set, appends `--regex-hint <term>` to the replay command so the
3738
* result set from a regex query can be reproduced exactly. */
@@ -49,7 +50,8 @@ export function buildReplayCommand(
4950
// Fix: forward all input options so the replay command is fully reproducible — see issue #11
5051
options: ReplayOptions = {},
5152
): string {
52-
const { format, outputType, includeArchived, groupByTeamPrefix, regexHint } = options;
53+
const { format, outputType, includeArchived, excludeTemplates, groupByTeamPrefix, regexHint } =
54+
options;
5355
const parts: string[] = [`github-code-search ${shellQuote(query)} --org ${org} --no-interactive`];
5456

5557
const excludedReposList: string[] = [...excludedRepos].map((r) => shortRepo(r, org));
@@ -90,6 +92,9 @@ export function buildReplayCommand(
9092
if (includeArchived) {
9193
parts.push("--include-archived");
9294
}
95+
if (excludeTemplates) {
96+
parts.push("--exclude-template-repositories");
97+
}
9398
if (groupByTeamPrefix) {
9499
parts.push(`--group-by-team-prefix ${groupByTeamPrefix}`);
95100
}
@@ -264,7 +269,10 @@ export function buildOutput(
264269
excludedExtractRefs: Set<string>,
265270
format: OutputFormat,
266271
outputType: OutputType = "repo-and-matches",
267-
extraOptions: Pick<ReplayOptions, "includeArchived" | "groupByTeamPrefix" | "regexHint"> = {},
272+
extraOptions: Pick<
273+
ReplayOptions,
274+
"includeArchived" | "excludeTemplates" | "groupByTeamPrefix" | "regexHint"
275+
> = {},
268276
): string {
269277
const options: ReplayOptions = { format, outputType, ...extraOptions };
270278
if (format === "json") {

src/tui.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export async function runInteractive(
108108
format: OutputFormat,
109109
outputType: OutputType = "repo-and-matches",
110110
includeArchived = false,
111+
excludeTemplates = false,
111112
groupByTeamPrefix = "",
112113
regexHint = "",
113114
): Promise<void> {
@@ -371,6 +372,7 @@ export async function runInteractive(
371372
console.log(
372373
buildOutput(groups, query, org, excludedRepos, excludedExtractRefs, format, outputType, {
373374
includeArchived,
375+
excludeTemplates,
374376
groupByTeamPrefix,
375377
regexHint: regexHint || undefined,
376378
}),

0 commit comments

Comments
 (0)