Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/publish/huggingface/huggingface.ts
6460 views
1
/*
2
* huggingface.ts
3
*
4
* Copyright (C) 2020-2024 Posit Software, PBC
5
*/
6
7
import { info } from "../../deno_ral/log.ts";
8
import { dirname, join } from "../../deno_ral/path.ts";
9
import * as colors from "fmt/colors";
10
import { ProjectContext } from "../../project/types.ts";
11
import {
12
AccountToken,
13
PublishFiles,
14
PublishProvider,
15
} from "../provider-types.ts";
16
import { PublishOptions, PublishRecord } from "../types.ts";
17
import { RenderFlags } from "../../command/render/types.ts";
18
import { gitCmds, gitVersion } from "../../core/git.ts";
19
import {
20
anonymousAccount,
21
gitHubContextForPublish,
22
verifyContext,
23
} from "../common/git.ts";
24
import { throwUnableToPublish } from "../common/errors.ts";
25
import { Input } from "cliffy/prompt/input.ts";
26
import { assert } from "testing/asserts";
27
import { Secret } from "cliffy/prompt/secret.ts";
28
29
export const kHuggingFace = "huggingface";
30
const kHuggingFaceDescription = "Hugging Face Spaces";
31
32
export const huggingfaceProvider: PublishProvider = {
33
name: kHuggingFace,
34
description: kHuggingFaceDescription,
35
requiresServer: false,
36
listOriginOnly: false,
37
accountTokens: () => Promise.resolve([anonymousAccount()]),
38
authorizeToken,
39
removeToken: () => {},
40
publishRecord,
41
resolveTarget: (
42
_account: AccountToken,
43
target: PublishRecord,
44
): Promise<PublishRecord | undefined> => Promise.resolve(target),
45
publish,
46
isUnauthorized: () => false,
47
isNotFound: () => false,
48
resolveProjectPath: (path: string) => join(path, "src"),
49
};
50
51
async function authorizeToken(options: PublishOptions) {
52
const ghContext = await gitHubContextForPublish(options.input);
53
const provider = "Hugging Face Spaces";
54
verifyContext(ghContext, provider);
55
56
if (
57
!ghContext.originUrl!.match(/^https:\/\/(.*:.*@)?huggingface.co\/spaces\//)
58
) {
59
throwUnableToPublish(
60
"the git repository does not appear to have a Hugging Face Space origin",
61
provider,
62
);
63
}
64
65
// good to go!
66
return Promise.resolve(anonymousAccount());
67
}
68
69
async function publishRecord(
70
input: string | ProjectContext,
71
): Promise<PublishRecord | undefined> {
72
const ghContext = await gitHubContextForPublish(input);
73
if (ghContext.ghPagesRemote) {
74
return {
75
id: kHuggingFace,
76
url: ghContext.siteUrl || ghContext.originUrl,
77
};
78
}
79
}
80
81
async function publish(
82
_account: AccountToken,
83
_type: "document" | "site",
84
input: string,
85
_title: string,
86
_slug: string,
87
_render: (flags?: RenderFlags) => Promise<PublishFiles>,
88
options: PublishOptions,
89
_target?: PublishRecord,
90
): Promise<[PublishRecord | undefined, URL | undefined]> {
91
// convert input to dir if necessary
92
input = Deno.statSync(input).isDirectory ? input : dirname(input);
93
94
// check if git version is new enough
95
const version = await gitVersion();
96
97
// git 2.17.0 appears to be the first to support git-worktree add --track
98
// https://github.com/git/git/blob/master/Documentation/RelNotes/2.17.0.txt#L368
99
if (version.compare("2.17.0") < 0) {
100
throw new Error(
101
"git version 2.17.0 or higher is required to publish to GitHub Pages",
102
);
103
}
104
105
// get context
106
const ghContext = await gitHubContextForPublish(options.input);
107
verifyContext(ghContext, "Hugging Face Spaces");
108
109
if (
110
!ghContext.originUrl!.match(/^https:\/\/.*:.*@huggingface.co\/spaces\//)
111
) {
112
const previousRemotePath = ghContext.originUrl!.match(
113
/.*huggingface.co(\/spaces\/.*)/,
114
);
115
assert(previousRemotePath);
116
info(colors.yellow([
117
"The current git repository needs to be reconfigured to allow `quarto publish`",
118
"to publish to Hugging Face Spaces. Please enter your username and authentication token.",
119
"Refer to https://huggingface.co/blog/password-git-deprecation#switching-to-personal-access-token",
120
"for more information on how to obtain a personal access token.",
121
].join("\n")));
122
const username = await Input.prompt({
123
indent: "",
124
message: "Hugging Face username",
125
});
126
const token = await Secret.prompt({
127
indent: "",
128
message: "Hugging Face authentication token:",
129
hint: "Create a token at https://huggingface.co/settings/tokens",
130
});
131
await gitCmds(input, [
132
[
133
"remote",
134
"set-url",
135
"origin",
136
`https://${username}:${token}@huggingface.co${previousRemotePath![1]}`,
137
],
138
]);
139
}
140
141
// sync from remote and push to main
142
await gitCmds(input, [
143
["stash"],
144
["fetch", "origin", "main"],
145
]);
146
try {
147
await gitCmds(input, [
148
["merge", "origin/main"],
149
]);
150
} catch (_e) {
151
info(colors.yellow([
152
"Could not merge origin/main. This is likely because of git conflicts.",
153
"Please resolve those manually and run `quarto publish` again.",
154
].join("\n")));
155
return Promise.resolve([
156
undefined,
157
undefined,
158
]);
159
}
160
try {
161
await gitCmds(input, [
162
["stash", "pop"],
163
]);
164
} catch (_e) {
165
info(colors.yellow([
166
"Could not pop git stash.",
167
"This is likely because there are no changes to push to the repository.",
168
].join("\n")));
169
return Promise.resolve([
170
undefined,
171
undefined,
172
]);
173
}
174
await gitCmds(input, [
175
["add", "-Af", "."],
176
["commit", "--allow-empty", "-m", "commit from `quarto publish`"],
177
["push", "origin", "main"],
178
]);
179
180
// warn users about latency between push and remote publish
181
info(colors.yellow(
182
"NOTE: Hugging Face Space sites build the content remotely and use caching.\n" +
183
"You will need to wait a moment for Hugging Face to rebuild your site, and\n" +
184
"then click the refresh button within your web browser to see changes after deployment.\n\n" +
185
"Specifically, you need to:\n" +
186
"- wait for your space's status to go from 'Building' to 'Running'\n" +
187
" (this is visible in the status bar above the space)\n" +
188
"- force-reload the web page by holding Shift and hitting the reload button in your browser.\n",
189
));
190
await new Promise((resolve) => setTimeout(resolve, 3000));
191
192
return Promise.resolve([
193
undefined,
194
new URL(ghContext.originUrl!),
195
]);
196
}
197
198