🎉 初始提交 - 2026/6/8
这个提交包含在:
父节点
c05e688a04
当前提交
d2a9e04a2b
75
package.json
75
package.json
@ -2,7 +2,7 @@
|
|||||||
"name": "auto-git-sync",
|
"name": "auto-git-sync",
|
||||||
"displayName": "Auto Git Sync - 自动Git同步",
|
"displayName": "Auto Git Sync - 自动Git同步",
|
||||||
"description": "每行代码更改后自动提交并推送到你自己的Gitea服务器,按日期每天一个提交",
|
"description": "每行代码更改后自动提交并推送到你自己的Gitea服务器,按日期每天一个提交",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"publisher": "kozyax",
|
"publisher": "kozyax",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.85.0"
|
"vscode": "^1.85.0"
|
||||||
@ -31,9 +31,80 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "autoGitSync.initRepo",
|
"command": "autoGitSync.initRepo",
|
||||||
"title": "Auto Git: 初始化仓库并推送到Gitea"
|
"title": "Auto Git: 🏗️ 创建Git仓库并推送到Gitea"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.viewLog",
|
||||||
|
"title": "Auto Git: 📋 查看版本日志"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.restoreVersion",
|
||||||
|
"title": "Auto Git: ⏪ 恢复到指定版本"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.createBranch",
|
||||||
|
"title": "Auto Git: 🌿 创建新分支"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.switchBranch",
|
||||||
|
"title": "Auto Git: 🔀 切换分支"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.pullLatest",
|
||||||
|
"title": "Auto Git: ⬇️ 拉取最新代码"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"menus": {
|
||||||
|
"explorer/context": [
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.initRepo",
|
||||||
|
"when": "!explorerResourceIsFolder || explorerResourceIsFolder",
|
||||||
|
"group": "autoGitSync@1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.forceSync",
|
||||||
|
"group": "autoGitSync@2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.viewLog",
|
||||||
|
"group": "autoGitSync@3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.restoreVersion",
|
||||||
|
"group": "autoGitSync@4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.createBranch",
|
||||||
|
"group": "autoGitSync@5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.switchBranch",
|
||||||
|
"group": "autoGitSync@6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.pullLatest",
|
||||||
|
"group": "autoGitSync@7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.enable",
|
||||||
|
"group": "autoGitSync@8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.disable",
|
||||||
|
"group": "autoGitSync@9"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"editor/title/context": [
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.forceSync",
|
||||||
|
"group": "autoGitSync@1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "autoGitSync.viewLog",
|
||||||
|
"group": "autoGitSync@2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"title": "Auto Git Sync 配置",
|
"title": "Auto Git Sync 配置",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
733
src/extension.ts
733
src/extension.ts
@ -1,7 +1,9 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { exec, execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as http from 'http';
|
||||||
|
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
let statusBarItem: vscode.StatusBarItem;
|
let statusBarItem: vscode.StatusBarItem;
|
||||||
@ -49,10 +51,45 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化仓库并推送到Gitea
|
// 🏗️ 创建Git仓库并推送到Gitea
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand('autoGitSync.initRepo', async () => {
|
vscode.commands.registerCommand('autoGitSync.initRepo', async (clickedUri?: vscode.Uri) => {
|
||||||
await initRepoAndPushToGitea();
|
await initRepoAndPushToGitea(clickedUri);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 📋 查看版本日志
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('autoGitSync.viewLog', async (clickedUri?: vscode.Uri) => {
|
||||||
|
await viewVersionLog(clickedUri);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// ⏪ 恢复到指定版本
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('autoGitSync.restoreVersion', async (clickedUri?: vscode.Uri) => {
|
||||||
|
await restoreToVersion(clickedUri);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🌿 创建新分支
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('autoGitSync.createBranch', async (clickedUri?: vscode.Uri) => {
|
||||||
|
await createNewBranch(clickedUri);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔀 切换分支
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('autoGitSync.switchBranch', async (clickedUri?: vscode.Uri) => {
|
||||||
|
await switchBranch(clickedUri);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// ⬇️ 拉取最新代码
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('autoGitSync.pullLatest', async (clickedUri?: vscode.Uri) => {
|
||||||
|
await pullLatest(clickedUri);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -270,16 +307,630 @@ function runGit(cwd: string, command: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化仓库并推送到Gitea
|
* 获取工作区根目录(优先使用右键点击的目录)
|
||||||
*/
|
*/
|
||||||
async function initRepoAndPushToGitea() {
|
function getWorkspaceRoot(clickedUri?: vscode.Uri): string | undefined {
|
||||||
|
// 优先使用右键点击的目录/文件所在目录
|
||||||
|
if (clickedUri) {
|
||||||
|
const clickedPath = clickedUri.fsPath;
|
||||||
|
// 如果点击的是目录,直接使用
|
||||||
|
if (fs.existsSync(clickedPath) && fs.statSync(clickedPath).isDirectory()) {
|
||||||
|
return clickedPath;
|
||||||
|
}
|
||||||
|
// 如果点击的是文件,使用其所在目录
|
||||||
|
return path.dirname(clickedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有右键点击时,使用当前工作区
|
||||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||||
vscode.window.showErrorMessage('请先打开一个项目文件夹!');
|
vscode.window.showErrorMessage('❌ 请先打开一个项目文件夹!');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return workspaceFolders[0].uri.fsPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 📋 查看版本日志 ==========
|
||||||
|
|
||||||
|
interface GitCommit {
|
||||||
|
hash: string;
|
||||||
|
shortHash: string;
|
||||||
|
author: string;
|
||||||
|
date: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function viewVersionLog(clickedUri?: vscode.Uri) {
|
||||||
|
const workspaceRoot = getWorkspaceRoot(clickedUri);
|
||||||
|
if (!workspaceRoot) { return; }
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(workspaceRoot, '.git'))) {
|
||||||
|
vscode.window.showErrorMessage('❌ 当前项目不是Git仓库!请先使用"创建Git仓库"命令。');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceRoot = workspaceFolders[0].uri.fsPath;
|
try {
|
||||||
|
// 获取最近30条提交记录
|
||||||
|
const logOutput = runGit(workspaceRoot, 'log --max-count=30 --pretty=format:"%H|%h|%an|%ai|%s"');
|
||||||
|
|
||||||
|
if (!logOutput.trim()) {
|
||||||
|
vscode.window.showInformationMessage('📋 暂无提交记录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commits: GitCommit[] = logOutput.trim().split('\n').map(line => {
|
||||||
|
const parts = line.split('|');
|
||||||
|
return {
|
||||||
|
hash: parts[0] || '',
|
||||||
|
shortHash: parts[1] || '',
|
||||||
|
author: parts[2] || '',
|
||||||
|
date: parts[3] || '',
|
||||||
|
message: parts.slice(4).join('|') || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建Webview面板显示日志
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'gitVersionLog',
|
||||||
|
'📋 Git版本日志',
|
||||||
|
vscode.ViewColumn.One,
|
||||||
|
{ enableScripts: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.webview.html = getLogWebviewHtml(commits);
|
||||||
|
|
||||||
|
// 监听webview消息
|
||||||
|
panel.webview.onDidReceiveMessage(
|
||||||
|
async (msg) => {
|
||||||
|
if (msg.command === 'restore') {
|
||||||
|
panel.dispose();
|
||||||
|
await restoreToCommit(msg.hash, msg.message);
|
||||||
|
} else if (msg.command === 'viewDiff') {
|
||||||
|
await viewDiff(msg.hash);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
vscode.window.showErrorMessage(`❌ 获取版本日志失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogWebviewHtml(commits: GitCommit[]): string {
|
||||||
|
const commitRows = commits.map((c, index) => {
|
||||||
|
const isFirst = index === 0;
|
||||||
|
const tag = isFirst ? '<span style="background:#28a745;color:white;padding:2px 8px;border-radius:10px;font-size:11px;margin-left:8px;">最新</span>' : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="commit-item" data-hash="${c.hash}">
|
||||||
|
<div class="commit-header">
|
||||||
|
<span class="commit-hash">${c.shortHash}</span>
|
||||||
|
${tag}
|
||||||
|
<span class="commit-date">${c.date}</span>
|
||||||
|
</div>
|
||||||
|
<div class="commit-message">${c.message}</div>
|
||||||
|
<div class="commit-author">👤 ${c.author}</div>
|
||||||
|
<div class="commit-actions">
|
||||||
|
<button class="btn btn-diff" onclick="viewDiff('${c.hash}')">📊 查看差异</button>
|
||||||
|
${!isFirst ? `<button class="btn btn-restore" onclick="restoreVersion('${c.hash}', '${c.message.replace(/'/g, "\\'")}')">⏪ 恢复到此版本</button>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Git版本日志</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #4fc3f7;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 22px;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.commit-item {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.commit-item:hover {
|
||||||
|
border-color: #4fc3f7;
|
||||||
|
}
|
||||||
|
.commit-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.commit-hash {
|
||||||
|
background: #404040;
|
||||||
|
color: #4fc3f7;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.commit-date {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.commit-message {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
.commit-author {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.commit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 5px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.btn:hover { opacity: 0.85; }
|
||||||
|
.btn-diff {
|
||||||
|
background: #0d47a1;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-restore {
|
||||||
|
background: #b71c1c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
background: #1a3a5c;
|
||||||
|
border: 1px solid #264f78;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📋 Git版本日志</h1>
|
||||||
|
<div class="info">💡 点击"恢复到此版本"可将项目回退到该版本的状态。点击"查看差异"可查看该版本的改动内容。</div>
|
||||||
|
${commitRows}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const vscode = acquireVsCodeApi();
|
||||||
|
function restoreVersion(hash, message) {
|
||||||
|
if (confirm('确定要恢复到版本: ' + message + ' ?\\n\\n这将把项目文件恢复到该版本的状态。')) {
|
||||||
|
vscode.postMessage({ command: 'restore', hash: hash, message: message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function viewDiff(hash) {
|
||||||
|
vscode.postMessage({ command: 'viewDiff', hash: hash });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== ⏪ 恢复到指定版本 ==========
|
||||||
|
|
||||||
|
async function restoreToVersion(clickedUri?: vscode.Uri) {
|
||||||
|
const workspaceRoot = getWorkspaceRoot(clickedUri);
|
||||||
|
if (!workspaceRoot) { return; }
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(workspaceRoot, '.git'))) {
|
||||||
|
vscode.window.showErrorMessage('❌ 当前项目不是Git仓库!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取提交列表
|
||||||
|
const logOutput = runGit(workspaceRoot, 'log --max-count=20 --pretty=format:"%H|%h|%s"');
|
||||||
|
if (!logOutput.trim()) {
|
||||||
|
vscode.window.showInformationMessage('📋 暂无提交记录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commits = logOutput.trim().split('\n').map(line => {
|
||||||
|
const parts = line.split('|');
|
||||||
|
return {
|
||||||
|
hash: parts[0],
|
||||||
|
shortHash: parts[1],
|
||||||
|
message: parts.slice(2).join('|')
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = commits.map(c => ({
|
||||||
|
label: `$(git-commit) ${c.shortHash}`,
|
||||||
|
description: c.message,
|
||||||
|
detail: c.hash
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selected = await vscode.window.showQuickPick(items, {
|
||||||
|
placeHolder: '选择要恢复到的版本',
|
||||||
|
title: '⏪ 恢复到指定版本'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
await restoreToCommit(selected.detail, selected.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
vscode.window.showErrorMessage(`❌ 获取版本列表失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreToCommit(hash: string, message: string) {
|
||||||
|
const workspaceRoot = getWorkspaceRoot();
|
||||||
|
if (!workspaceRoot) { return; }
|
||||||
|
|
||||||
|
// 选择恢复方式
|
||||||
|
const restoreType = await vscode.window.showQuickPick([
|
||||||
|
{
|
||||||
|
label: '$(copy) 软恢复(保留更改在工作区)',
|
||||||
|
description: 'git reset --soft',
|
||||||
|
detail: '回退提交,但保留文件更改在工作区,可以重新编辑'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '$(file) 混合恢复(保留更改在暂存区)',
|
||||||
|
description: 'git reset --mixed',
|
||||||
|
detail: '回退提交,文件更改放回暂存区'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '$(trash) 硬恢复(丢弃所有更改)⚠️',
|
||||||
|
description: 'git reset --hard',
|
||||||
|
detail: '完全回退到该版本,丢弃之后的所有更改!不可恢复!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '$(branch) 创建新分支保留',
|
||||||
|
description: '从该版本创建新分支',
|
||||||
|
detail: '在该版本上创建新分支,当前分支不受影响'
|
||||||
|
}
|
||||||
|
], {
|
||||||
|
placeHolder: '选择恢复方式',
|
||||||
|
title: `⏪ 恢复到: ${message}`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!restoreType) { return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (restoreType.description === 'git reset --soft') {
|
||||||
|
runGit(workspaceRoot, `reset --soft ${hash}`);
|
||||||
|
vscode.window.showInformationMessage(`✅ 已软恢复到版本 ${hash.substring(0, 7)},更改保留在工作区`);
|
||||||
|
} else if (restoreType.description === 'git reset --mixed') {
|
||||||
|
runGit(workspaceRoot, `reset --mixed ${hash}`);
|
||||||
|
vscode.window.showInformationMessage(`✅ 已混合恢复到版本 ${hash.substring(0, 7)},更改在暂存区`);
|
||||||
|
} else if (restoreType.description === 'git reset --hard') {
|
||||||
|
// 硬恢复需要确认
|
||||||
|
const confirm = await vscode.window.showWarningMessage(
|
||||||
|
`⚠️ 确定要硬恢复吗?这将丢弃版本 ${hash.substring(0, 7)} 之后的所有更改,不可恢复!`,
|
||||||
|
{ modal: true },
|
||||||
|
'确认恢复'
|
||||||
|
);
|
||||||
|
if (confirm === '确认恢复') {
|
||||||
|
runGit(workspaceRoot, `reset --hard ${hash}`);
|
||||||
|
vscode.window.showInformationMessage(`✅ 已硬恢复到版本 ${hash.substring(0, 7)}`);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (restoreType.description === '从该版本创建新分支') {
|
||||||
|
const branchName = await vscode.window.showInputBox({
|
||||||
|
prompt: '请输入新分支名称',
|
||||||
|
placeHolder: '例如: feature/restore-version'
|
||||||
|
});
|
||||||
|
if (branchName) {
|
||||||
|
runGit(workspaceRoot, `checkout -b ${branchName} ${hash}`);
|
||||||
|
vscode.window.showInformationMessage(`✅ 已从版本 ${hash.substring(0, 7)} 创建并切换到新分支 ${branchName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推送到远程
|
||||||
|
const autoPush = vscode.workspace.getConfiguration('autoGitSync').get<boolean>('autoPush', true);
|
||||||
|
if (autoPush && restoreType.description !== '从该版本创建新分支') {
|
||||||
|
try {
|
||||||
|
runGit(workspaceRoot, 'push --force-with-lease');
|
||||||
|
vscode.window.showInformationMessage('✅ 已强制推送到远程');
|
||||||
|
} catch (e: any) {
|
||||||
|
vscode.window.showWarningMessage(`⚠️ 推送失败: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
vscode.window.showErrorMessage(`❌ 恢复失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 📊 查看差异 ==========
|
||||||
|
|
||||||
|
async function viewDiff(hash: string) {
|
||||||
|
const workspaceRoot = getWorkspaceRoot();
|
||||||
|
if (!workspaceRoot) { return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const diffOutput = runGit(workspaceRoot, `show --stat ${hash}`);
|
||||||
|
const fullDiff = runGit(workspaceRoot, `show ${hash}`);
|
||||||
|
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
'gitDiff',
|
||||||
|
`📊 版本差异 - ${hash.substring(0, 7)}`,
|
||||||
|
vscode.ViewColumn.One,
|
||||||
|
{ enableScripts: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.webview.html = getDiffWebviewHtml(hash, diffOutput, fullDiff);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
vscode.window.showErrorMessage(`❌ 查看差异失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiffWebviewHtml(hash: string, stat: string, diff: string): string {
|
||||||
|
// 转义HTML特殊字符
|
||||||
|
const escapeHtml = (str: string) => str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
|
||||||
|
const diffLines = escapeHtml(diff).split('\n').map(line => {
|
||||||
|
if (line.startsWith('+++') || line.startsWith('---')) {
|
||||||
|
return `<div class="diff-file">${line}</div>`;
|
||||||
|
} else if (line.startsWith('@@')) {
|
||||||
|
return `<div class="diff-hunk">${line}</div>`;
|
||||||
|
} else if (line.startsWith('+')) {
|
||||||
|
return `<div class="diff-add">${line}</div>`;
|
||||||
|
} else if (line.startsWith('-')) {
|
||||||
|
return `<div class="diff-remove">${line}</div>`;
|
||||||
|
} else {
|
||||||
|
return `<div class="diff-normal">${line}</div>`;
|
||||||
|
}
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>版本差异</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
h1 { color: #4fc3f7; margin-bottom: 10px; font-size: 18px; }
|
||||||
|
.stat {
|
||||||
|
background: #2d2d2d;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.diff-container {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.diff-file { color: #4fc3f7; font-weight: bold; padding: 4px 0; }
|
||||||
|
.diff-hunk { color: #7c7cff; padding: 4px 0; }
|
||||||
|
.diff-add { background: #1a3a1a; color: #6dff6d; padding: 1px 4px; }
|
||||||
|
.diff-remove { background: #3a1a1a; color: #ff6d6d; padding: 1px 4px; }
|
||||||
|
.diff-normal { color: #999; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📊 版本差异 - ${hash.substring(0, 7)}</h1>
|
||||||
|
<div class="stat">${escapeHtml(stat)}</div>
|
||||||
|
<div class="diff-container">${diffLines}</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 🌿 创建新分支 ==========
|
||||||
|
|
||||||
|
async function createNewBranch(clickedUri?: vscode.Uri) {
|
||||||
|
const workspaceRoot = getWorkspaceRoot(clickedUri);
|
||||||
|
if (!workspaceRoot) { return; }
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(workspaceRoot, '.git'))) {
|
||||||
|
vscode.window.showErrorMessage('❌ 当前项目不是Git仓库!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchName = await vscode.window.showInputBox({
|
||||||
|
prompt: '请输入新分支名称',
|
||||||
|
placeHolder: '例如: feature/new-feature',
|
||||||
|
title: '🌿 创建新分支'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!branchName) { return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先提交当前更改
|
||||||
|
if (checkHasChanges(workspaceRoot)) {
|
||||||
|
const shouldCommit = await vscode.window.showQuickPick([
|
||||||
|
{ label: '$(check) 提交更改后创建分支', description: '先提交当前更改' },
|
||||||
|
{ label: '$(x) 暂存更改后创建分支', description: 'git stash 保存更改' },
|
||||||
|
{ label: '$(warning) 直接创建分支', description: '更改将带到新分支' }
|
||||||
|
], { placeHolder: '当前有未提交的更改,如何处理?' });
|
||||||
|
|
||||||
|
if (!shouldCommit) { return; }
|
||||||
|
|
||||||
|
if (shouldCommit.description === '先提交当前更改') {
|
||||||
|
runGit(workspaceRoot, 'add -A');
|
||||||
|
const commitMsg = generateCommitMessage('创建分支前提交');
|
||||||
|
runGit(workspaceRoot, `commit -m "${commitMsg}" --allow-empty`);
|
||||||
|
} else if (shouldCommit.description === 'git stash 保存更改') {
|
||||||
|
runGit(workspaceRoot, 'stash');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runGit(workspaceRoot, `checkout -b ${branchName}`);
|
||||||
|
vscode.window.showInformationMessage(`✅ 已创建并切换到新分支: ${branchName}`);
|
||||||
|
|
||||||
|
// 推送新分支到远程
|
||||||
|
try {
|
||||||
|
runGit(workspaceRoot, `push -u origin ${branchName}`);
|
||||||
|
vscode.window.showInformationMessage(`✅ 新分支已推送到远程`);
|
||||||
|
} catch (e: any) {
|
||||||
|
vscode.window.showWarningMessage(`⚠️ 推送新分支失败: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
vscode.window.showErrorMessage(`❌ 创建分支失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 🔀 切换分支 ==========
|
||||||
|
|
||||||
|
async function switchBranch(clickedUri?: vscode.Uri) {
|
||||||
|
const workspaceRoot = getWorkspaceRoot(clickedUri);
|
||||||
|
if (!workspaceRoot) { return; }
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(workspaceRoot, '.git'))) {
|
||||||
|
vscode.window.showErrorMessage('❌ 当前项目不是Git仓库!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取所有分支
|
||||||
|
const branchOutput = runGit(workspaceRoot, 'branch -a');
|
||||||
|
const branches = branchOutput.trim().split('\n')
|
||||||
|
.map(b => b.replace('*', '').trim())
|
||||||
|
.filter(b => b && !b.includes('HEAD'));
|
||||||
|
|
||||||
|
if (branches.length === 0) {
|
||||||
|
vscode.window.showInformationMessage('📋 暂无其他分支');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBranch = runGit(workspaceRoot, 'branch --show-current').trim();
|
||||||
|
|
||||||
|
const items = branches.map(b => {
|
||||||
|
const isCurrent = b === currentBranch;
|
||||||
|
const isRemote = b.includes('remotes/');
|
||||||
|
const displayName = isRemote ? `$(cloud) ${b.replace('remotes/', '')}` : `$(git-branch) ${b}`;
|
||||||
|
return {
|
||||||
|
label: displayName,
|
||||||
|
description: isCurrent ? '(当前)' : '',
|
||||||
|
detail: b,
|
||||||
|
picked: isCurrent
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const selected = await vscode.window.showQuickPick(items, {
|
||||||
|
placeHolder: `当前分支: ${currentBranch} — 选择要切换到的分支`,
|
||||||
|
title: '🔀 切换分支'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selected && selected.detail !== currentBranch) {
|
||||||
|
let branchName = selected.detail;
|
||||||
|
|
||||||
|
// 处理未提交的更改
|
||||||
|
if (checkHasChanges(workspaceRoot)) {
|
||||||
|
const shouldStash = await vscode.window.showQuickPick([
|
||||||
|
{ label: '$(check) 提交更改后切换', description: '先提交当前更改' },
|
||||||
|
{ label: '$(archive) 暂存更改后切换', description: 'git stash' },
|
||||||
|
{ label: '$(warning) 强制切换', description: '可能丢失更改' }
|
||||||
|
], { placeHolder: '当前有未提交的更改,如何处理?' });
|
||||||
|
|
||||||
|
if (!shouldStash) { return; }
|
||||||
|
|
||||||
|
if (shouldStash.description === '先提交当前更改') {
|
||||||
|
runGit(workspaceRoot, 'add -A');
|
||||||
|
const commitMsg = generateCommitMessage('切换分支前提交');
|
||||||
|
runGit(workspaceRoot, `commit -m "${commitMsg}" --allow-empty`);
|
||||||
|
} else if (shouldStash.description === 'git stash') {
|
||||||
|
runGit(workspaceRoot, 'stash');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是远程分支,需要创建本地分支
|
||||||
|
if (branchName.includes('remotes/origin/')) {
|
||||||
|
const localName = branchName.replace('remotes/origin/', '');
|
||||||
|
runGit(workspaceRoot, `checkout -b ${localName} ${branchName}`);
|
||||||
|
} else {
|
||||||
|
runGit(workspaceRoot, `checkout ${branchName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
vscode.window.showInformationMessage(`✅ 已切换到分支: ${selected.detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
vscode.window.showErrorMessage(`❌ 切换分支失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== ⬇️ 拉取最新代码 ==========
|
||||||
|
|
||||||
|
async function pullLatest(clickedUri?: vscode.Uri) {
|
||||||
|
const workspaceRoot = getWorkspaceRoot(clickedUri);
|
||||||
|
if (!workspaceRoot) { return; }
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(workspaceRoot, '.git'))) {
|
||||||
|
vscode.window.showErrorMessage('❌ 当前项目不是Git仓库!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
statusBarItem.text = '$(sync~spin) Auto Git: 正在拉取...';
|
||||||
|
|
||||||
|
// 先提交本地更改
|
||||||
|
if (checkHasChanges(workspaceRoot)) {
|
||||||
|
runGit(workspaceRoot, 'add -A');
|
||||||
|
const commitMsg = generateCommitMessage('拉取前自动提交');
|
||||||
|
runGit(workspaceRoot, `commit -m "${commitMsg}" --allow-empty`);
|
||||||
|
}
|
||||||
|
|
||||||
|
runGit(workspaceRoot, 'pull --rebase');
|
||||||
|
statusBarItem.text = '$(check) Auto Git: 已拉取';
|
||||||
|
vscode.window.showInformationMessage('✅ 已拉取最新代码');
|
||||||
|
|
||||||
|
updateStatusBar();
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
statusBarItem.text = '$(error) Auto Git: 拉取失败';
|
||||||
|
// 尝试中止rebase
|
||||||
|
try {
|
||||||
|
runGit(workspaceRoot, 'rebase --abort');
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
vscode.window.showErrorMessage(`❌ 拉取失败: ${error.message}`);
|
||||||
|
setTimeout(updateStatusBar, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 🏗️ 初始化仓库并推送到Gitea ==========
|
||||||
|
|
||||||
|
async function initRepoAndPushToGitea(clickedUri?: vscode.Uri) {
|
||||||
|
const workspaceRoot = getWorkspaceRoot(clickedUri);
|
||||||
|
if (!workspaceRoot) { return; }
|
||||||
const config = vscode.workspace.getConfiguration('autoGitSync');
|
const config = vscode.workspace.getConfiguration('autoGitSync');
|
||||||
const giteaUrl = config.get<string>('giteaUrl', 'http://localhost:3000');
|
const giteaUrl = config.get<string>('giteaUrl', 'http://localhost:3000');
|
||||||
const giteaToken = config.get<string>('giteaToken', '');
|
const giteaToken = config.get<string>('giteaToken', '');
|
||||||
@ -292,29 +943,58 @@ async function initRepoAndPushToGitea() {
|
|||||||
});
|
});
|
||||||
if (!username) { return; }
|
if (!username) { return; }
|
||||||
|
|
||||||
// 输入仓库名
|
// 输入仓库名 - 自动清理不合法字符
|
||||||
|
const rawName = path.basename(workspaceRoot);
|
||||||
|
const defaultName = rawName.replace(/[^a-zA-Z0-9_\-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||||
const projectName = await vscode.window.showInputBox({
|
const projectName = await vscode.window.showInputBox({
|
||||||
prompt: '请输入仓库名称',
|
prompt: '请输入仓库名称(仅支持字母、数字、中横线、下划线)',
|
||||||
placeHolder: '例如: my-project',
|
placeHolder: '例如: my-project',
|
||||||
value: path.basename(workspaceRoot)
|
value: defaultName,
|
||||||
|
validateInput: (value) => {
|
||||||
|
if (!/^[a-zA-Z0-9_\-]+$/.test(value)) {
|
||||||
|
return '仓库名称只能包含字母、数字、中横线(-)和下划线(_)';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (!projectName) { return; }
|
if (!projectName) { return; }
|
||||||
|
|
||||||
|
// 选择仓库可见性
|
||||||
|
const visibility = await vscode.window.showQuickPick([
|
||||||
|
{ label: '$(lock) 私有仓库', description: 'private', detail: '仅自己可见' },
|
||||||
|
{ label: '$(globe) 公开仓库', description: 'public', detail: '所有人可见' }
|
||||||
|
], { placeHolder: '选择仓库可见性' });
|
||||||
|
|
||||||
|
const isPrivate = visibility?.description !== 'public';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
statusBarItem.text = '$(sync~spin) Auto Git: 正在创建仓库...';
|
||||||
|
|
||||||
// 如果不是Git仓库,先初始化
|
// 如果不是Git仓库,先初始化
|
||||||
if (!fs.existsSync(path.join(workspaceRoot, '.git'))) {
|
if (!fs.existsSync(path.join(workspaceRoot, '.git'))) {
|
||||||
runGit(workspaceRoot, 'init');
|
runGit(workspaceRoot, 'init');
|
||||||
vscode.window.showInformationMessage('✅ Git仓库已初始化');
|
vscode.window.showInformationMessage('✅ Git仓库已初始化');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置用户信息
|
||||||
|
try { runGit(workspaceRoot, `config user.name "${username}"`); } catch { /* ignore */ }
|
||||||
|
try { runGit(workspaceRoot, 'config user.email "user@gitea.local"'); } catch { /* ignore */ }
|
||||||
|
|
||||||
// 创建远程仓库(通过Gitea API)
|
// 创建远程仓库(通过Gitea API)
|
||||||
if (giteaToken) {
|
if (giteaToken) {
|
||||||
await createGiteaRepo(giteaUrl, giteaToken, username, projectName);
|
try {
|
||||||
|
await createGiteaRepo(giteaUrl, giteaToken, username, projectName, isPrivate);
|
||||||
vscode.window.showInformationMessage('✅ Gitea远程仓库已创建');
|
vscode.window.showInformationMessage('✅ Gitea远程仓库已创建');
|
||||||
|
} catch (e: any) {
|
||||||
|
vscode.window.showWarningMessage(`⚠️ 创建远程仓库失败: ${e.message},请手动创建`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vscode.window.showWarningMessage('⚠️ 未设置Gitea令牌,请手动在Gitea上创建仓库');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置远程地址
|
// 设置远程地址
|
||||||
const remoteUrl = `${giteaUrl}/${username}/${projectName}.git`;
|
const cleanUrl = giteaUrl.replace(/\/$/, '');
|
||||||
|
const remoteUrl = `${cleanUrl}/${username}/${projectName}.git`;
|
||||||
try {
|
try {
|
||||||
runGit(workspaceRoot, 'remote remove origin');
|
runGit(workspaceRoot, 'remote remove origin');
|
||||||
} catch { /* remote可能不存在 */ }
|
} catch { /* remote可能不存在 */ }
|
||||||
@ -330,32 +1010,39 @@ async function initRepoAndPushToGitea() {
|
|||||||
runGit(workspaceRoot, 'branch -M main');
|
runGit(workspaceRoot, 'branch -M main');
|
||||||
runGit(workspaceRoot, 'push -u origin main');
|
runGit(workspaceRoot, 'push -u origin main');
|
||||||
|
|
||||||
|
statusBarItem.text = '$(check) Auto Git: 已推送';
|
||||||
vscode.window.showInformationMessage(`🎉 仓库已推送到 ${remoteUrl}`);
|
vscode.window.showInformationMessage(`🎉 仓库已推送到 ${remoteUrl}`);
|
||||||
|
|
||||||
|
updateStatusBar();
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
vscode.window.showErrorMessage(`初始化失败: ${error.message}`);
|
statusBarItem.text = '$(error) Auto Git: 创建失败';
|
||||||
|
vscode.window.showErrorMessage(`❌ 初始化失败: ${error.message}`);
|
||||||
|
setTimeout(updateStatusBar, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过Gitea API创建仓库
|
* 通过Gitea API创建仓库
|
||||||
*/
|
*/
|
||||||
async function createGiteaRepo(giteaUrl: string, token: string, username: string, repoName: string): Promise<void> {
|
async function createGiteaRepo(giteaUrl: string, token: string, username: string, repoName: string, isPrivate: boolean): Promise<void> {
|
||||||
const axios = await import('https');
|
const cleanUrl = giteaUrl.replace(/\/$/, '');
|
||||||
|
const url = `${cleanUrl}/api/v1/user/repos`;
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const url = `${giteaUrl}/api/v1/user/repos`;
|
|
||||||
const data = JSON.stringify({
|
const data = JSON.stringify({
|
||||||
name: repoName,
|
name: repoName,
|
||||||
private: true,
|
private: isPrivate,
|
||||||
auto_init: false,
|
auto_init: false,
|
||||||
description: `自动创建的仓库 - ${new Date().toLocaleDateString('zh-CN')}`
|
description: `自动创建的仓库 - ${new Date().toLocaleDateString('zh-CN')}`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
const options = {
|
const isHttps = parsedUrl.protocol === 'https:';
|
||||||
|
const requestModule = isHttps ? https : http;
|
||||||
|
|
||||||
|
const options: any = {
|
||||||
hostname: parsedUrl.hostname,
|
hostname: parsedUrl.hostname,
|
||||||
port: parsedUrl.port,
|
port: parsedUrl.port || (isHttps ? 443 : 80),
|
||||||
path: parsedUrl.pathname,
|
path: parsedUrl.pathname,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -366,7 +1053,7 @@ async function createGiteaRepo(giteaUrl: string, token: string, username: string
|
|||||||
rejectUnauthorized: false
|
rejectUnauthorized: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const req = axios.request(options, (res) => {
|
const req = requestModule.request(options, (res) => {
|
||||||
let body = '';
|
let body = '';
|
||||||
res.on('data', (chunk: string) => { body += chunk; });
|
res.on('data', (chunk: string) => { body += chunk; });
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户