Skip to content

Commit 64192c9

Browse files
cdervclaude
andauthored
Fix gh-pages publish leaving stale worktrees on commit failure (#14046) (#14048)
Validate git user identity (name/email) before creating the publish worktree, and use `--force` on worktree removal to handle dirty state. Also checks GIT_AUTHOR_*/GIT_COMMITTER_* environment variables as fallback, matching git's own identity resolution behavior. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fbe6aa6 commit 64192c9

3 files changed

Lines changed: 52 additions & 3 deletions

File tree

news/changelog-1.9.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ All changes included in 1.9:
137137

138138
- ([#13414](https://github.com/quarto-dev/quarto-cli/issues/13414)): Be more forgiving when Confluence server returns malformed JSON response. (author: @m1no)
139139

140+
### `gh-pages`
141+
142+
- ([#14046](https://github.com/quarto-dev/quarto-cli/issues/14046)): Fix `quarto publish gh-pages` leaving stale worktrees when `git commit` fails due to missing `user.name`/`user.email` configuration. Git identity is now validated before worktree creation, and worktree cleanup uses `--force` to handle modified/untracked files.
143+
140144
## Lua API
141145

142146
- ([#13762](https://github.com/quarto-dev/quarto-cli/issues/13762)): Add `quarto.paths.typst()` to Quarto's Lua API to resolve Typst binary path in Lua filters and extensions consistently with Quarto itself. (author: @mcanouil)

src/core/git.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,35 @@ export async function gitBranchExists(
8787
return Promise.resolve(undefined);
8888
}
8989

90+
export async function gitUserIdentityConfigured(
91+
dir: string,
92+
): Promise<boolean> {
93+
const name = await execProcess({
94+
cmd: "git",
95+
args: ["config", "user.name"],
96+
cwd: dir,
97+
stdout: "piped",
98+
stderr: "piped",
99+
});
100+
const email = await execProcess({
101+
cmd: "git",
102+
args: ["config", "user.email"],
103+
cwd: dir,
104+
stdout: "piped",
105+
stderr: "piped",
106+
});
107+
108+
const hasName = (name.success && (name.stdout?.trim().length ?? 0) > 0) ||
109+
(Deno.env.get("GIT_AUTHOR_NAME")?.trim().length ?? 0) > 0 ||
110+
(Deno.env.get("GIT_COMMITTER_NAME")?.trim().length ?? 0) > 0;
111+
112+
const hasEmail = (email.success && (email.stdout?.trim().length ?? 0) > 0) ||
113+
(Deno.env.get("GIT_AUTHOR_EMAIL")?.trim().length ?? 0) > 0 ||
114+
(Deno.env.get("GIT_COMMITTER_EMAIL")?.trim().length ?? 0) > 0;
115+
116+
return hasName && hasEmail;
117+
}
118+
90119
export async function gitCmdOutput(
91120
dir: string,
92121
args: string[],

src/publish/gh-pages/gh-pages.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ import { joinUrl } from "../../core/url.ts";
2727
import { completeMessage, withSpinner } from "../../core/console.ts";
2828
import { renderForPublish } from "../common/publish.ts";
2929
import { RenderFlags } from "../../command/render/types.ts";
30-
import { gitBranchExists, gitCmds, gitVersion } from "../../core/git.ts";
30+
import {
31+
gitBranchExists,
32+
gitCmds,
33+
gitUserIdentityConfigured,
34+
gitVersion,
35+
} from "../../core/git.ts";
3136
import {
3237
anonymousAccount,
3338
gitHubContextForPublish,
3439
verifyContext,
3540
} from "../common/git.ts";
41+
import { throwUnableToPublish } from "../common/errors.ts";
3642
import { createTempContext } from "../../core/temp.ts";
3743
import { projectScratchPath } from "../../project/project-scratch.ts";
3844

@@ -116,6 +122,16 @@ async function publish(
116122
const ghContext = await gitHubContextForPublish(options.input);
117123
verifyContext(ghContext, "GitHub Pages");
118124

125+
// verify git user identity is configured (needed for commits in worktree)
126+
if (!await gitUserIdentityConfigured(input)) {
127+
throwUnableToPublish(
128+
"git user.name and/or user.email is not configured\n" +
129+
"(run 'git config user.name \"Your Name\"' and " +
130+
"'git config user.email \"you@example.com\"' to set them)",
131+
"GitHub Pages",
132+
);
133+
}
134+
119135
// create gh pages branch on remote and local if there is none yet
120136
const createGhPagesBranchRemote = !ghContext.ghPagesRemote;
121137
const createGhPagesBranchLocal = !ghContext.ghPagesLocal;
@@ -211,7 +227,7 @@ async function publish(
211227
const worktreePath = join(projectScratchPath(input), entry.name);
212228
await execProcess({
213229
cmd: "git",
214-
args: ["worktree", "remove", worktreePath],
230+
args: ["worktree", "remove", "--force", worktreePath],
215231
cwd: projectScratchPath(input),
216232
});
217233
removeIfExists(worktreePath);
@@ -423,7 +439,7 @@ async function withWorktree(
423439
} finally {
424440
await execProcess({
425441
cmd: "git",
426-
args: ["worktree", "remove", siteDir],
442+
args: ["worktree", "remove", "--force", siteDir],
427443
cwd: dir,
428444
});
429445
}

0 commit comments

Comments
 (0)