Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/actions/setup-node-pnpm/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ inputs:
pnpm_version:
description: 'pnpm version'
required: false
default: '9.15.0'
default: '9.4.0'

outputs:
store_path:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vscode
out
dist
node_modules
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"type": "npm",
"script": "watch:esbuild",
"group": "build",
"problemMatcher": "$esbuild-watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"label": "npm: watch:esbuild",
"presentation": {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
196 changes: 105 additions & 91 deletions src/githubService.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,6 +27,10 @@ interface GistBody {
public?: boolean;
}

interface FileDataResponse {
sha?: string;
}

export class GitHubService {
private readonly baseUrl = API_BASE_URL;

Expand All @@ -33,129 +44,132 @@ 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<string, string> = {};
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<void> {
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<boolean> {
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;
} catch (error) {
return false;
}
}

// ===== 私有辅助方法 =====

private getAuthHeaders(token: string, contentType?: string): Record<string, string> {
const headers: Record<string, string> = {
Authorization: `token ${token}`,
Accept: GITHUB_ACCEPT_HEADER,
};

if (contentType) {
headers['Content-Type'] = contentType;
}

return headers;
}

private async getFileSha(token: string, fileUrl: string): Promise<string | undefined> {
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<void> {
if (!response.ok) {
const errorText = await response.text();
throw new Error(`GitHub API 错误: ${response.status} - ${errorText}`);
}
}

private async saveGistIdToConfig(gistId: string): Promise<void> {
const config = vscode.workspace.getConfiguration('vscode-syncing');
await config.update('gistId', gistId, vscode.ConfigurationTarget.Global);
vscode.window.showInformationMessage(`新的 Gist 已创建,ID: ${gistId}`);
}
}
Loading