Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ github-code-search query "useFeatureFlag" --org my-org --group-by-team-prefix pl

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

**Skip template repositories**

```bash
github-code-search query "TODO" --org my-org --exclude-template-repositories
```

Exclude repositories that are marked as GitHub templates, so boilerplate repos don't clutter your results.

**Regex search — pattern-based code audit**

```bash
Expand Down
1 change: 1 addition & 0 deletions docs/reference/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ github-code-search completions [--shell <shell>]
| `--format <format>` | `markdown` \| `json` | ❌ | `markdown` | Output format. See [Output formats](/usage/output-formats). |
| `--output-type <type>` | `repo-and-matches` \| `repo-only` | ❌ | `repo-and-matches` | Controls output detail level. `repo-only` lists repository names only, without individual extracts. |
| `--include-archived` | boolean (flag) | ❌ | `false` | Include archived repositories in results (excluded by default). |
| `--exclude-template-repositories` | boolean (flag) | ❌ | `false` | Exclude template repositories from results (included by default). See [Filtering](/usage/filtering#--exclude-template-repositories). |
| `--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. |
| `--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`. |
| `--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). |
Expand Down
17 changes: 15 additions & 2 deletions docs/usage/filtering.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Filtering

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

## `--exclude-repositories`

Expand Down Expand Up @@ -64,13 +64,26 @@ github-code-search "useFeatureFlag" --org fulll --include-archived
Archived repos are silently filtered out in the aggregation step before the TUI is shown. This flag overrides that behaviour.
:::

## `--exclude-template-repositories`

By default, template repositories are included in results. Pass `--exclude-template-repositories` to filter them out:

Comment thread
shouze marked this conversation as resolved.
```bash
github-code-search "useFeatureFlag" --org fulll --exclude-template-repositories
```

::: info
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.
:::
Comment thread
shouze marked this conversation as resolved.

## Combining filters

All three flags can be combined freely:
All four flags can be combined freely:

```bash
github-code-search "useFeatureFlag" --org fulll \
--include-archived \
--exclude-template-repositories \
--exclude-repositories legacy-monolith \
--exclude-extracts billing-api:src/flags.ts:0
```
Expand Down
10 changes: 10 additions & 0 deletions github-code-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ function addSearchOptions(cmd: Command): Command {
"Include archived repositories in results (default: false)",
false,
)
.option(
"--exclude-template-repositories",
"Exclude template repositories from results (default: false)",
false,
)
.option(
"--group-by-team-prefix <prefixes>",
[
Expand Down Expand Up @@ -203,6 +208,7 @@ async function searchAction(
format: string;
outputType: string;
includeArchived: boolean;
excludeTemplateRepositories: boolean;
groupByTeamPrefix: string;
cache: boolean;
regexHint?: string;
Expand All @@ -219,6 +225,7 @@ async function searchAction(
const format: OutputFormat = opts.format === "json" ? "json" : "markdown";
const outputType: OutputType = opts.outputType === "repo-only" ? "repo-only" : "repo-and-matches";
const includeArchived = Boolean(opts.includeArchived);
const excludeTemplates = Boolean(opts.excludeTemplateRepositories);

const excludedRepos = new Set(
opts.excludeRepositories
Expand Down Expand Up @@ -305,6 +312,7 @@ async function searchAction(
excludedExtractRefs,
includeArchived,
regexFilter,
excludeTemplates,
);

// ─── Team-prefix grouping ─────────────────────────────────────────────────
Expand All @@ -327,6 +335,7 @@ async function searchAction(
console.log(
buildOutput(groups, query, org, excludedRepos, excludedExtractRefs, format, outputType, {
includeArchived,
excludeTemplates,
groupByTeamPrefix: opts.groupByTeamPrefix,
regexHint: opts.regexHint,
}),
Expand Down Expand Up @@ -378,6 +387,7 @@ async function searchAction(
format,
outputType,
includeArchived,
excludeTemplates,
opts.groupByTeamPrefix,
opts.regexHint ?? "",
);
Expand Down
24 changes: 23 additions & 1 deletion src/aggregate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ describe("extractRef", () => {

// ─── aggregate ───────────────────────────────────────────────────────────────

function makeMatch(repo: string, path: string, archived = false): CodeMatch {
function makeMatch(repo: string, path: string, archived = false, isTemplate = false): CodeMatch {
return {
path,
repoFullName: repo,
htmlUrl: `https://github.com/${repo}/blob/main/${path}`,
archived,
isTemplate,
textMatches: [],
};
}
Expand Down Expand Up @@ -136,6 +137,26 @@ describe("aggregate", () => {
const groups = aggregate(matches, new Set(), new Set());
expect(groups).toHaveLength(0);
});

