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
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,41 @@ it.effect("bounds template reads and marks truncated content", () =>
}),
),
);

it.effect.each([
".github/pull_request_template.txt",
"PULL_REQUEST_TEMPLATE.TXT",
"docs/PuLl_ReQuEsT_TeMpLaTe.Md",
".github/PULL_REQUEST_TEMPLATE/template.TxT",
])("reads committed text and mixed-case template $0 from a subdirectory", (relativePath) =>
runWithTempDirectory((cwd) =>
Effect.gen(function* () {
const path = yield* Path.Path;
yield* runGit(cwd, ["config", "grep.patternType", "fixed"]);
yield* runGit(cwd, ["config", "color.grep", "always"]);
yield* writeTemplate(cwd, relativePath, "committed template");
yield* writeTemplate(cwd, "nested/README.md", "project");
for (let index = 0; index < 600; index += 1) {
yield* writeTemplate(cwd, `docs/${index}-${"a".repeat(180)}.md`, "guide");
}
yield* commitTemplates(cwd);
yield* writeTemplate(cwd, relativePath, "uncommitted template");

assert.strictEqual(
Option.getOrUndefined(yield* detectTemplate(path.join(cwd, "nested"))),
"committed template",
);
}),
),
);

it.effect("does not guess between Markdown and text directory templates", () =>
runWithTempDirectory((cwd) =>
Effect.gen(function* () {
yield* writeTemplate(cwd, ".github/PULL_REQUEST_TEMPLATE/a.md", "markdown");
yield* writeTemplate(cwd, ".github/PULL_REQUEST_TEMPLATE/b.TXT", "text");
yield* commitTemplates(cwd);
assert.isTrue(Option.isNone(yield* detectTemplate(cwd)));
}),
),
);
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ const TRUNCATION_MARKER = "[truncated]";
const TEMPLATE_PATHS = [
".github/pull_request_template.md",
".github/PULL_REQUEST_TEMPLATE.md",
".github/pull_request_template.txt",
".github/PULL_REQUEST_TEMPLATE.txt",
"pull_request_template.md",
"PULL_REQUEST_TEMPLATE.md",
"pull_request_template.txt",
"PULL_REQUEST_TEMPLATE.txt",
"docs/pull_request_template.md",
"docs/PULL_REQUEST_TEMPLATE.md",
"docs/pull_request_template.txt",
"docs/PULL_REQUEST_TEMPLATE.txt",
] as const;

const TEMPLATE_DIRECTORIES = [
Expand All @@ -23,8 +29,6 @@ const TEMPLATE_DIRECTORIES = [
"docs/PULL_REQUEST_TEMPLATE",
] as const;

const TREE_PATHS = [...TEMPLATE_PATHS, ...TEMPLATE_DIRECTORIES] as const;

type ExecuteGit = SourceControlHost.SourceControlHost["Service"]["git"]["execute"];

interface TemplateTreeEntry {
Expand Down Expand Up @@ -105,7 +109,7 @@ const readTemplateDirectory = Effect.fn("gitHubChangeRequestTemplate.readTemplat
return false;
}
const relativePath = entry.path.slice(prefix.length);
return !relativePath.includes("/") && relativePath.toLowerCase().endsWith(".md");
return !relativePath.includes("/") && /\.(?:md|txt)$/i.test(relativePath);
});

const templates: string[] = [];
Expand All @@ -132,10 +136,50 @@ export const detect = Effect.fn("gitHubChangeRequestTemplate.detect")(function*
) {
return yield* Effect.gen(function* () {
// Read committed regular blobs; worktree paths can become symlinks between validation and open.
// ls-tree does not support case-insensitive pathspecs; grep discovers only matching names.
const paths = yield* executeGit({
operation: "gitHubChangeRequestTemplate.findTemplatePaths",
cwd,
args: [
"grep",
"-G",
"--no-color",
"-l",
"-z",
"--full-name",
"--no-recurse-submodules",
"-e",
"^",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
treeish,
"--",
...TEMPLATE_PATHS.map((path) => `:(top,icase,literal)${path}`),
],
maxOutputBytes: TREE_LIST_MAX_BYTES,
}).pipe(
Effect.map((result) =>
result.stdoutTruncated
? []
: result.stdout
.split("\0")
.filter((path) => path.startsWith(`${treeish}:`))
.map((path) => path.slice(treeish.length + 1)),
),
// grep exits with status 1 when no nonempty templates match.
Effect.orElseSucceed(() => []),
);
const result = yield* executeGit({
operation: "gitHubChangeRequestTemplate.listTemplates",
cwd,
args: ["ls-tree", "-r", "-z", "--full-tree", treeish, "--", ...TREE_PATHS],
args: [
"ls-tree",
"-r",
"-z",
"--full-tree",
treeish,
"--",
...new Set([...TEMPLATE_PATHS, ...paths]),
...TEMPLATE_DIRECTORIES,
],
maxOutputBytes: TREE_LIST_MAX_BYTES,
appendTruncationMarker: true,
});
Expand All @@ -144,12 +188,20 @@ export const detect = Effect.fn("gitHubChangeRequestTemplate.detect")(function*
}

const entries = parseTemplateTreeEntries(result.stdout);
const entriesByPath = new Map(entries.map((entry) => [entry.path, entry]));
for (const templatePath of TEMPLATE_PATHS) {
const entry = entriesByPath.get(templatePath);
if (!entry) {
continue;
}
const candidates = [".github/", "", "docs/"].flatMap((location) => {
const paths = TEMPLATE_PATHS.filter(
(path) => path.slice(0, path.lastIndexOf("/") + 1) === location,
);
return [
...paths.flatMap((path) => entries.filter((entry) => entry.path === path)),
...entries.filter(
(entry) =>
!paths.some((path) => path === entry.path) &&
paths.some((path) => path.toLowerCase() === entry.path.toLowerCase()),
),
];
});
for (const entry of candidates) {
const template = yield* readTemplateBlob({ cwd, executeGit, entry });
if (Option.isSome(template)) {
return template;
Expand Down
Loading