Skip to content

Commit fef097b

Browse files
authored
Merge pull request #120 from fulll/feat/exclude-template-repositories
Add --exclude-template-repositories option
2 parents ed80a1f + 5326c3b commit fef097b

File tree

12 files changed

+103
-13
lines changed

12 files changed

+103
-13
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: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Filtering
22

3-
`github-code-search` provides three pre-query filtering options so you can exclude noise before results ever appear in the TUI or output.
3+
`github-code-search` provides four result filtering options so you can exclude noise from what appears in the TUI or output.
44

55
## `--exclude-repositories`
66

@@ -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 — both in interactive and non-interactive mode. 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 === true) 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.test.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe("buildReplayCommand", () => {
8484
const groups = [makeGroup("myorg/repoA", ["a.ts"])];
8585
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set());
8686
expect(cmd).toContain(`github-code-search`);
87-
expect(cmd).toContain(`--org ${ORG}`);
87+
expect(cmd).toContain(`--org '${ORG}'`);
8888
expect(cmd).toContain(`--no-interactive`);
8989
});
9090

@@ -117,7 +117,7 @@ describe("buildReplayCommand", () => {
117117
}),
118118
];
119119
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set());
120-
expect(cmd).toContain("--exclude-extracts repoA:b.ts:1");
120+
expect(cmd).toContain("--exclude-extracts 'repoA:b.ts:1'");
121121
});
122122

123123
it("does not double-add pre-existing exclusions", () => {
@@ -169,11 +169,25 @@ describe("buildReplayCommand", () => {
169169
expect(cmd).not.toContain("--include-archived");
170170
});
171171

172+
it("includes --exclude-template-repositories when excludeTemplates is true", () => {
173+
const groups = [makeGroup("myorg/repoA", ["a.ts"])];
174+
const opts: ReplayOptions = { excludeTemplates: true };
175+
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set(), opts);
176+
expect(cmd).toContain("--exclude-template-repositories");
177+
});
178+
179+
it("does not include --exclude-template-repositories when excludeTemplates is false (default)", () => {
180+
const groups = [makeGroup("myorg/repoA", ["a.ts"])];
181+
const opts: ReplayOptions = { excludeTemplates: false };
182+
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set(), opts);
183+
expect(cmd).not.toContain("--exclude-template-repositories");
184+
});
185+
172186
it("includes --group-by-team-prefix when groupByTeamPrefix is set", () => {
173187
const groups = [makeGroup("myorg/repoA", ["a.ts"])];
174188
const opts: ReplayOptions = { groupByTeamPrefix: "squad-,chapter-" };
175189
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set(), opts);
176-
expect(cmd).toContain("--group-by-team-prefix squad-,chapter-");
190+
expect(cmd).toContain("--group-by-team-prefix 'squad-,chapter-'");
177191
});
178192

179193
it("does not include --group-by-team-prefix when groupByTeamPrefix is empty (default)", () => {
@@ -567,6 +581,6 @@ describe("buildOutput", () => {
567581
groupByTeamPrefix: "squad-",
568582
});
569583
const parsed = JSON.parse(out);
570-
expect(parsed.replayCommand).toContain("--group-by-team-prefix squad-");
584+
expect(parsed.replayCommand).toContain("--group-by-team-prefix 'squad-'");
571585
});
572586
});

src/output.ts

Lines changed: 15 additions & 5 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,8 +50,11 @@ 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 parts: string[] = [`github-code-search ${shellQuote(query)} --org ${org} --no-interactive`];
53+
const { format, outputType, includeArchived, excludeTemplates, groupByTeamPrefix, regexHint } =
54+
options;
55+
const parts: string[] = [
56+
`github-code-search ${shellQuote(query)} --org ${shellQuote(org)} --no-interactive`,
57+
];
5458

5559
const excludedReposList: string[] = [...excludedRepos].map((r) => shortRepo(r, org));
5660
for (const group of groups) {
@@ -78,7 +82,7 @@ export function buildReplayCommand(
7882
}
7983
}
8084
if (excludedExtractsList.length > 0) {
81-
parts.push(`--exclude-extracts ${excludedExtractsList.join(",")}`);
85+
parts.push(`--exclude-extracts ${shellQuote(excludedExtractsList.join(","))}`);
8286
}
8387

8488
if (format && format !== "markdown") {
@@ -90,8 +94,11 @@ export function buildReplayCommand(
9094
if (includeArchived) {
9195
parts.push("--include-archived");
9296
}
97+
if (excludeTemplates) {
98+
parts.push("--exclude-template-repositories");
99+
}
93100
if (groupByTeamPrefix) {
94-
parts.push(`--group-by-team-prefix ${groupByTeamPrefix}`);
101+
parts.push(`--group-by-team-prefix ${shellQuote(groupByTeamPrefix)}`);
95102
}
96103
if (regexHint) {
97104
parts.push(`--regex-hint ${shellQuote(regexHint)}`);
@@ -264,7 +271,10 @@ export function buildOutput(
264271
excludedExtractRefs: Set<string>,
265272
format: OutputFormat,
266273
outputType: OutputType = "repo-and-matches",
267-
extraOptions: Pick<ReplayOptions, "includeArchived" | "groupByTeamPrefix" | "regexHint"> = {},
274+
extraOptions: Pick<
275+
ReplayOptions,
276+
"includeArchived" | "excludeTemplates" | "groupByTeamPrefix" | "regexHint"
277+
> = {},
268278
): string {
269279
const options: ReplayOptions = { format, outputType, ...extraOptions };
270280
if (format === "json") {

0 commit comments

Comments
 (0)