🎉 初始提交 - Auto Git Sync VS Code扩展

这个提交包含在:
kozyax 2026-06-08 16:59:50 +08:00
当前提交 c05e688a04
共有 7 个文件被更改,包括 607 次插入0 次删除

5
.gitignore vendored 普通文件
查看文件

@ -0,0 +1,5 @@
node_modules/
out/
*.vsix
*.js.map
.vscode/

9
.vscodeignore 普通文件
查看文件

@ -0,0 +1,9 @@
.vscode/**
.vscode-test/**
src/**
.gitignore
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
node_modules/**

21
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.

58
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"
}
}
}

85
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"
}
}

414
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<typeof setTimeout> | 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<boolean>('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<number>('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<number>('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<boolean>('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<boolean>('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<string>('giteaUrl', 'http://localhost:3000');
const giteaToken = config.get<string>('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<void> {
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();
}
}

15
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"]
}