it("excludes template repos when excludeTemplates = true", () => {
const matches: CodeMatch[] = [
makeMatch("myorg/repoA", "src/a.ts", false, false),
makeMatch("myorg/templateRepo", "src/b.ts", false, true),
];
const groups = aggregate(matches, new Set(), new Set(), false, null, true);
expect(groups).toHaveLength(1);
expect(groups[0].repoFullName).toBe("myorg/repoA");
});

it("includes template repos when excludeTemplates = false (default)", () => {
const matches: CodeMatch[] = [
makeMatch("myorg/repoA", "src/a.ts", false, false),
makeMatch("myorg/templateRepo", "src/b.ts", false, true),
];
const groups = aggregate(matches, new Set(), new Set());
expect(groups).toHaveLength(2);
expect(groups.map((g) => g.repoFullName)).toContain("myorg/templateRepo");
});
});

// ─── aggregate with regexFilter ───────────────────────────────────────────────
Expand All @@ -146,6 +167,7 @@ function makeMatchWithFragments(repo: string, path: string, fragments: string[])
repoFullName: repo,
htmlUrl: `https://github.com/${repo}/blob/main/${path}`,
archived: false,
isTemplate: false,
textMatches: fragments.map((fragment) => ({ fragment, matches: [] })),
};
}
Expand Down
2 changes: 2 additions & 0 deletions src/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export function aggregate(
excludedExtractRefs: Set<string>,
includeArchived = false,
regexFilter?: RegExp | null,
excludeTemplates = false,
): RepoGroup[] {
// Compile the global regex once per aggregate() call rather than once per
// fragment inside recomputeSegments — avoids repeated RegExp construction
Expand All @@ -97,6 +98,7 @@ export function aggregate(
for (const m of matches) {
if (excludedRepos.has(m.repoFullName)) continue;
if (!includeArchived && m.archived) continue;
if (excludeTemplates && m.isTemplate === true) continue;
// Fix: when a regex filter is active, replace each TextMatch's API-provided
// segments (which point at the literal search term) with segments derived
// from the actual regex match positions — see issue #111 / fix highlight bug
Expand Down
3 changes: 2 additions & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface RawTextMatch {
interface RawCodeItem {
path: string;
html_url: string;
repository: { full_name: string; archived?: boolean };
repository: { full_name: string; archived?: boolean; is_template?: boolean };
text_matches?: RawTextMatch[];
}

Expand Down Expand Up @@ -277,6 +277,7 @@ export async function fetchAllResults(
repoFullName: item.repository.full_name,
htmlUrl: item.html_url,
archived: item.repository.archived === true,
isTemplate: item.repository.is_template === true,
textMatches: (item.text_matches ?? []).map((m) => {
const fragment: string = m.fragment ?? "";
const fragmentStartLine = fileContent ? computeFragmentStartLine(fileContent, fragment) : 1;
Expand Down
6 changes: 6 additions & 0 deletions src/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ const OPTIONS = [
takesArg: false,
values: [],
},
{
flag: "exclude-template-repositories",
description: "Exclude template repositories",
takesArg: false,
values: [],
},
{
flag: "no-cache",
description: "Bypass the 24 h team-list cache",
Expand Down
22 changes: 18 additions & 4 deletions src/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe("buildReplayCommand", () => {
const groups = [makeGroup("myorg/repoA", ["a.ts"])];
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set());
expect(cmd).toContain(`github-code-search`);
expect(cmd).toContain(`--org ${ORG}`);
expect(cmd).toContain(`--org '${ORG}'`);
expect(cmd).toContain(`--no-interactive`);
});

Expand Down Expand Up @@ -117,7 +117,7 @@ describe("buildReplayCommand", () => {
}),
];
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set());
expect(cmd).toContain("--exclude-extracts repoA:b.ts:1");
expect(cmd).toContain("--exclude-extracts 'repoA:b.ts:1'");
});

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

it("includes --exclude-template-repositories when excludeTemplates is true", () => {
const groups = [makeGroup("myorg/repoA", ["a.ts"])];
const opts: ReplayOptions = { excludeTemplates: true };
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set(), opts);
expect(cmd).toContain("--exclude-template-repositories");
});

it("does not include --exclude-template-repositories when excludeTemplates is false (default)", () => {
const groups = [makeGroup("myorg/repoA", ["a.ts"])];
const opts: ReplayOptions = { excludeTemplates: false };
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set(), opts);
expect(cmd).not.toContain("--exclude-template-repositories");
});

it("includes --group-by-team-prefix when groupByTeamPrefix is set", () => {
const groups = [makeGroup("myorg/repoA", ["a.ts"])];
const opts: ReplayOptions = { groupByTeamPrefix: "squad-,chapter-" };
const cmd = buildReplayCommand(groups, QUERY, ORG, new Set(), new Set(), opts);
expect(cmd).toContain("--group-by-team-prefix squad-,chapter-");
expect(cmd).toContain("--group-by-team-prefix 'squad-,chapter-'");
});

it("does not include --group-by-team-prefix when groupByTeamPrefix is empty (default)", () => {
Expand Down Expand Up @@ -567,6 +581,6 @@ describe("buildOutput", () => {
groupByTeamPrefix: "squad-",
});
const parsed = JSON.parse(out);
expect(parsed.replayCommand).toContain("--group-by-team-prefix squad-");
expect(parsed.replayCommand).toContain("--group-by-team-prefix 'squad-'");
});
});
20 changes: 15 additions & 5 deletions src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ReplayOptions {
format?: OutputFormat;
outputType?: OutputType;
includeArchived?: boolean;
excludeTemplates?: boolean;
groupByTeamPrefix?: string;
/** When set, appends `--regex-hint <term>` to the replay command so the
* result set from a regex query can be reproduced exactly. */
Expand All @@ -49,8 +50,11 @@ export function buildReplayCommand(
// Fix: forward all input options so the replay command is fully reproducible — see issue #11
options: ReplayOptions = {},
): string {
const { format, outputType, includeArchived, groupByTeamPrefix, regexHint } = options;
const parts: string[] = [`github-code-search ${shellQuote(query)} --org ${org} --no-interactive`];
const { format, outputType, includeArchived, excludeTemplates, groupByTeamPrefix, regexHint } =
options;
const parts: string[] = [
`github-code-search ${shellQuote(query)} --org ${shellQuote(org)} --no-interactive`,
];
Comment thread
shouze marked this conversation as resolved.

const excludedReposList: string[] = [...excludedRepos].map((r) => shortRepo(r, org));
for (const group of groups) {
Expand Down Expand Up @@ -78,7 +82,7 @@ export function buildReplayCommand(
}
}
if (excludedExtractsList.length > 0) {
parts.push(`--exclude-extracts ${excludedExtractsList.join(",")}`);
parts.push(`--exclude-extracts ${shellQuote(excludedExtractsList.join(","))}`);
}

if (format && format !== "markdown") {
Expand All @@ -90,8 +94,11 @@ export function buildReplayCommand(
if (includeArchived) {
parts.push("--include-archived");
}
if (excludeTemplates) {
parts.push("--exclude-template-repositories");
}
Comment thread
shouze marked this conversation as resolved.
if (groupByTeamPrefix) {
parts.push(`--group-by-team-prefix ${groupByTeamPrefix}`);
parts.push(`--group-by-team-prefix ${shellQuote(groupByTeamPrefix)}`);
}
if (regexHint) {
parts.push(`--regex-hint ${shellQuote(regexHint)}`);
Expand Down Expand Up @@ -264,7 +271,10 @@ export function buildOutput(
excludedExtractRefs: Set<string>,
format: OutputFormat,
outputType: OutputType = "repo-and-matches",
extraOptions: Pick<ReplayOptions, "includeArchived" | "groupByTeamPrefix" | "regexHint"> = {},
extraOptions: Pick<
ReplayOptions,
"includeArchived" | "excludeTemplates" | "groupByTeamPrefix" | "regexHint"
> = {},
): string {
const options: ReplayOptions = { format, outputType, ...extraOptions };
if (format === "json") {
Expand Down
2 changes: 2 additions & 0 deletions src/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export async function runInteractive(
format: OutputFormat,
outputType: OutputType = "repo-and-matches",
includeArchived = false,
excludeTemplates = false,
groupByTeamPrefix = "",
regexHint = "",
): Promise<void> {
Expand Down Expand Up @@ -371,6 +372,7 @@ export async function runInteractive(
console.log(
buildOutput(groups, query, org, excludedRepos, excludedExtractRefs, format, outputType, {
includeArchived,
excludeTemplates,
groupByTeamPrefix,
regexHint: regexHint || undefined,
}),
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface CodeMatch {
htmlUrl: string;
textMatches: TextMatch[];
archived: boolean;
isTemplate?: boolean;
}

export interface RepoGroup {
Expand Down
Loading