diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml index 52470be..da518ae 100644 --- a/.github/actions/setup-node-pnpm/action.yml +++ b/.github/actions/setup-node-pnpm/action.yml @@ -9,7 +9,7 @@ inputs: pnpm_version: description: 'pnpm version' required: false - default: '9.15.0' + default: '9.4.0' outputs: store_path: @@ -26,8 +26,6 @@ runs: - name: Setup pnpm uses: pnpm/action-setup@v4 - with: - version: ${{ inputs.pnpm_version }} - name: Get pnpm store directory id: get-store-path diff --git a/.gitignore b/.gitignore index 0dfb59a..a693147 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode out dist node_modules diff --git a/.vscode/settings.json b/.vscode/settings.json index 7ad7704..869b9da 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,8 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", + "terminal.integrated.env.linux": { + "PATH": "/config/.asdf/installs/nodejs/22.13.0/bin:${env:PATH}" + }, "github-actions.remote-name": "upstream" } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3cf99c3..a0c9b0c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -21,7 +21,7 @@ "type": "npm", "script": "watch:esbuild", "group": "build", - "problemMatcher": "$esbuild-watch", + "problemMatcher": "$tsc-watch", "isBackground": true, "label": "npm: watch:esbuild", "presentation": { diff --git a/package.json b/package.json index b080a47..269d133 100644 --- a/package.json +++ b/package.json @@ -167,5 +167,6 @@ "npm-run-all": "^4.1.5", "prettier": "3.6.2", "typescript": "^5.8.3" - } + }, + "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a" } diff --git a/src/githubService.ts b/src/githubService.ts index 60be943..867756f 100644 --- a/src/githubService.ts +++ b/src/githubService.ts @@ -1,6 +1,13 @@ import * as vscode from 'vscode'; import { API_BASE_URL } from './constants'; +// ===== 常量定义 ===== +const GITHUB_ACCEPT_HEADER = 'application/vnd.github.v3+json'; +const CONTENT_TYPE_JSON = 'application/json'; +const DEFAULT_GIST_DESCRIPTION = 'VSCode 配置同步'; +const DEFAULT_COMMIT_MESSAGE = '自动同步配置'; + +// ===== 接口定义 ===== interface GistResponse { id: string; html_url: string; @@ -20,6 +27,10 @@ interface GistBody { public?: boolean; } +interface FileDataResponse { + sha?: string; +} + export class GitHubService { private readonly baseUrl = API_BASE_URL; @@ -33,124 +44,78 @@ export class GitHubService { const method = gistId ? 'PATCH' : 'POST'; const body: GistBody = { - description: `VSCode 配置同步 - ${new Date().toLocaleString()}`, + description: `${DEFAULT_GIST_DESCRIPTION} - ${new Date().toLocaleString()}`, files: { [fileName]: { - content: content, + content, }, }, + public: false, }; - if (!gistId) { - body.public = false; - } + const headers = this.getAuthHeaders(token, CONTENT_TYPE_JSON); - const response = await fetch(url, { - method: method, - headers: { - Authorization: `token ${token}`, - Accept: 'application/vnd.github.v3+json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); + try { + const response = await fetch(url, { + method, + headers, + body: JSON.stringify(body), + }); - if (!response.ok) { - const error = await response.text(); - throw new Error(`GitHub API 错误: ${response.status} - ${error}`); - } + // 获取响应头信息 + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + await this.handleGitHubError(response); - const result = (await response.json()) as GistResponse; + const result = (await response.json()) as GistResponse; - // 如果是新创建的Gist,保存ID - if (!gistId && result.id) { - const config = vscode.workspace.getConfiguration('vscode-syncing'); - await config.update('gistId', result.id, vscode.ConfigurationTarget.Global); - vscode.window.showInformationMessage(`新的Gist已创建,ID: ${result.id}`); + if (!gistId && result.id) { + await this.saveGistIdToConfig(result.id); + } + + return result; + } catch (error) { + throw error; } - return result; } + // 更新仓库文件 async updateRepository( token: string, repoName: string, branch: string, fileName: string, content: string, - commitMessage: string, + commitMessage: string = DEFAULT_COMMIT_MESSAGE, ): Promise { - try { - // 获取文件的当前SHA(如果存在) - const fileUrl = `${this.baseUrl}/repos/${repoName}/contents/${fileName}?ref=${branch}`; - let sha: string | undefined; - - try { - const fileResponse = await fetch(fileUrl, { - headers: { - Authorization: `token ${token}`, - Accept: 'application/vnd.github.v3+json', - }, - }); - - if (fileResponse.ok) { - const fileData = await fileResponse.json(); - if ( - typeof fileData === 'object' && - fileData !== null && - 'sha' in fileData && - typeof fileData.sha === 'string' - ) { - sha = fileData.sha; - } - } - } catch (error) { - // 文件不存在,这是正常的 - } - - // 更新或创建文件 - interface RepoUpdateBody { - message: string; - content: string; - branch: string; - sha?: string; - } - const updateUrl = `${this.baseUrl}/repos/${repoName}/contents/${fileName}`; - const body: RepoUpdateBody = { - message: commitMessage, - content: Buffer.from(content).toString('base64'), - branch: branch, - }; - - if (sha) { - body.sha = sha; - } + const fileUrl = `${this.baseUrl}/repos/${repoName}/contents/${fileName}?ref=${branch}`; + const sha = await this.getFileSha(token, fileUrl); + + const updateUrl = `${this.baseUrl}/repos/${repoName}/contents/${fileName}`; + const body = { + message: commitMessage, + content: Buffer.from(content).toString('base64'), + branch, + ...(sha && { sha }), + }; - const response = await fetch(updateUrl, { - method: 'PUT', - headers: { - Authorization: `token ${token}`, - Accept: 'application/vnd.github.v3+json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); + const response = await fetch(updateUrl, { + method: 'PUT', + headers: this.getAuthHeaders(token, CONTENT_TYPE_JSON), + body: JSON.stringify(body), + }); - if (!response.ok) { - const error = await response.text(); - throw new Error(`GitHub API 错误: ${response.status} - ${error}`); - } - } catch (error) { - throw new Error(`更新仓库失败: ${error}`); - } + await this.handleGitHubError(response); } + // 测试连接 async testConnection(token: string): Promise { try { const response = await fetch(`${this.baseUrl}/user`, { - headers: { - Authorization: `token ${token}`, - Accept: 'application/vnd.github.v3+json', - }, + headers: this.getAuthHeaders(token), }); return response.ok; @@ -158,4 +123,53 @@ export class GitHubService { return false; } } + + // ===== 私有辅助方法 ===== + + private getAuthHeaders(token: string, contentType?: string): Record { + const headers: Record = { + Authorization: `token ${token}`, + Accept: GITHUB_ACCEPT_HEADER, + }; + + if (contentType) { + headers['Content-Type'] = contentType; + } + + return headers; + } + + private async getFileSha(token: string, fileUrl: string): Promise { + try { + const response = await fetch(fileUrl, { + headers: this.getAuthHeaders(token), + }); + + if (!response.ok) { + return undefined; + } + + const fileData = (await response.json()) as FileDataResponse; + + return typeof fileData === 'object' && fileData !== null && 'sha' in fileData + ? (fileData.sha as string) + : undefined; + } catch (error) { + console.warn('获取文件 SHA 时出错:', error); + return undefined; + } + } + + private async handleGitHubError(response: Response): Promise { + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`GitHub API 错误: ${response.status} - ${errorText}`); + } + } + + private async saveGistIdToConfig(gistId: string): Promise { + const config = vscode.workspace.getConfiguration('vscode-syncing'); + await config.update('gistId', gistId, vscode.ConfigurationTarget.Global); + vscode.window.showInformationMessage(`新的 Gist 已创建,ID: ${gistId}`); + } }