fix: escape rewrite replacement, diagnostic no-op commit, doc + askpass hardening
- Use function replacer in rewriteBody to prevent $& / $$ / $` / $' pattern corruption when alt text or siteUrl contains dollar-sign sequences - Detect empty staged index after `git add -A` and throw a clear "No changes to publish" error instead of a cryptic git failure - Correct README flat-slug description: single image → <slug>.<ext>, multiple → <slug>-1.<ext>, <slug>-2.<ext> (original filename discarded) - Harden askpass dir resolution: show a Notice and return early if getFullPath is absent (desktop-only guard), rather than passing a bad path Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ An Obsidian plugin that publishes the active note as a Jekyll blog post via git.
|
|||||||
|
|
||||||
## Image strategies
|
## Image strategies
|
||||||
|
|
||||||
**flat-slug** — all images for the post are copied flat into `<images-dir>/` and renamed to `<slug>-<original-name>`. Keeps the images directory shallow; suitable for sites with few images per post.
|
**flat-slug** — all images for the post are copied flat into `<images-dir>/`. A single image is renamed to `<slug>.<ext>`; multiple images are renamed to `<slug>-1.<ext>`, `<slug>-2.<ext>`, … (the original Obsidian filename is discarded). Keeps the images directory shallow; suitable for sites with few images per post.
|
||||||
|
|
||||||
**per-post-folder** — images are copied into `<images-dir>/<slug>/` preserving their original filenames. Keeps each post's images grouped together; suitable for posts with many images.
|
**per-post-folder** — images are copied into `<images-dir>/<slug>/` preserving their original filenames. Keeps each post's images grouped together; suitable for posts with many images.
|
||||||
|
|
||||||
|
|||||||
@@ -50,3 +50,22 @@ test("second sync resets cleanly (fetch+reset path)", async () => {
|
|||||||
execFileSync("git", ["clone", bare, verify]);
|
execFileSync("git", ["clone", bare, verify]);
|
||||||
expect(readFileSync(join(verify, "_posts/second.md"), "utf8")).toBe("x");
|
expect(readFileSync(join(verify, "_posts/second.md"), "utf8")).toBe("x");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("commitAndPush rejects with diagnostic error when nothing is staged", async () => {
|
||||||
|
// Use a fresh base dir so we get a clean clone
|
||||||
|
const base2 = join(root, "work2");
|
||||||
|
mkdirSync(base2, { recursive: true });
|
||||||
|
const client = new ChildProcessGitClient({ baseDir: base2 });
|
||||||
|
// Clone and write a file, then commit it
|
||||||
|
await client.syncClone({ url: bare, branch: "main" });
|
||||||
|
await client.writeFiles([{ repoPath: "_posts/no-change.md", data: "same content" }]);
|
||||||
|
await client.commitAndPush({ message: "initial no-change", branch: "main", authorName: "t", authorEmail: "t@t" });
|
||||||
|
|
||||||
|
// Now sync again — tree now matches origin, re-writing the same content stages nothing
|
||||||
|
const client2 = new ChildProcessGitClient({ baseDir: base2 });
|
||||||
|
await client2.syncClone({ url: bare, branch: "main" });
|
||||||
|
await client2.writeFiles([{ repoPath: "_posts/no-change.md", data: "same content" }]);
|
||||||
|
await expect(
|
||||||
|
client2.commitAndPush({ message: "noop", branch: "main", authorName: "t", authorEmail: "t@t" })
|
||||||
|
).rejects.toThrow(/no changes to publish/i);
|
||||||
|
});
|
||||||
|
|||||||
10
src/git.ts
10
src/git.ts
@@ -66,6 +66,16 @@ export class ChildProcessGitClient implements GitClient {
|
|||||||
|
|
||||||
async commitAndPush(o: { message: string; branch: string; authorName?: string; authorEmail?: string }): Promise<void> {
|
async commitAndPush(o: { message: string; branch: string; authorName?: string; authorEmail?: string }): Promise<void> {
|
||||||
await this.git(this.workdir, ["add", "-A"]);
|
await this.git(this.workdir, ["add", "-A"]);
|
||||||
|
let hasChanges = false;
|
||||||
|
try {
|
||||||
|
await this.git(this.workdir, ["diff", "--cached", "--quiet"]);
|
||||||
|
// exit 0 => no staged changes
|
||||||
|
} catch {
|
||||||
|
hasChanges = true; // exit 1 => staged changes exist
|
||||||
|
}
|
||||||
|
if (!hasChanges) {
|
||||||
|
throw new Error("No changes to publish: the post and images are identical to what is already committed.");
|
||||||
|
}
|
||||||
const cfg: string[] = [];
|
const cfg: string[] = [];
|
||||||
if (o.authorName) cfg.push("-c", `user.name=${o.authorName}`);
|
if (o.authorName) cfg.push("-c", `user.name=${o.authorName}`);
|
||||||
if (o.authorEmail) cfg.push("-c", `user.email=${o.authorEmail}`);
|
if (o.authorEmail) cfg.push("-c", `user.email=${o.authorEmail}`);
|
||||||
|
|||||||
@@ -67,4 +67,12 @@ describe("rewriteBody", () => {
|
|||||||
"\n\n"
|
"\n\n"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("alt text containing $ patterns is preserved verbatim (no replacement-pattern corruption)", () => {
|
||||||
|
const slug = "my-post";
|
||||||
|
const body = "";
|
||||||
|
const refs = findImageRefs(body);
|
||||||
|
const { rewrittenBody } = planImages(refs, { slug, strategy: "flat-slug", imagesDir: "assets/img", body });
|
||||||
|
expect(rewrittenBody).toBe("");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function rewriteBody(body: string, refs: ImageRef[], plan: ImagePlanItem[
|
|||||||
for (const ref of refs) {
|
for (const ref of refs) {
|
||||||
const siteUrl = url.get(ref.linktext);
|
const siteUrl = url.get(ref.linktext);
|
||||||
if (!siteUrl) continue;
|
if (!siteUrl) continue;
|
||||||
out = out.replace(ref.raw, ``);
|
out = out.replace(ref.raw, () => ``);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,10 +44,13 @@ export default class JekyllPublishPlugin extends Plugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dir = normalizePath(this.app.vault.configDir + "/plugins/jekyll-publish");
|
const dir = normalizePath(this.app.vault.configDir + "/plugins/jekyll-publish");
|
||||||
|
const adapter = this.app.vault.adapter as any;
|
||||||
|
if (typeof adapter.getFullPath !== "function") {
|
||||||
|
new Notice("Jekyll Publish requires desktop Obsidian (filesystem access unavailable).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const bridge = new AskpassBridge({
|
const bridge = new AskpassBridge({
|
||||||
dir: (this.app.vault.adapter as any).getFullPath
|
dir: adapter.getFullPath(dir),
|
||||||
? (this.app.vault.adapter as any).getFullPath(dir)
|
|
||||||
: dir,
|
|
||||||
onPrompt: (prompt) => this.promptCredential(prompt),
|
onPrompt: (prompt) => this.promptCredential(prompt),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user