commit c05e688a04ebe0dce1f7c720feed9a165b79db27 Author: kozyax Date: Mon Jun 8 16:59:50 2026 +0800 🎉 初始提交 - Auto Git Sync VS Code扩展 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8744cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +out/ +*.vsix +*.js.map +.vscode/ diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..3324ec7 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +.vscode-test/** +src/** +.gitignore +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts +node_modules/** diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0f56a1f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 kozyax + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..44c6a65 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,58 @@ +{ + "name": "auto-git-sync", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "auto-git-sync", + "version": "1.0.0", + "devDependencies": { + "@types/node": "18.x", + "@types/vscode": "^1.85.0", + "typescript": "^5.3.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/vscode": { + "version": "1.120.0", + "resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.120.0.tgz", + "integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7e88f2e --- /dev/null +++ b/package.json @@ -0,0 +1,85 @@ +{ + "name": "auto-git-sync", + "displayName": "Auto Git Sync - 自动Git同步", + "description": "每行代码更改后自动提交并推送到你自己的Gitea服务器,按日期每天一个提交", + "version": "1.0.0", + "publisher": "kozyax", + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Other", + "SCM" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "autoGitSync.enable", + "title": "Auto Git: 启用自动同步" + }, + { + "command": "autoGitSync.disable", + "title": "Auto Git: 停用自动同步" + }, + { + "command": "autoGitSync.forceSync", + "title": "Auto Git: 立即同步" + }, + { + "command": "autoGitSync.initRepo", + "title": "Auto Git: 初始化仓库并推送到Gitea" + } + ], + "configuration": { + "title": "Auto Git Sync 配置", + "properties": { + "autoGitSync.giteaUrl": { + "type": "string", + "default": "http://localhost:3000", + "description": "Gitea 服务器地址" + }, + "autoGitSync.giteaToken": { + "type": "string", + "default": "", + "description": "Gitea 访问令牌 (在Gitea设置 -> 应用 -> 生成令牌)" + }, + "autoGitSync.autoPush": { + "type": "boolean", + "default": true, + "description": "提交后是否自动推送到远程" + }, + "autoGitSync.debounceSeconds": { + "type": "number", + "default": 5, + "description": "保存后等待几秒再提交(防抖)" + }, + "autoGitSync.dailyCommit": { + "type": "boolean", + "default": true, + "description": "按日期每天一个提交(追加到当天提交)" + }, + "autoGitSync.enabled": { + "type": "boolean", + "default": true, + "description": "是否启用自动同步" + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "pretest": "npm run compile && npm run lint", + "lint": "eslint src --ext ts" + }, + "devDependencies": { + "@types/vscode": "^1.85.0", + "@types/node": "18.x", + "typescript": "^5.3.0" + } +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..09568c1 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,414 @@ +import * as vscode from 'vscode'; +import { exec, execSync } from 'child_process'; +import * as path from 'path'; +import * as fs from 'fs'; + +let debounceTimer: ReturnType | undefined; +let statusBarItem: vscode.StatusBarItem; +let isEnabled = true; + +export function activate(context: vscode.ExtensionContext) { + console.log('Auto Git Sync 插件已激活!'); + + // 读取配置 + const config = vscode.workspace.getConfiguration('autoGitSync'); + isEnabled = config.get('enabled', true); + + // 创建状态栏 + statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); + updateStatusBar(); + statusBarItem.show(); + context.subscriptions.push(statusBarItem); + + // ========== 命令注册 ========== + + // 启用自动同步 + context.subscriptions.push( + vscode.commands.registerCommand('autoGitSync.enable', () => { + isEnabled = true; + config.update('enabled', true, true); + updateStatusBar(); + vscode.window.showInformationMessage('✅ Auto Git Sync 已启用!'); + }) + ); + + // 停用自动同步 + context.subscriptions.push( + vscode.commands.registerCommand('autoGitSync.disable', () => { + isEnabled = false; + config.update('enabled', false, true); + updateStatusBar(); + vscode.window.showInformationMessage('⏸️ Auto Git Sync 已停用!'); + }) + ); + + // 立即同步 + context.subscriptions.push( + vscode.commands.registerCommand('autoGitSync.forceSync', async () => { + await autoCommitAndPush('手动同步'); + }) + ); + + // 初始化仓库并推送到Gitea + context.subscriptions.push( + vscode.commands.registerCommand('autoGitSync.initRepo', async () => { + await initRepoAndPushToGitea(); + }) + ); + + // ========== 监听文件保存 ========== + context.subscriptions.push( + vscode.workspace.onDidSaveTextDocument((doc) => { + if (!isEnabled) { return; } + + // 只处理文件类型的文档 + if (doc.uri.scheme !== 'file') { return; } + + // 忽略.git目录下的文件 + const filePath = doc.uri.fsPath; + if (filePath.includes('.git') || filePath.includes('node_modules')) { return; } + + const debounceSeconds = config.get('debounceSeconds', 5); + + // 防抖:保存后等待几秒再提交 + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + statusBarItem.text = '$(sync~spin) Auto Git: 等待提交...'; + statusBarItem.tooltip = `代码已保存,${debounceSeconds}秒后自动提交`; + + debounceTimer = setTimeout(async () => { + await autoCommitAndPush(); + }, debounceSeconds * 1000); + }) + ); + + // ========== 监听文件创建/删除 ========== + context.subscriptions.push( + vscode.workspace.onDidCreateFiles(() => { + if (!isEnabled) { return; } + scheduleAutoSync(); + }) + ); + + context.subscriptions.push( + vscode.workspace.onDidDeleteFiles(() => { + if (!isEnabled) { return; } + scheduleAutoSync(); + }) + ); +} + +// ========== 核心功能 ========== + +/** + * 防抖调度自动同步 + */ +function scheduleAutoSync() { + const config = vscode.workspace.getConfiguration('autoGitSync'); + const debounceSeconds = config.get('debounceSeconds', 5); + + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + statusBarItem.text = '$(sync~spin) Auto Git: 等待提交...'; + + debounceTimer = setTimeout(async () => { + await autoCommitAndPush(); + }, debounceSeconds * 1000); +} + +/** + * 自动提交并推送 + */ +async function autoCommitAndPush(manualReason?: string) { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + return; + } + + const workspaceRoot = workspaceFolders[0].uri.fsPath; + const config = vscode.workspace.getConfiguration('autoGitSync'); + + // 检查是否是Git仓库 + if (!fs.existsSync(path.join(workspaceRoot, '.git'))) { + return; + } + + try { + // 检查是否有更改 + const hasChanges = checkHasChanges(workspaceRoot); + if (!hasChanges) { + updateStatusBar(); + return; + } + + statusBarItem.text = '$(sync~spin) Auto Git: 正在提交...'; + + // git add all + runGit(workspaceRoot, 'add -A'); + + // 生成提交信息 + const commitMsg = generateCommitMessage(manualReason); + + // git commit + const dailyCommit = config.get('dailyCommit', true); + + if (dailyCommit) { + // 按日期每天一个提交:先尝试amend到今天的提交 + const todayCommitExists = checkTodayCommitExists(workspaceRoot); + if (todayCommitExists) { + runGit(workspaceRoot, `commit --amend --no-edit --allow-empty`); + } else { + runGit(workspaceRoot, `commit -m "${commitMsg}" --allow-empty`); + } + } else { + runGit(workspaceRoot, `commit -m "${commitMsg}" --allow-empty`); + } + + // 自动推送 + const autoPush = config.get('autoPush', true); + if (autoPush) { + statusBarItem.text = '$(sync~spin) Auto Git: 正在推送...'; + runGit(workspaceRoot, 'push --force-with-lease'); + statusBarItem.text = '$(check) Auto Git: 已同步'; + vscode.window.setStatusBarMessage('✅ Auto Git: 已同步到服务器', 3000); + } else { + statusBarItem.text = '$(check) Auto Git: 已提交'; + } + + updateStatusBar(); + + } catch (error: any) { + console.error('Auto Git Sync 错误:', error); + statusBarItem.text = '$(error) Auto Git: 错误'; + statusBarItem.tooltip = `错误: ${error.message}`; + setTimeout(updateStatusBar, 5000); + } +} + +/** + * 生成提交信息 + */ +function generateCommitMessage(manualReason?: string): string { + const now = new Date(); + const dateStr = now.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + const timeStr = now.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + if (manualReason) { + return `📝 ${manualReason} - ${dateStr} ${timeStr}`; + } + + return `📋 自动同步 - ${dateStr} ${timeStr}`; +} + +/** + * 检查今天是否已有提交 + */ +function checkTodayCommitExists(workspaceRoot: string): boolean { + try { + const now = new Date(); + const todayStr = now.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + + // 获取最近一次提交的信息 + const lastMsg = runGit(workspaceRoot, 'log -1 --pretty=%s').trim(); + + // 如果今天已有提交(包含今天的日期),则amend + if (lastMsg.includes(todayStr) || lastMsg.includes('自动同步')) { + return true; + } + return false; + } catch { + return false; + } +} + +/** + * 检查是否有更改 + */ +function checkHasChanges(workspaceRoot: string): boolean { + try { + const status = runGit(workspaceRoot, 'status --porcelain'); + return status.trim().length > 0; + } catch { + return false; + } +} + +/** + * 运行Git命令 + */ +function runGit(cwd: string, command: string): string { + try { + return execSync(`git ${command}`, { + cwd, + encoding: 'utf-8', + timeout: 30000, + stdio: ['pipe', 'pipe', 'pipe'] + }); + } catch (error: any) { + // git commit 无更改时不报错 + if (command.startsWith('commit') && error.stderr?.includes('nothing to commit')) { + return ''; + } + throw new Error(`Git命令失败: git ${command}\n${error.stderr || error.message}`); + } +} + +/** + * 初始化仓库并推送到Gitea + */ +async function initRepoAndPushToGitea() { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + vscode.window.showErrorMessage('请先打开一个项目文件夹!'); + return; + } + + const workspaceRoot = workspaceFolders[0].uri.fsPath; + const config = vscode.workspace.getConfiguration('autoGitSync'); + const giteaUrl = config.get('giteaUrl', 'http://localhost:3000'); + const giteaToken = config.get('giteaToken', ''); + + // 输入Gitea用户名 + const username = await vscode.window.showInputBox({ + prompt: '请输入Gitea用户名', + placeHolder: '例如: kozyax', + value: 'kozyax' + }); + if (!username) { return; } + + // 输入仓库名 + const projectName = await vscode.window.showInputBox({ + prompt: '请输入仓库名称', + placeHolder: '例如: my-project', + value: path.basename(workspaceRoot) + }); + if (!projectName) { return; } + + try { + // 如果不是Git仓库,先初始化 + if (!fs.existsSync(path.join(workspaceRoot, '.git'))) { + runGit(workspaceRoot, 'init'); + vscode.window.showInformationMessage('✅ Git仓库已初始化'); + } + + // 创建远程仓库(通过Gitea API) + if (giteaToken) { + await createGiteaRepo(giteaUrl, giteaToken, username, projectName); + vscode.window.showInformationMessage('✅ Gitea远程仓库已创建'); + } + + // 设置远程地址 + const remoteUrl = `${giteaUrl}/${username}/${projectName}.git`; + try { + runGit(workspaceRoot, 'remote remove origin'); + } catch { /* remote可能不存在 */ } + runGit(workspaceRoot, `remote add origin ${remoteUrl}`); + + // 添加所有文件并提交 + runGit(workspaceRoot, 'add -A'); + try { + runGit(workspaceRoot, `commit -m "🎉 初始提交 - ${new Date().toLocaleDateString('zh-CN')}"`); + } catch { /* 可能没有文件 */ } + + // 推送 + runGit(workspaceRoot, 'branch -M main'); + runGit(workspaceRoot, 'push -u origin main'); + + vscode.window.showInformationMessage(`🎉 仓库已推送到 ${remoteUrl}`); + + } catch (error: any) { + vscode.window.showErrorMessage(`初始化失败: ${error.message}`); + } +} + +/** + * 通过Gitea API创建仓库 + */ +async function createGiteaRepo(giteaUrl: string, token: string, username: string, repoName: string): Promise { + const axios = await import('https'); + + return new Promise((resolve, reject) => { + const url = `${giteaUrl}/api/v1/user/repos`; + const data = JSON.stringify({ + name: repoName, + private: true, + auto_init: false, + description: `自动创建的仓库 - ${new Date().toLocaleDateString('zh-CN')}` + }); + + const parsedUrl = new URL(url); + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname, + method: 'POST', + headers: { + 'Authorization': `token ${token}`, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data) + }, + rejectUnauthorized: false + }; + + const req = axios.request(options, (res) => { + let body = ''; + res.on('data', (chunk: string) => { body += chunk; }); + res.on('end', () => { + if (res.statusCode === 201) { + resolve(); + } else if (res.statusCode === 409) { + // 仓库已存在 + resolve(); + } else { + reject(new Error(`创建仓库失败: ${res.statusCode} ${body}`)); + } + }); + }); + + req.on('error', (e: Error) => reject(e)); + req.write(data); + req.end(); + }); +} + +/** + * 更新状态栏 + */ +function updateStatusBar() { + if (isEnabled) { + statusBarItem.text = '$(git-commit) Auto Git: 已启用'; + statusBarItem.tooltip = 'Auto Git Sync 已启用 - 保存文件时自动提交推送'; + statusBarItem.command = 'autoGitSync.disable'; + statusBarItem.backgroundColor = undefined; + } else { + statusBarItem.text = '$(git-commit) Auto Git: 已停用'; + statusBarItem.tooltip = 'Auto Git Sync 已停用 - 点击启用'; + statusBarItem.command = 'autoGitSync.enable'; + statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); + } +} + +export function deactivate() { + if (debounceTimer) { + clearTimeout(debounceTimer); + } + if (statusBarItem) { + statusBarItem.dispose(); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2b95fa2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", ".vscode-test"] +}