diff --git a/news/changelog-1.5.md b/news/changelog-1.5.md index 4eef93572f..fb045b9eb0 100644 --- a/news/changelog-1.5.md +++ b/news/changelog-1.5.md @@ -145,6 +145,7 @@ All changes included in 1.5: ## Publishing - ([#9308](https://github.com/quarto-dev/quarto-cli/issues/9308)): Improved error message when trying to publish to Github pages with `quarto publish gh-pages`. +- ([#9585](https://github.com/quarto-dev/quarto-cli/issues/9585)): Improved `quarto publish gh-pages` workflow when existing gh-pages branch is present or problem with the remote repository. ## `quarto inspect` diff --git a/src/core/git.ts b/src/core/git.ts index 6a102dd0ff..7749743931 100644 --- a/src/core/git.ts +++ b/src/core/git.ts @@ -64,3 +64,21 @@ export async function lsFiles( return Promise.resolve(undefined); } + +export async function gitBranchExists( + branch: string, + cwd?: string, +): Promise { + if (await which("git")) { + const result = await execProcess({ + cmd: ["git", "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], + cwd, + stdout: "piped", + stderr: "piped", + }); + + return result.code === 0; + } + + return Promise.resolve(undefined); +} diff --git a/src/core/github-types.ts b/src/core/github-types.ts index b04583847a..27399b5145 100644 --- a/src/core/github-types.ts +++ b/src/core/github-types.ts @@ -9,7 +9,8 @@ export type GitHubContext = { repo: boolean; originUrl?: string; repoUrl?: string; - ghPages?: boolean; + ghPagesRemote?: boolean; + ghPagesLocal?: boolean; siteUrl?: string; browse?: boolean; organization?: string; diff --git a/src/core/github.ts b/src/core/github.ts index 273469906f..fdaeda10b2 100644 --- a/src/core/github.ts +++ b/src/core/github.ts @@ -11,6 +11,7 @@ import { join } from "../deno_ral/path.ts"; import { existsSync } from "fs/mod.ts"; import { isHttpUrl } from "./url.ts"; import { GitHubContext } from "./github-types.ts"; +import { gitBranchExists } from "./git.ts"; export async function gitHubContext(dir: string) { // establish dir @@ -43,7 +44,7 @@ export async function gitHubContext(dir: string) { context.originUrl = result.stdout?.trim(); // check for a gh-pages branch - context.ghPages = (await execProcess({ + const ghPagesRemote = await execProcess({ cmd: [ "git", "ls-remote", @@ -54,7 +55,22 @@ export async function gitHubContext(dir: string) { ], stdout: "piped", stderr: "piped", - })).success; + }); + + context.ghPagesRemote = ghPagesRemote.success; + if (!ghPagesRemote.success) { + // when no gh-pages branch on remote, check local to avoid creation error + // as if local branch exists, we don't want to create a new one + // https://git-scm.com/docs/git-ls-remote#Documentation/git-ls-remote.txt---exit-code + if (ghPagesRemote.code === 2) { + context.ghPagesLocal = await gitBranchExists("gh-pages"); + } else { + // if we go there, this means something is not right with the remote origin + throw new Error( + `There is an error while retrieving information from remote 'origin'.\n Git error: ${ghPagesRemote.stderr}. \n Git status code: ${ghPagesRemote.code}.`, + ); + } + } // determine siteUrl context.siteUrl = siteUrl( diff --git a/src/publish/gh-pages/gh-pages.ts b/src/publish/gh-pages/gh-pages.ts index 1bf3fcfa7e..92b97e3d2d 100644 --- a/src/publish/gh-pages/gh-pages.ts +++ b/src/publish/gh-pages/gh-pages.ts @@ -27,7 +27,7 @@ import { joinUrl } from "../../core/url.ts"; import { completeMessage, withSpinner } from "../../core/console.ts"; import { renderForPublish } from "../common/publish.ts"; import { RenderFlags } from "../../command/render/types.ts"; -import { gitCmds, gitVersion } from "../../core/git.ts"; +import { gitBranchExists, gitCmds, gitVersion } from "../../core/git.ts"; import { anonymousAccount, gitHubContextForPublish, @@ -71,7 +71,7 @@ async function publishRecord( input: string | ProjectContext, ): Promise { const ghContext = await gitHubContextForPublish(input); - if (ghContext.ghPages) { + if (ghContext.ghPagesRemote) { return { id: "gh-pages", url: ghContext.siteUrl || ghContext.originUrl, @@ -114,17 +114,27 @@ async function publish( const ghContext = await gitHubContextForPublish(options.input); verifyContext(ghContext, "GitHub Pages"); - // create gh pages branch if there is none yet - const createGhPagesBranch = !ghContext.ghPages; - if (createGhPagesBranch) { + // create gh pages branch on remote and local if there is none yet + const createGhPagesBranchRemote = !ghContext.ghPagesRemote; + const createGhPagesBranchLocal = !ghContext.ghPagesLocal; + if (createGhPagesBranchRemote) { // confirm - const confirmed = await Confirm.prompt({ + let confirmed = await Confirm.prompt({ indent: "", message: `Publish site to ${ ghContext.siteUrl || ghContext.originUrl } using gh-pages?`, default: true, }); + if (confirmed && !createGhPagesBranchLocal) { + confirmed = await Confirm.prompt({ + indent: "", + message: + `A local gh-pages branch already exists. Should it be pushed to remote 'origin'?`, + default: true, + }); + } + if (!confirmed) { throw new Error(); } @@ -135,9 +145,29 @@ async function publish( } const oldBranch = await gitCurrentBranch(input); try { - await gitCreateGhPages(input); + // Create and push if necessary, or just push local branch + if (createGhPagesBranchLocal) { + await gitCreateGhPages(input); + } else { + await gitPushGhPages(input); + } + } catch { + // Something failed so clean up, i.e + // if we created the branch then delete it. + // Example of failure: Auth error on push (https://github.com/quarto-dev/quarto-cli/issues/9585) + if (createGhPagesBranchLocal && await gitBranchExists("gh-pages")) { + await gitCmds(input, [ + ["checkout", oldBranch], + ["branch", "-D", "gh-pages"], + ]); + } + throw new Error( + "Publishing to gh-pages with `quarto publish gh-pages` failed.", + ); } finally { - await gitCmds(input, [["checkout", oldBranch]]); + if (await gitCurrentBranch(input) !== oldBranch) { + await gitCmds(input, [["checkout", oldBranch]]); + } if (stash) { await gitStashApply(input); } @@ -191,7 +221,7 @@ async function publish( /^https:\/\/(.+?)\.github\.io\/$/, ); if (defaultSiteMatch) { - if (createGhPagesBranch) { + if (createGhPagesBranchRemote) { notifyGhPagesBranch = true; } else { try { @@ -207,7 +237,7 @@ async function publish( } // if this is an update then warn that updates may require a browser refresh - if (!createGhPagesBranch && !notifyGhPagesBranch) { + if (!createGhPagesBranchRemote && !notifyGhPagesBranch) { info(colors.yellow( "NOTE: GitHub Pages sites use caching so you might need to click the refresh\n" + "button within your web browser to see changes after deployment.\n", @@ -360,7 +390,14 @@ async function gitCreateGhPages(dir: string) { await gitCmds(dir, [ ["checkout", "--orphan", "gh-pages"], ["rm", "-rf", "--quiet", "."], - ["commit", "--allow-empty", "-m", `Initializing gh-pages branch`], - ["push", "origin", `HEAD:gh-pages`], + ["commit", "--allow-empty", "-m", "Initializing gh-pages branch"], ]); + await gitPushGhPages(dir); +} + +async function gitPushGhPages(dir: string) { + if (await gitCurrentBranch(dir) !== "gh-pages") { + await gitCmds(dir, [["checkout", "gh-pages"]]); + } + await gitCmds(dir, [["push", "origin", "HEAD:gh-pages"]]); } diff --git a/src/publish/huggingface/huggingface.ts b/src/publish/huggingface/huggingface.ts index 33e067e5a2..e2bb61e7ef 100644 --- a/src/publish/huggingface/huggingface.ts +++ b/src/publish/huggingface/huggingface.ts @@ -70,7 +70,7 @@ async function publishRecord( input: string | ProjectContext, ): Promise { const ghContext = await gitHubContextForPublish(input); - if (ghContext.ghPages) { + if (ghContext.ghPagesRemote) { return { id: kHuggingFace, url: ghContext.siteUrl || ghContext.originUrl,