5 Commits

Author SHA1 Message Date
zyj
5154bb5564 chore: 升级版本号到 v0.2.0,修复 macOS/Linux 配置\n\n- 升级所有平台版本号 0.1.0 → 0.2.0\n- 修复 macOS Info.plist 残留的旧项目名 Peter Agent → GitPilot\n- 修复 Linux desktop 文件残留的旧项目名" 2026-03-11 10:10:37 +08:00
zyj
af15e86df6 feat: 新增冲突处理、提交搜索、批量操作功能\n\n冲突处理:\n- 检测合并状态并显示冲突提示栏\n- 冲突文件列表展示和内容编辑器\n- 支持保存冲突文件、标记已解决、中止合并、完成合并\n\n提交搜索:\n- 支持按提交信息关键字和作者搜索\n- 搜索结果与完整历史切换显示\n\n批量操作:\n- 侧边栏新增批量操作入口\n- 展示所有项目概览(分支/变更/未推送状态)\n- 支持批量 Pull/Push 并显示执行结果" 2026-03-11 10:06:28 +08:00
zyj
6354cbfc94 feat: 新增分支管理、Stash贮藏、远程分支、设置页面\n\n分支管理:\n- 创建新分支\n- 删除本地分支(安全/强制)\n- 合并分支到当前分支\n\n远程分支:\n- 查看远程分支列表\n- 检出远程分支到本地\n- 删除远程分支\n\nStash 贮藏:\n- 保存当前变更(支持自定义消息)\n- 应用/弹出/删除贮藏\n- 贮藏列表管理\n\n设置:\n- Git 全局配置 (user.name / user.email)\n- 设置弹窗 UI" 2026-03-11 09:37:58 +08:00
zyj
cb6b21ef98 refactor: 拆分 proxy.go 为平台特定文件,兼容 macOS 和 Linux\n\n- proxy.go: 保留通用类型和辅助函数\n- proxy_windows.go: 通过注册表读取 Windows 系统代理\n- proxy_darwin.go: 通过 scutil --proxy 读取 macOS 系统代理\n- proxy_linux.go: 通过 gsettings/kreadconfig 读取 Linux 系统代理" 2026-03-10 11:55:34 +08:00
zyj
6e0becb748 新增远程项目克隆功能 2026-03-10 10:25:45 +08:00
17 changed files with 2841 additions and 121 deletions

View File

@@ -4,17 +4,17 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>Peter Agent</string>
<string>GitPilot</string>
<key>CFBundleExecutable</key>
<string>peter-agent</string>
<string>gitpilot</string>
<key>CFBundleIdentifier</key>
<string>com.peteragent.app</string>
<string>com.gitpilot.app</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<string>0.2.0</string>
<key>CFBundleGetInfoString</key>
<string>AI Agent Application</string>
<string>Git Repository Management Tool</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>0.2.0</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
@@ -22,7 +22,7 @@
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© 2025, Peter Agent</string>
<string>© 2025, GitPilot</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>

View File

@@ -4,17 +4,17 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>Peter Agent</string>
<string>GitPilot</string>
<key>CFBundleExecutable</key>
<string>peter-agent</string>
<string>gitpilot</string>
<key>CFBundleIdentifier</key>
<string>com.peteragent.app</string>
<string>com.gitpilot.app</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<string>0.2.0</string>
<key>CFBundleGetInfoString</key>
<string>AI Agent Application</string>
<string>Git Repository Management Tool</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>0.2.0</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
@@ -22,6 +22,6 @@
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© 2025, Peter Agent</string>
<string>© 2025, GitPilot</string>
</dict>
</plist>

View File

@@ -1,13 +1,13 @@
[Desktop Entry]
Version=1.0
Name=Peter Agent
Comment=AI Agent Application
Name=GitPilot
Comment=Git Repository Management Tool
# The Exec line includes %u to pass the URL to the application
Exec=/usr/local/bin/peter-agent %u
Exec=/usr/local/bin/gitpilot %u
Terminal=false
Type=Application
Icon=peter-agent
Icon=gitpilot
Categories=Utility;
StartupWMClass=peter-agent
StartupWMClass=gitpilot

View File

@@ -1,10 +1,10 @@
{
"fixed": {
"file_version": "0.1.0"
"file_version": "0.2.0"
},
"info": {
"0000": {
"ProductVersion": "0.1.0",
"ProductVersion": "0.2.0",
"CompanyName": "GitPilot",
"FileDescription": "Git Repository Management Tool",
"LegalCopyright": "© 2025, GitPilot",

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,16 @@ import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
CloudDownloadOutlined,
LoadingOutlined,
AppstoreOutlined,
CloudUploadOutlined,
SyncOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
BranchesOutlined,
} from '@ant-design/icons-vue'
import { message, Modal } from 'ant-design-vue'
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
const props = defineProps({
@@ -24,6 +33,24 @@ const selectedKeys = ref([])
const showAddDialog = ref(false)
const addForm = ref({ platform: '', username: '', name: '', path: '' })
// 克隆项目弹窗
const showCloneDialog = ref(false)
const cloneForm = ref({ platform: '', username: '', repoURL: '', parentDir: '', name: '' })
const cloneLoading = ref(false)
// ---- 批量操作 ----
const showBatchModal = ref(false)
const batchOverviews = ref([])
const batchLoading = ref(false)
const selectedBatchPaths = ref([])
const batchResults = ref([])
const batchActionLoading = ref('')
const batchResultMap = computed(() => {
const map = {}
batchResults.value.forEach(r => { map[r.path] = r })
return map
})
// 右键菜单
const contextMenu = ref({ visible: false, x: 0, y: 0, type: '', data: {} })
@@ -142,6 +169,7 @@ function onContextMenuClick({ key: action }) {
case 'edit-platform': openEditPlatformDialog(data.key); break
case 'remove-platform': removePlatform(data.key); break
case 'add-project': openAddDialog(data.platformKey, data.username); break
case 'clone-project': openCloneDialog(data.platformKey, data.username); break
case 'edit-user': openEditUserDialog(data.platformKey, data.username); break
case 'remove-user': removeUser(data.platformKey, data.username); break
case 'remove-project': removeProject(data.platformKey, data.username, data.name); break
@@ -166,6 +194,7 @@ const contextMenuItems = computed(() => {
if (type === 'user') {
return [
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
{ key: 'clone-project', label: '克隆项目', icon: h(CloudDownloadOutlined) },
{ key: 'edit-user', label: '编辑用户', icon: h(EditOutlined) },
{ type: 'divider' },
{ key: 'remove-user', label: '删除用户', danger: true, icon: h(DeleteOutlined) },
@@ -174,6 +203,7 @@ const contextMenuItems = computed(() => {
if (type === 'project') {
return [
{ key: 'add-project', label: '添加项目', icon: h(FolderOutlined) },
{ key: 'clone-project', label: '克隆项目', icon: h(CloudDownloadOutlined) },
{ type: 'divider' },
{ key: 'remove-project', label: '删除项目', danger: true, icon: h(DeleteOutlined) },
]
@@ -320,6 +350,57 @@ async function addProject() {
}
}
// --- 克隆项目 ---
function openCloneDialog(platformKey, username) {
cloneForm.value = { platform: platformKey, username: username, repoURL: '', parentDir: '', name: '' }
showCloneDialog.value = true
}
async function pickCloneDirectory() {
try {
const path = await AppService.SelectDirectory()
if (path) {
cloneForm.value.parentDir = path
}
} catch (e) {
console.error('选择文件夹失败:', e)
}
}
function onRepoURLChange() {
// 从仓库 URL 自动提取项目名称
if (!cloneForm.value.name && cloneForm.value.repoURL) {
const url = cloneForm.value.repoURL.trim()
const match = url.match(/\/([^\/]+?)(\.git)?$/)
if (match) {
cloneForm.value.name = match[1]
}
}
}
async function cloneProject() {
if (!cloneForm.value.repoURL || !cloneForm.value.parentDir || !cloneForm.value.name) return
cloneLoading.value = true
try {
await AppService.CloneProject(
cloneForm.value.platform,
cloneForm.value.username,
cloneForm.value.repoURL,
cloneForm.value.parentDir,
cloneForm.value.name
)
showCloneDialog.value = false
message.success('克隆成功')
await loadTree()
emit('tree-updated')
} catch (e) {
console.error('克隆项目失败:', e)
message.error('克隆失败: ' + String(e))
} finally {
cloneLoading.value = false
}
}
async function removeProject(platform, username, name) {
if (!confirm(`确定删除项目 "${name}" 吗?`)) return
try {
@@ -331,6 +412,79 @@ async function removeProject(platform, username, name) {
}
}
// ---- 批量操作 ----
async function openBatchModal() {
showBatchModal.value = true
batchResults.value = []
await loadBatchOverview()
}
async function loadBatchOverview() {
batchLoading.value = true
try {
const overviews = await AppService.GetAllProjectOverview()
batchOverviews.value = Array.isArray(overviews) ? overviews : []
selectedBatchPaths.value = batchOverviews.value
.filter(p => !p.error)
.map(p => p.path)
} catch (e) {
console.error('获取项目概览失败:', e)
} finally {
batchLoading.value = false
}
}
function toggleBatchPath(path) {
const idx = selectedBatchPaths.value.indexOf(path)
if (idx >= 0) {
selectedBatchPaths.value = selectedBatchPaths.value.filter(p => p !== path)
} else {
selectedBatchPaths.value = [...selectedBatchPaths.value, path]
}
}
function toggleAllBatchPaths(e) {
if (e.target.checked) {
selectedBatchPaths.value = batchOverviews.value
.filter(p => !p.error)
.map(p => p.path)
} else {
selectedBatchPaths.value = []
}
}
async function doBatchPull() {
if (!selectedBatchPaths.value.length) return
batchActionLoading.value = 'pull'
try {
const results = await AppService.BatchPull(selectedBatchPaths.value)
batchResults.value = Array.isArray(results) ? results : []
const successCount = batchResults.value.filter(r => r.success).length
message.info(`批量 Pull 完成:${successCount}/${batchResults.value.length} 成功`)
await loadBatchOverview()
} catch (e) {
Modal.error({ title: '批量 Pull 失败', content: String(e) })
} finally {
batchActionLoading.value = ''
}
}
async function doBatchPush() {
if (!selectedBatchPaths.value.length) return
batchActionLoading.value = 'push'
try {
const results = await AppService.BatchPush(selectedBatchPaths.value)
batchResults.value = Array.isArray(results) ? results : []
const successCount = batchResults.value.filter(r => r.success).length
message.info(`批量 Push 完成:${successCount}/${batchResults.value.length} 成功`)
await loadBatchOverview()
} catch (e) {
Modal.error({ title: '批量 Push 失败', content: String(e) })
} finally {
batchActionLoading.value = ''
}
}
// 平台 SVG Logo
const platformLogos = {
github: `<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>`,
@@ -353,9 +507,14 @@ onMounted(() => {
<aside class="sidebar">
<div class="sidebar-header">
<span class="logo">GitPilot</span>
<a-space :size="0">
<a-button type="text" size="small" @click="openBatchModal" title="批量操作">
<template #icon><AppstoreOutlined /></template>
</a-button>
<a-button type="text" size="small" @click="openAddPlatformDialog" title="添加平台">
<template #icon><PlusOutlined /></template>
</a-button>
</a-space>
</div>
<div class="sidebar-content" @contextmenu="onSidebarContextMenu">
@@ -441,6 +600,53 @@ onMounted(() => {
</a-form>
</a-modal>
<!-- 克隆项目弹窗 -->
<a-modal
v-model:open="showCloneDialog"
title="克隆项目"
@ok="cloneProject"
:ok-text="cloneLoading ? '克隆中...' : '克隆'"
cancel-text="取消"
:ok-button-props="{ loading: cloneLoading, disabled: !cloneForm.repoURL || !cloneForm.parentDir || !cloneForm.name }"
:closable="!cloneLoading"
:maskClosable="!cloneLoading"
:width="480"
>
<a-form layout="vertical" :style="{ marginTop: '16px' }">
<a-form-item label="平台">
<a-input :value="cloneForm.platform" disabled />
</a-form-item>
<a-form-item label="用户">
<a-input :value="cloneForm.username" disabled />
</a-form-item>
<a-form-item label="仓库地址" required>
<a-input
v-model:value="cloneForm.repoURL"
placeholder="https://github.com/user/repo.git"
@blur="onRepoURLChange"
/>
</a-form-item>
<a-form-item label="克隆到目录" required>
<a-input v-model:value="cloneForm.parentDir" placeholder="选择父目录" read-only>
<template #suffix>
<FolderOpenOutlined
style="cursor: pointer; color: var(--accent, #89b4fa);"
title="选择文件夹"
@click="pickCloneDirectory"
/>
</template>
</a-input>
</a-form-item>
<a-form-item label="项目名称" required>
<a-input v-model:value="cloneForm.name" placeholder="自动从仓库地址提取" />
</a-form-item>
<div v-if="cloneForm.parentDir && cloneForm.name" style="font-size: 12px; color: var(--text-muted); margin-top: -8px; margin-bottom: 8px;">
<span>目标路径</span>
<span style="color: var(--accent, #89b4fa);">{{ cloneForm.parentDir }}/{{ cloneForm.name }}</span>
</div>
</a-form>
</a-modal>
<!-- 平台弹窗 -->
<a-modal
v-model:open="showPlatformDialog"
@@ -491,6 +697,82 @@ onMounted(() => {
</a-form-item>
</a-form>
</a-modal>
<!-- 批量操作弹窗 -->
<a-modal
v-model:open="showBatchModal"
title="批量操作"
:width="600"
:footer="null"
>
<div class="batch-panel">
<div class="batch-header">
<a-checkbox
:checked="selectedBatchPaths.length === batchOverviews.filter(p => !p.error).length && batchOverviews.length > 0"
:indeterminate="selectedBatchPaths.length > 0 && selectedBatchPaths.length < batchOverviews.filter(p => !p.error).length"
@change="toggleAllBatchPaths"
>
全选
</a-checkbox>
<span style="flex:1"></span>
<a-space :size="4">
<a-button size="small" :loading="batchActionLoading === 'pull'" :disabled="!selectedBatchPaths.length" @click="doBatchPull">
<template #icon><CloudDownloadOutlined /></template>
Pull
</a-button>
<a-button size="small" :loading="batchActionLoading === 'push'" :disabled="!selectedBatchPaths.length" @click="doBatchPush">
<template #icon><CloudUploadOutlined /></template>
Push
</a-button>
<a-button size="small" :loading="batchLoading" @click="loadBatchOverview">
<template #icon><SyncOutlined /></template>
</a-button>
</a-space>
</div>
<div class="batch-list">
<template v-if="batchLoading">
<div v-for="i in 4" :key="i" style="padding: 10px 12px;">
<div style="height: 14px; background: var(--bg-hover, #333); border-radius: 4px; animation: pulse 1.5s infinite;" :style="{ width: (50 + i * 8) + '%' }"></div>
</div>
</template>
<div v-else-if="!batchOverviews.length" style="padding: 24px; text-align: center; color: var(--text-muted);">
暂无项目
</div>
<template v-else>
<div
v-for="proj in batchOverviews"
:key="proj.key"
class="batch-item"
:class="{ error: !!proj.error }"
>
<a-checkbox
:checked="selectedBatchPaths.includes(proj.path)"
:disabled="!!proj.error"
@change="toggleBatchPath(proj.path)"
style="flex-shrink: 0;"
/>
<div class="batch-item-info">
<div class="batch-item-name">{{ proj.name }}</div>
<div class="batch-item-meta">
<a-tag v-if="proj.branch" size="small" color="green">
<BranchesOutlined /> {{ proj.branch }}
</a-tag>
<a-tag v-if="proj.hasChanges" size="small" color="orange">有变更</a-tag>
<a-tag v-if="proj.unpushed > 0" size="small" color="blue">{{ proj.unpushed }} 未推送</a-tag>
<span v-if="proj.error" style="color: #f38ba8; font-size: 11px;">{{ proj.error }}</span>
</div>
</div>
<div v-if="batchResultMap[proj.path]" class="batch-result-icon">
<CheckCircleOutlined v-if="batchResultMap[proj.path].success" style="color: #a6e3a1;" />
<a-tooltip v-else :title="batchResultMap[proj.path].message">
<CloseCircleOutlined style="color: #f38ba8;" />
</a-tooltip>
</div>
</div>
</template>
</div>
</div>
</a-modal>
</aside>
</template>
@@ -617,4 +899,72 @@ onMounted(() => {
:deep(.ant-tree .ant-tree-icon__customize) {
display: none;
}
/* 批量操作 */
.batch-panel {
margin-top: 8px;
}
.batch-header {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color, #313244);
margin-bottom: 8px;
}
.batch-list {
max-height: 400px;
overflow-y: auto;
}
.batch-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 4px;
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.04));
transition: background 0.12s;
}
.batch-item:hover {
background: var(--bg-hover, rgba(255,255,255,0.04));
}
.batch-item.error {
opacity: 0.6;
}
.batch-item-info {
flex: 1;
min-width: 0;
}
.batch-item-name {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.batch-item-meta {
display: flex;
align-items: center;
gap: 4px;
margin-top: 2px;
flex-wrap: wrap;
}
.batch-result-icon {
flex-shrink: 0;
font-size: 16px;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
</style>

View File

@@ -0,0 +1,7 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export {
Settings
} from "./models.js";

View File

@@ -0,0 +1,49 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime";
export class Settings {
/**
* Creates a new Settings instance.
* @param {Partial<Settings>} [$$source = {}] - The source object to create the Settings.
*/
constructor($$source = {}) {
if (!("Concurrency" in $$source)) {
/**
* @member
* @type {number}
*/
this["Concurrency"] = 0;
}
if (!("NetworkCheck" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["NetworkCheck"] = false;
}
if (!("LogLevel" in $$source)) {
/**
* @member
* @type {string}
*/
this["LogLevel"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new Settings instance from a string or object.
* @param {any} [$$source = {}]
* @returns {Settings}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Settings(/** @type {Partial<Settings>} */($$parsedSource));
}
}

View File

@@ -14,11 +14,23 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../../wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as config$0 from "../../config/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
/**
* AbortMerge 中止合并
* @param {string} path
* @returns {$CancellablePromise<void>}
*/
export function AbortMerge(path) {
return $Call.ByID(3035150106, path);
}
/**
* AddPlatform 添加新平台
* @param {string} name
@@ -52,6 +64,51 @@ export function AddUser(platform, username, token) {
return $Call.ByID(2264426704, platform, username, token);
}
/**
* BatchPull 批量拉取指定项目
* @param {string[]} paths
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/
export function BatchPull(paths) {
return $Call.ByID(758996647, paths).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result);
}));
}
/**
* BatchPush 批量推送指定项目
* @param {string[]} paths
* @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/
export function BatchPush(paths) {
return $Call.ByID(794082076, paths).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result);
}));
}
/**
* CheckoutRemoteBranch 检出远程分支到本地
* @param {string} path
* @param {string} remoteBranch
* @returns {$CancellablePromise<void>}
*/
export function CheckoutRemoteBranch(path, remoteBranch) {
return $Call.ByID(3682237522, path, remoteBranch);
}
/**
* CloneProject 克隆远程仓库到本地目录,并添加到项目树
* @param {string} platform
* @param {string} username
* @param {string} repoURL
* @param {string} parentDir
* @param {string} name
* @returns {$CancellablePromise<void>}
*/
export function CloneProject(platform, username, repoURL, parentDir, name) {
return $Call.ByID(2347019414, platform, username, repoURL, parentDir, name);
}
/**
* CommitChanges 提交已暂存的更改
* @param {string} path
@@ -62,6 +119,16 @@ export function CommitChanges(path, message) {
return $Call.ByID(921984546, path, message);
}
/**
* CreateBranch 创建新分支
* @param {string} path
* @param {string} name
* @returns {$CancellablePromise<void>}
*/
export function CreateBranch(path, name) {
return $Call.ByID(1422152924, path, name);
}
/**
* CreateTag 创建标签
* @param {string} path
@@ -73,6 +140,27 @@ export function CreateTag(path, name, message) {
return $Call.ByID(4209616956, path, name, message);
}
/**
* DeleteBranch 删除本地分支
* @param {string} path
* @param {string} name
* @param {boolean} force
* @returns {$CancellablePromise<void>}
*/
export function DeleteBranch(path, name, force) {
return $Call.ByID(580272035, path, name, force);
}
/**
* DeleteRemoteBranch 删除远程分支
* @param {string} path
* @param {string} branch
* @returns {$CancellablePromise<void>}
*/
export function DeleteRemoteBranch(path, branch) {
return $Call.ByID(2626609049, path, branch);
}
/**
* DeleteTag 删除标签(本地+远程)
* @param {string} path
@@ -102,6 +190,26 @@ export function FetchProject(path) {
return $Call.ByID(3541106829, path);
}
/**
* GetAllProjectOverview 获取所有项目的概览状态
* @returns {$CancellablePromise<$models.ProjectOverview[]>}
*/
export function GetAllProjectOverview() {
return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => {
return $$createType3($result);
}));
}
/**
* GetAppSettings 获取应用设置
* @returns {$CancellablePromise<config$0.Settings | null>}
*/
export function GetAppSettings() {
return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => {
return $$createType5($result);
}));
}
/**
* GetBranches 获取所有本地分支
* @param {string} path
@@ -109,7 +217,7 @@ export function FetchProject(path) {
*/
export function GetBranches(path) {
return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result);
return $$createType7($result);
}));
}
@@ -142,7 +250,7 @@ export function GetCommitFileDiff(path, hash, filePath) {
*/
export function GetCommitFiles(path, hash) {
return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => {
return $$createType3($result);
return $$createType9($result);
}));
}
@@ -154,7 +262,28 @@ export function GetCommitFiles(path, hash) {
*/
export function GetCommitLog(path, count) {
return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => {
return $$createType5($result);
return $$createType11($result);
}));
}
/**
* GetConflictFileContent 获取冲突文件内容(包含冲突标记)
* @param {string} projectPath
* @param {string} filePath
* @returns {$CancellablePromise<string>}
*/
export function GetConflictFileContent(projectPath, filePath) {
return $Call.ByID(298498517, projectPath, filePath);
}
/**
* GetConflictFiles 获取冲突文件列表
* @param {string} path
* @returns {$CancellablePromise<$models.ConflictFileInfo[]>}
*/
export function GetConflictFiles(path) {
return $Call.ByID(2446806137, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType13($result);
}));
}
@@ -188,6 +317,16 @@ export function GetFileDiffStaged(projectPath, filePath) {
return $Call.ByID(2773256455, projectPath, filePath);
}
/**
* GetGitGlobalConfig 获取 git 全局配置
* @returns {$CancellablePromise<$models.GitConfig | null>}
*/
export function GetGitGlobalConfig() {
return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => {
return $$createType15($result);
}));
}
/**
* GetPlatformInfo 获取平台信息
* @param {string} name
@@ -195,7 +334,7 @@ export function GetFileDiffStaged(projectPath, filePath) {
*/
export function GetPlatformInfo(name) {
return $Call.ByID(2668095547, name).then(/** @type {($result: any) => any} */(($result) => {
return $$createType7($result);
return $$createType17($result);
}));
}
@@ -206,7 +345,7 @@ export function GetPlatformInfo(name) {
*/
export function GetProjectChangedFiles(path) {
return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType9($result);
return $$createType19($result);
}));
}
@@ -217,7 +356,7 @@ export function GetProjectChangedFiles(path) {
*/
export function GetProjectStatus(path) {
return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType11($result);
return $$createType21($result);
}));
}
@@ -227,7 +366,29 @@ export function GetProjectStatus(path) {
*/
export function GetProjectTree() {
return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => {
return $$createType13($result);
return $$createType23($result);
}));
}
/**
* GetRemoteBranches 获取远程分支列表
* @param {string} path
* @returns {$CancellablePromise<$models.BranchInfo[]>}
*/
export function GetRemoteBranches(path) {
return $Call.ByID(994796370, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType7($result);
}));
}
/**
* GetStashList 获取贮藏列表
* @param {string} path
* @returns {$CancellablePromise<$models.StashInfo[]>}
*/
export function GetStashList(path) {
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType25($result);
}));
}
@@ -238,7 +399,7 @@ export function GetProjectTree() {
*/
export function GetTags(path) {
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType15($result);
return $$createType27($result);
}));
}
@@ -250,10 +411,29 @@ export function GetTags(path) {
*/
export function GetUserInfo(platform, username) {
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
return $$createType17($result);
return $$createType29($result);
}));
}
/**
* IsMerging 检查是否处于合并状态
* @param {string} path
* @returns {$CancellablePromise<boolean>}
*/
export function IsMerging(path) {
return $Call.ByID(693809703, path);
}
/**
* MergeBranch 合并指定分支到当前分支
* @param {string} path
* @param {string} branch
* @returns {$CancellablePromise<string>}
*/
export function MergeBranch(path, branch) {
return $Call.ByID(278161076, path, branch);
}
/**
* PullProject 拉取项目(当前分支)
* @param {string} path
@@ -324,6 +504,16 @@ export function ResetProject(path, hash, mode) {
return $Call.ByID(2061148684, path, hash, mode);
}
/**
* ResolveConflictFile 将冲突文件标记为已解决
* @param {string} path
* @param {string[]} files
* @returns {$CancellablePromise<void>}
*/
export function ResolveConflictFile(path, files) {
return $Call.ByID(1893119072, path, files);
}
/**
* RevertCommit 撤回指定提交(生成一个反向提交)
* @param {string} path
@@ -334,6 +524,31 @@ export function RevertCommit(path, hash) {
return $Call.ByID(1334788539, path, hash);
}
/**
* SaveConflictFile 保存冲突文件内容(手动解决冲突后保存)
* @param {string} projectPath
* @param {string} filePath
* @param {string} content
* @returns {$CancellablePromise<void>}
*/
export function SaveConflictFile(projectPath, filePath, content) {
return $Call.ByID(3340289161, projectPath, filePath, content);
}
/**
* SearchCommitLog 搜索提交历史
* @param {string} path
* @param {string} keyword
* @param {string} author
* @param {number} maxCount
* @returns {$CancellablePromise<$models.CommitLog[]>}
*/
export function SearchCommitLog(path, keyword, author, maxCount) {
return $Call.ByID(2874450917, path, keyword, author, maxCount).then(/** @type {($result: any) => any} */(($result) => {
return $$createType11($result);
}));
}
/**
* SelectDirectory 打开系统文件夹选择器,返回选中的路径
* @returns {$CancellablePromise<string>}
@@ -350,6 +565,16 @@ export function SetApplication(app) {
return $Call.ByID(383695022, app);
}
/**
* SetGitGlobalConfig 设置 git 全局配置
* @param {string} name
* @param {string} email
* @returns {$CancellablePromise<void>}
*/
export function SetGitGlobalConfig(name, email) {
return $Call.ByID(3875064981, name, email);
}
/**
* StageAll 暂存所有变更文件
* @param {string} path
@@ -369,6 +594,46 @@ export function StageFiles(path, files) {
return $Call.ByID(4064506881, path, files);
}
/**
* StashApply 应用贮藏(不删除)
* @param {string} path
* @param {number} index
* @returns {$CancellablePromise<void>}
*/
export function StashApply(path, index) {
return $Call.ByID(721583959, path, index);
}
/**
* StashDrop 删除贮藏
* @param {string} path
* @param {number} index
* @returns {$CancellablePromise<void>}
*/
export function StashDrop(path, index) {
return $Call.ByID(3821556536, path, index);
}
/**
* StashPop 应用贮藏并删除
* @param {string} path
* @param {number} index
* @returns {$CancellablePromise<void>}
*/
export function StashPop(path, index) {
return $Call.ByID(4117964460, path, index);
}
/**
* StashSave 保存当前变更到贮藏
* @param {string} path
* @param {string} message
* @returns {$CancellablePromise<void>}
*/
export function StashSave(path, message) {
return $Call.ByID(1178263140, path, message);
}
/**
* SwitchBranch 切换分支
* @param {string} path
@@ -398,6 +663,15 @@ export function UnstageFiles(path, files) {
return $Call.ByID(3546473046, path, files);
}
/**
* UpdateAppSettings 更新应用设置
* @param {string} logLevel
* @returns {$CancellablePromise<void>}
*/
export function UpdateAppSettings(logLevel) {
return $Call.ByID(718820989, logLevel);
}
/**
* UpdatePlatform 修改平台信息base_url
* @param {string} name
@@ -421,21 +695,33 @@ export function UpdateUser(platform, oldUsername, newUsername, token) {
}
// Private type creation functions
const $$createType0 = $models.BranchInfo.createFrom;
const $$createType0 = $models.BatchPullResult.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $models.CommitFileInfo.createFrom;
const $$createType2 = $models.ProjectOverview.createFrom;
const $$createType3 = $Create.Array($$createType2);
const $$createType4 = $models.CommitLog.createFrom;
const $$createType5 = $Create.Array($$createType4);
const $$createType6 = $models.PlatformInfo.createFrom;
const $$createType7 = $Create.Nullable($$createType6);
const $$createType8 = $models.FileInfo.createFrom;
const $$createType4 = config$0.Settings.createFrom;
const $$createType5 = $Create.Nullable($$createType4);
const $$createType6 = $models.BranchInfo.createFrom;
const $$createType7 = $Create.Array($$createType6);
const $$createType8 = $models.CommitFileInfo.createFrom;
const $$createType9 = $Create.Array($$createType8);
const $$createType10 = $models.ProjectStatus.createFrom;
const $$createType11 = $Create.Nullable($$createType10);
const $$createType12 = $models.TreeNode.createFrom;
const $$createType10 = $models.CommitLog.createFrom;
const $$createType11 = $Create.Array($$createType10);
const $$createType12 = $models.ConflictFileInfo.createFrom;
const $$createType13 = $Create.Array($$createType12);
const $$createType14 = $models.TagInfo.createFrom;
const $$createType15 = $Create.Array($$createType14);
const $$createType16 = $models.UserInfo.createFrom;
const $$createType14 = $models.GitConfig.createFrom;
const $$createType15 = $Create.Nullable($$createType14);
const $$createType16 = $models.PlatformInfo.createFrom;
const $$createType17 = $Create.Nullable($$createType16);
const $$createType18 = $models.FileInfo.createFrom;
const $$createType19 = $Create.Array($$createType18);
const $$createType20 = $models.ProjectStatus.createFrom;
const $$createType21 = $Create.Nullable($$createType20);
const $$createType22 = $models.TreeNode.createFrom;
const $$createType23 = $Create.Array($$createType22);
const $$createType24 = $models.StashInfo.createFrom;
const $$createType25 = $Create.Array($$createType24);
const $$createType26 = $models.TagInfo.createFrom;
const $$createType27 = $Create.Array($$createType26);
const $$createType28 = $models.UserInfo.createFrom;
const $$createType29 = $Create.Nullable($$createType28);

View File

@@ -8,12 +8,17 @@ export {
};
export {
BatchPullResult,
BranchInfo,
CommitFileInfo,
CommitLog,
ConflictFileInfo,
FileInfo,
GitConfig,
PlatformInfo,
ProjectOverview,
ProjectStatus,
StashInfo,
TagInfo,
TreeNode,
UserInfo

View File

@@ -6,6 +6,58 @@
// @ts-ignore: Unused imports
import { Create as $Create } from "@wailsio/runtime";
/**
* BatchPullResult 批量 pull 结果
*/
export class BatchPullResult {
/**
* Creates a new BatchPullResult instance.
* @param {Partial<BatchPullResult>} [$$source = {}] - The source object to create the BatchPullResult.
*/
constructor($$source = {}) {
if (!("name" in $$source)) {
/**
* @member
* @type {string}
*/
this["name"] = "";
}
if (!("path" in $$source)) {
/**
* @member
* @type {string}
*/
this["path"] = "";
}
if (!("success" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["success"] = false;
}
if (!("message" in $$source)) {
/**
* @member
* @type {string}
*/
this["message"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new BatchPullResult instance from a string or object.
* @param {any} [$$source = {}]
* @returns {BatchPullResult}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new BatchPullResult(/** @type {Partial<BatchPullResult>} */($$parsedSource));
}
}
/**
* BranchInfo 分支信息
*/
@@ -155,6 +207,37 @@ export class CommitLog {
}
}
/**
* ConflictFileInfo 冲突文件信息
*/
export class ConflictFileInfo {
/**
* Creates a new ConflictFileInfo instance.
* @param {Partial<ConflictFileInfo>} [$$source = {}] - The source object to create the ConflictFileInfo.
*/
constructor($$source = {}) {
if (!("filePath" in $$source)) {
/**
* @member
* @type {string}
*/
this["filePath"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new ConflictFileInfo instance from a string or object.
* @param {any} [$$source = {}]
* @returns {ConflictFileInfo}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ConflictFileInfo(/** @type {Partial<ConflictFileInfo>} */($$parsedSource));
}
}
/**
* FileInfo 文件信息
*/
@@ -207,6 +290,44 @@ export class FileInfo {
}
}
/**
* GitConfig Git 全局配置
*/
export class GitConfig {
/**
* Creates a new GitConfig instance.
* @param {Partial<GitConfig>} [$$source = {}] - The source object to create the GitConfig.
*/
constructor($$source = {}) {
if (!("userName" in $$source)) {
/**
* @member
* @type {string}
*/
this["userName"] = "";
}
if (!("userEmail" in $$source)) {
/**
* @member
* @type {string}
*/
this["userEmail"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new GitConfig instance from a string or object.
* @param {any} [$$source = {}]
* @returns {GitConfig}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new GitConfig(/** @type {Partial<GitConfig>} */($$parsedSource));
}
}
/**
* PlatformInfo 平台信息
*/
@@ -245,6 +366,79 @@ export class PlatformInfo {
}
}
/**
* ProjectOverview 项目概览信息(轻量级)
*/
export class ProjectOverview {
/**
* Creates a new ProjectOverview instance.
* @param {Partial<ProjectOverview>} [$$source = {}] - The source object to create the ProjectOverview.
*/
constructor($$source = {}) {
if (!("key" in $$source)) {
/**
* @member
* @type {string}
*/
this["key"] = "";
}
if (!("name" in $$source)) {
/**
* @member
* @type {string}
*/
this["name"] = "";
}
if (!("path" in $$source)) {
/**
* @member
* @type {string}
*/
this["path"] = "";
}
if (!("branch" in $$source)) {
/**
* @member
* @type {string}
*/
this["branch"] = "";
}
if (!("hasChanges" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["hasChanges"] = false;
}
if (!("unpushed" in $$source)) {
/**
* @member
* @type {number}
*/
this["unpushed"] = 0;
}
if (/** @type {any} */(false)) {
/**
* @member
* @type {string | undefined}
*/
this["error"] = undefined;
}
Object.assign(this, $$source);
}
/**
* Creates a new ProjectOverview instance from a string or object.
* @param {any} [$$source = {}]
* @returns {ProjectOverview}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ProjectOverview(/** @type {Partial<ProjectOverview>} */($$parsedSource));
}
}
/**
* ProjectStatus 项目状态信息
*/
@@ -294,6 +488,58 @@ export class ProjectStatus {
}
}
/**
* StashInfo 贮藏信息
*/
export class StashInfo {
/**
* Creates a new StashInfo instance.
* @param {Partial<StashInfo>} [$$source = {}] - The source object to create the StashInfo.
*/
constructor($$source = {}) {
if (!("index" in $$source)) {
/**
* @member
* @type {number}
*/
this["index"] = 0;
}
if (!("ref" in $$source)) {
/**
* @member
* @type {string}
*/
this["ref"] = "";
}
if (!("message" in $$source)) {
/**
* @member
* @type {string}
*/
this["message"] = "";
}
if (!("timestamp" in $$source)) {
/**
* @member
* @type {number}
*/
this["timestamp"] = 0;
}
Object.assign(this, $$source);
}
/**
* Creates a new StashInfo instance from a string or object.
* @param {any} [$$source = {}]
* @returns {StashInfo}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new StashInfo(/** @type {Partial<StashInfo>} */($$parsedSource));
}
}
/**
* TagInfo 标签信息
*/

View File

@@ -97,6 +97,31 @@ func (s *AppService) GetProjectTree() []TreeNode {
// --- 项目管理 ---
// CloneProject 克隆远程仓库到本地目录,并添加到项目树
func (s *AppService) CloneProject(platform, username, repoURL, parentDir, name string) error {
if repoURL == "" {
return fmt.Errorf("仓库地址不能为空")
}
if parentDir == "" {
return fmt.Errorf("目标目录不能为空")
}
if name == "" {
return fmt.Errorf("项目名称不能为空")
}
// 目标路径: parentDir/name
targetPath := parentDir + "/" + name
// 执行 git clone
_, err := s.gitClient.Clone(repoURL, targetPath)
if err != nil {
return fmt.Errorf("克隆失败: %w", err)
}
// 克隆成功后添加到项目树
return s.AddProject(platform, username, name, targetPath)
}
// AddProject 添加项目到指定平台/用户下
func (s *AppService) AddProject(platform, username, name, path string) error {
p, ok := s.config.Platforms[platform]
@@ -795,3 +820,453 @@ func (s *AppService) PushTag(path, name string) error {
_, err := s.gitClient.PushTag(path, name)
return err
}
// --- 分支管理 ---
// CreateBranch 创建新分支
func (s *AppService) CreateBranch(path, name string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("分支名不能为空")
}
_, err := s.gitClient.CreateBranch(path, name)
return err
}
// DeleteBranch 删除本地分支
func (s *AppService) DeleteBranch(path, name string, force bool) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("分支名不能为空")
}
var err error
if force {
_, err = s.gitClient.ForceDeleteBranch(path, name)
} else {
_, err = s.gitClient.DeleteBranch(path, name)
}
return err
}
// MergeBranch 合并指定分支到当前分支
func (s *AppService) MergeBranch(path, branch string) (string, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return "", fmt.Errorf("项目路径不存在: %s", path)
}
branch = strings.TrimSpace(branch)
if branch == "" {
return "", fmt.Errorf("分支名不能为空")
}
return s.gitClient.MergeBranch(path, branch)
}
// --- 远程分支管理 ---
// GetRemoteBranches 获取远程分支列表
func (s *AppService) GetRemoteBranches(path string) ([]BranchInfo, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", path)
}
out, err := s.gitClient.RemoteBranchList(path)
if err != nil {
return nil, fmt.Errorf("获取远程分支列表失败: %w", err)
}
var branches []BranchInfo
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
name := strings.TrimSpace(line)
if name == "" || strings.Contains(name, "HEAD") {
continue
}
branches = append(branches, BranchInfo{Name: name, Current: false})
}
return branches, nil
}
// CheckoutRemoteBranch 检出远程分支到本地
func (s *AppService) CheckoutRemoteBranch(path, remoteBranch string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
remoteBranch = strings.TrimSpace(remoteBranch)
if remoteBranch == "" {
return fmt.Errorf("远程分支名不能为空")
}
// origin/feature -> feature
localBranch := remoteBranch
if idx := strings.Index(remoteBranch, "/"); idx != -1 {
localBranch = remoteBranch[idx+1:]
}
_, err := s.gitClient.CheckoutNewBranch(path, localBranch, remoteBranch)
return err
}
// DeleteRemoteBranch 删除远程分支
func (s *AppService) DeleteRemoteBranch(path, branch string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
branch = strings.TrimSpace(branch)
if branch == "" {
return fmt.Errorf("分支名不能为空")
}
// origin/feature -> feature
localName := branch
if idx := strings.Index(branch, "/"); idx != -1 {
localName = branch[idx+1:]
}
_, err := s.gitClient.DeleteRemoteBranch(path, localName)
return err
}
// --- Stash 贮藏管理 ---
// StashInfo 贮藏信息
type StashInfo struct {
Index int `json:"index"`
Ref string `json:"ref"`
Message string `json:"message"`
Timestamp int64 `json:"timestamp"`
}
// StashSave 保存当前变更到贮藏
func (s *AppService) StashSave(path, message string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
_, err := s.gitClient.StashSave(path, message)
return err
}
// GetStashList 获取贮藏列表
func (s *AppService) GetStashList(path string) ([]StashInfo, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", path)
}
out, err := s.gitClient.StashList(path)
if err != nil {
return nil, fmt.Errorf("获取贮藏列表失败: %w", err)
}
var stashes []StashInfo
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
if line == "" {
continue
}
parts := strings.SplitN(line, "\t", 3)
if len(parts) < 2 {
continue
}
ref := parts[0]
message := parts[1]
var ts int64
if len(parts) >= 3 {
fmt.Sscanf(parts[2], "%d", &ts)
}
var index int
fmt.Sscanf(ref, "stash@{%d}", &index)
stashes = append(stashes, StashInfo{
Index: index,
Ref: ref,
Message: message,
Timestamp: ts,
})
}
return stashes, nil
}
// StashApply 应用贮藏(不删除)
func (s *AppService) StashApply(path string, index int) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
_, err := s.gitClient.StashApply(path, index)
return err
}
// StashPop 应用贮藏并删除
func (s *AppService) StashPop(path string, index int) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
_, err := s.gitClient.StashPop(path, index)
return err
}
// StashDrop 删除贮藏
func (s *AppService) StashDrop(path string, index int) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
_, err := s.gitClient.StashDrop(path, index)
return err
}
// --- 设置管理 ---
// GitConfig Git 全局配置
type GitConfig struct {
UserName string `json:"userName"`
UserEmail string `json:"userEmail"`
}
// GetGitGlobalConfig 获取 git 全局配置
func (s *AppService) GetGitGlobalConfig() (*GitConfig, error) {
name, _ := s.gitClient.GetGitGlobalConfig("user.name")
email, _ := s.gitClient.GetGitGlobalConfig("user.email")
return &GitConfig{
UserName: name,
UserEmail: email,
}, nil
}
// SetGitGlobalConfig 设置 git 全局配置
func (s *AppService) SetGitGlobalConfig(name, email string) error {
if name != "" {
if _, err := s.gitClient.SetGitGlobalConfig("user.name", name); err != nil {
return fmt.Errorf("设置 user.name 失败: %w", err)
}
}
if email != "" {
if _, err := s.gitClient.SetGitGlobalConfig("user.email", email); err != nil {
return fmt.Errorf("设置 user.email 失败: %w", err)
}
}
return nil
}
// GetAppSettings 获取应用设置
func (s *AppService) GetAppSettings() *config.Settings {
return &s.config.Settings
}
// UpdateAppSettings 更新应用设置
func (s *AppService) UpdateAppSettings(logLevel string) error {
s.config.Settings.LogLevel = logLevel
return config.SaveConfig(s.config)
}
// --- 冲突处理 ---
// ConflictFileInfo 冲突文件信息
type ConflictFileInfo struct {
FilePath string `json:"filePath"`
}
// GetConflictFiles 获取冲突文件列表
func (s *AppService) GetConflictFiles(path string) ([]ConflictFileInfo, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", path)
}
out, err := s.gitClient.ConflictFiles(path)
if err != nil {
// 没有冲突文件时命令可能返回错误,返回空列表
return []ConflictFileInfo{}, nil
}
var files []ConflictFileInfo
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
files = append(files, ConflictFileInfo{FilePath: line})
}
return files, nil
}
// GetConflictFileContent 获取冲突文件内容(包含冲突标记)
func (s *AppService) GetConflictFileContent(projectPath, filePath string) (string, error) {
fullPath := filepath.Join(projectPath, filePath)
data, err := os.ReadFile(fullPath)
if err != nil {
return "", fmt.Errorf("读取冲突文件失败: %w", err)
}
return string(data), nil
}
// ResolveConflictFile 将冲突文件标记为已解决
func (s *AppService) ResolveConflictFile(path string, files []string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
if len(files) == 0 {
return fmt.Errorf("未指定文件")
}
_, err := s.gitClient.MarkResolved(path, files...)
return err
}
// SaveConflictFile 保存冲突文件内容(手动解决冲突后保存)
func (s *AppService) SaveConflictFile(projectPath, filePath, content string) error {
fullPath := filepath.Join(projectPath, filePath)
return os.WriteFile(fullPath, []byte(content), 0o644)
}
// AbortMerge 中止合并
func (s *AppService) AbortMerge(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path)
}
_, err := s.gitClient.AbortMerge(path)
return err
}
// IsMerging 检查是否处于合并状态
func (s *AppService) IsMerging(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return s.gitClient.MergeStatus(path)
}
// --- 提交搜索 ---
// SearchCommitLog 搜索提交历史
func (s *AppService) SearchCommitLog(path, keyword, author string, maxCount int) ([]CommitLog, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", path)
}
if maxCount <= 0 {
maxCount = 100
}
out, err := s.gitClient.SearchCommits(path, keyword, author, maxCount)
if err != nil {
return nil, fmt.Errorf("搜索提交历史失败: %w", err)
}
logs := parseCommitLog(out)
// 标记推送状态
branch, brErr := s.gitClient.Branch(path)
if brErr == nil {
branch = strings.TrimSpace(branch)
unpushedOut, upErr := s.gitClient.UnpushedCommits(path, branch)
unpushedSet := make(map[string]bool)
if upErr == nil {
for _, h := range strings.Split(strings.TrimSpace(unpushedOut), "\n") {
if h != "" {
unpushedSet[h] = true
}
}
}
for i := range logs {
logs[i].Pushed = !unpushedSet[logs[i].Hash]
}
}
return logs, nil
}
// --- 批量操作 ---
// ProjectOverview 项目概览信息(轻量级)
type ProjectOverview struct {
Key string `json:"key"`
Name string `json:"name"`
Path string `json:"path"`
Branch string `json:"branch"`
HasChanges bool `json:"hasChanges"`
Unpushed int `json:"unpushed"`
Error string `json:"error,omitempty"`
}
// GetAllProjectOverview 获取所有项目的概览状态
func (s *AppService) GetAllProjectOverview() []ProjectOverview {
var results []ProjectOverview
for platformName, platform := range s.config.Platforms {
for _, user := range platform.Users {
for _, proj := range user.Projects {
overview := ProjectOverview{
Key: platformName + "/" + user.Username + "/" + proj.Name,
Name: proj.Name,
Path: proj.Path,
}
if _, err := os.Stat(proj.Path); os.IsNotExist(err) {
overview.Error = "路径不存在"
results = append(results, overview)
continue
}
branch, hasChanges, unpushed, err := s.gitClient.QuickStatus(proj.Path)
if err != nil {
overview.Error = err.Error()
} else {
overview.Branch = branch
overview.HasChanges = hasChanges
overview.Unpushed = unpushed
}
results = append(results, overview)
}
}
}
return results
}
// BatchPullResult 批量 pull 结果
type BatchPullResult struct {
Name string `json:"name"`
Path string `json:"path"`
Success bool `json:"success"`
Message string `json:"message"`
}
// BatchPull 批量拉取指定项目
func (s *AppService) BatchPull(paths []string) []BatchPullResult {
var results []BatchPullResult
for _, path := range paths {
result := BatchPullResult{Path: path}
// 从路径推断名称
result.Name = filepath.Base(path)
if _, err := os.Stat(path); os.IsNotExist(err) {
result.Message = "路径不存在"
results = append(results, result)
continue
}
branch, err := s.gitClient.Branch(path)
if err != nil {
result.Message = "获取分支失败: " + err.Error()
results = append(results, result)
continue
}
_, err = s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch))
if err != nil {
result.Message = err.Error()
} else {
result.Success = true
result.Message = "拉取成功"
}
results = append(results, result)
}
return results
}
// BatchPush 批量推送指定项目
func (s *AppService) BatchPush(paths []string) []BatchPullResult {
var results []BatchPullResult
for _, path := range paths {
result := BatchPullResult{Path: path}
result.Name = filepath.Base(path)
if _, err := os.Stat(path); os.IsNotExist(err) {
result.Message = "路径不存在"
results = append(results, result)
continue
}
branch, err := s.gitClient.Branch(path)
if err != nil {
result.Message = "获取分支失败: " + err.Error()
results = append(results, result)
continue
}
_, err = s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch))
if err != nil {
result.Message = err.Error()
} else {
result.Success = true
result.Message = "推送成功"
}
results = append(results, result)
}
return results
}

View File

@@ -65,7 +65,28 @@ func (g *GitClient) Status(path string) (string, error) {
}
func (g *GitClient) Clone(repoURL, path string) (string, error) {
return g.Run(".", "clone", repoURL, path)
// Clone 操作可能需要较长时间使用独立的超时设置10 分钟)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
args := []string{"-c", "core.quotePath=false"}
if g.Enabled {
args = append(args, "-c", "http.proxy="+g.Proxy, "-c", "https.proxy="+g.Proxy)
}
args = append(args, "clone", "--progress", repoURL, path)
log.Println(args)
cmd := exec.CommandContext(ctx, "git", args...)
hideWindow(cmd)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if ctx.Err() == context.DeadlineExceeded {
return "", fmt.Errorf("git clone timeout")
}
if err != nil {
return "", fmt.Errorf("git clone error: %v, stderr: %s", err, stderr.String())
}
return stdout.String(), nil
}
func (g *GitClient) Fetch(path string) (string, error) {
@@ -327,6 +348,174 @@ func parseNameStatus(output string) []FileChange {
return changes
}
// CreateBranch 创建新分支
func (g *GitClient) CreateBranch(path, name string) (string, error) {
return g.Run(path, "branch", name)
}
// DeleteBranch 删除本地分支 (-d 安全删除)
func (g *GitClient) DeleteBranch(path, name string) (string, error) {
return g.Run(path, "branch", "-d", name)
}
// ForceDeleteBranch 强制删除本地分支 (-D)
func (g *GitClient) ForceDeleteBranch(path, name string) (string, error) {
return g.Run(path, "branch", "-D", name)
}
// MergeBranch 合并指定分支到当前分支
func (g *GitClient) MergeBranch(path, branch string) (string, error) {
return g.Run(path, "merge", branch)
}
// DeleteRemoteBranch 删除远程分支
func (g *GitClient) DeleteRemoteBranch(path, branch string) (string, error) {
return g.Run(path, "push", "origin", "--delete", branch)
}
// RemoteBranchList 获取所有远程分支
func (g *GitClient) RemoteBranchList(path string) (string, error) {
return g.Run(path, "branch", "-r", "--format=%(refname:short)")
}
// CheckoutNewBranch 从远程分支检出新本地分支
func (g *GitClient) CheckoutNewBranch(path, localBranch, remoteBranch string) (string, error) {
return g.Run(path, "checkout", "-b", localBranch, remoteBranch)
}
// StashSave 保存当前工作区变更到贮藏
func (g *GitClient) StashSave(path, message string) (string, error) {
if message != "" {
return g.Run(path, "stash", "push", "-m", message)
}
return g.Run(path, "stash", "push")
}
// StashList 获取贮藏列表
func (g *GitClient) StashList(path string) (string, error) {
return g.Run(path, "stash", "list", "--format=%gd\t%s\t%at")
}
// StashApply 应用贮藏(不删除)
func (g *GitClient) StashApply(path string, index int) (string, error) {
return g.Run(path, "stash", "apply", fmt.Sprintf("stash@{%d}", index))
}
// StashPop 应用贮藏并删除
func (g *GitClient) StashPop(path string, index int) (string, error) {
return g.Run(path, "stash", "pop", fmt.Sprintf("stash@{%d}", index))
}
// StashDrop 删除贮藏
func (g *GitClient) StashDrop(path string, index int) (string, error) {
return g.Run(path, "stash", "drop", fmt.Sprintf("stash@{%d}", index))
}
// GetGitGlobalConfig 获取 git 全局配置
func (g *GitClient) GetGitGlobalConfig(key string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "git", "config", "--global", "--get", key)
hideWindow(cmd)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", nil // key 不存在不算错误
}
return strings.TrimSpace(stdout.String()), nil
}
// SetGitGlobalConfig 设置 git 全局配置
func (g *GitClient) SetGitGlobalConfig(key, value string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "git", "config", "--global", key, value)
hideWindow(cmd)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("git config error: %v, stderr: %s", err, stderr.String())
}
return strings.TrimSpace(stdout.String()), nil
}
// --- 冲突处理 ---
// ConflictFiles 获取冲突文件列表 (git diff --name-only --diff-filter=U)
func (g *GitClient) ConflictFiles(path string) (string, error) {
return g.Run(path, "diff", "--name-only", "--diff-filter=U")
}
// GetConflictContent 获取冲突文件的完整内容(含冲突标记)
func (g *GitClient) GetConflictContent(path, filePath string) (string, error) {
return g.Run(path, "show", ":0:"+filePath)
}
// MarkResolved 将冲突文件标记为已解决 (git add <file>)
func (g *GitClient) MarkResolved(path string, files ...string) (string, error) {
args := append([]string{"add"}, files...)
return g.Run(path, args...)
}
// AbortMerge 中止合并 (git merge --abort)
func (g *GitClient) AbortMerge(path string) (string, error) {
return g.Run(path, "merge", "--abort")
}
// MergeStatus 检查是否处于合并状态
func (g *GitClient) MergeStatus(path string) bool {
// 检查 .git/MERGE_HEAD 文件是否存在
_, err := g.Run(path, "rev-parse", "--verify", "MERGE_HEAD")
return err == nil
}
// --- 提交搜索 ---
// SearchCommits 搜索提交历史 (git log --grep / --author / --after / --before)
func (g *GitClient) SearchCommits(path string, keyword, author string, maxCount int) (string, error) {
args := []string{"log"}
if maxCount > 0 {
args = append(args, fmt.Sprintf("--max-count=%d", maxCount))
}
if keyword != "" {
args = append(args, "--grep="+keyword, "-i")
}
if author != "" {
args = append(args, "--author="+author)
}
args = append(args, "--format=%H%n%h%n%an%n%ae%n%at%n%s%n---END---")
return g.Run(path, args...)
}
// --- 批量操作 ---
// QuickStatus 快速获取分支名和是否有变更(轻量级)
func (g *GitClient) QuickStatus(path string) (branch string, hasChanges bool, unpushed int, err error) {
branchOut, err := g.Branch(path)
if err != nil {
return "", false, 0, err
}
branch = strings.TrimSpace(branchOut)
// 检查是否有变更
statusOut, err := g.Run(path, "status", "--porcelain")
if err == nil {
hasChanges = strings.TrimSpace(statusOut) != ""
}
// 检查未推送提交数
unpushedOut, err := g.Run(path, "rev-list", "--count", "origin/"+branch+"..HEAD")
if err == nil {
fmt.Sscanf(strings.TrimSpace(unpushedOut), "%d", &unpushed)
}
return branch, hasChanges, unpushed, nil
}
// StatusText 返回变更状态的中文描述
func (fc FileChange) StatusText() string {
switch {

View File

@@ -4,8 +4,6 @@ import (
"fmt"
"os"
"strings"
"golang.org/x/sys/windows/registry"
)
// ProxyInfo 保存当前系统代理信息
@@ -25,8 +23,8 @@ type ProxyInfo struct {
}
// GetCurrentProxy 获取本机当前的代理设置
// 优先读取 Windows 系统代理(注册表),同时也读取环境变量中的代理配置
// 支持检测 Clash、V2Ray 等常见代理工具的配置
// 优先读取系统代理(Windows 注册表 / macOS scutil / Linux 环境变量),
// 同时也读取环境变量中的代理配置
func GetCurrentProxy() (*ProxyInfo, error) {
info := &ProxyInfo{}
@@ -34,9 +32,8 @@ func GetCurrentProxy() (*ProxyInfo, error) {
info.HTTPProxy = getEnvProxy("HTTP_PROXY", "http_proxy")
info.HTTPSProxy = getEnvProxy("HTTPS_PROXY", "https_proxy")
// 2. 从 Windows 注册表读取系统代理设置
if err := readWindowsSystemProxy(info); err != nil {
// 注册表读取失败不算致命错误,环境变量中可能仍有代理信息
// 2. 从系统设置读取代理(平台相关)
if err := readSystemProxy(info); err != nil {
fmt.Printf("读取系统代理设置时出错: %v\n", err)
}
@@ -54,42 +51,6 @@ func GetCurrentProxy() (*ProxyInfo, error) {
return info, nil
}
// readWindowsSystemProxy 从 Windows 注册表读取系统代理设置
func readWindowsSystemProxy(info *ProxyInfo) error {
key, err := registry.OpenKey(
registry.CURRENT_USER,
`Software\Microsoft\Windows\CurrentVersion\Internet Settings`,
registry.QUERY_VALUE,
)
if err != nil {
return fmt.Errorf("无法打开注册表键: %w", err)
}
defer key.Close()
// 读取代理是否启用 (ProxyEnable: 0=关闭, 1=开启)
proxyEnable, _, err := key.GetIntegerValue("ProxyEnable")
if err != nil {
return fmt.Errorf("无法读取 ProxyEnable: %w", err)
}
info.Enabled = proxyEnable == 1
// 读取代理服务器地址
proxyServer, _, err := key.GetStringValue("ProxyServer")
if err == nil && proxyServer != "" {
info.Server = proxyServer
// 根据端口推断代理协议
info.Protocol = guessProtocol(proxyServer)
}
// 读取代理绕过列表
bypass, _, err := key.GetStringValue("ProxyOverride")
if err == nil {
info.Bypass = bypass
}
return nil
}
// getEnvProxy 依次检查多个环境变量名,返回第一个非空值
func getEnvProxy(names ...string) string {
for _, name := range names {
@@ -104,11 +65,9 @@ func getEnvProxy(names ...string) string {
// 例如 "http://127.0.0.1:7890" -> "127.0.0.1:7890"
func extractServerFromURL(proxyURL string) string {
s := proxyURL
// 去掉协议前缀
if idx := strings.Index(s, "://"); idx != -1 {
s = s[idx+3:]
}
// 去掉尾部斜杠
s = strings.TrimRight(s, "/")
return s
}
@@ -127,14 +86,14 @@ func extractProtocolFromURL(proxyURL string) string {
// V2Ray 默认: 10808(socks5), 10809(http)
func guessProtocol(server string) string {
knownPorts := map[string]string{
"7890": "http", // Clash HTTP
"7891": "socks5", // Clash SOCKS5
"7892": "http", // Clash mixed
"10808": "socks5", // V2Ray SOCKS5
"10809": "http", // V2Ray HTTP
"1080": "socks5", // 通用 SOCKS5
"1081": "http", // 通用 HTTP
"8080": "http", // 通用 HTTP
"7890": "http",
"7891": "socks5",
"7892": "http",
"10808": "socks5",
"10809": "http",
"1080": "socks5",
"1081": "http",
"8080": "http",
}
parts := strings.Split(server, ":")

82
internal/proxy_darwin.go Normal file
View File

@@ -0,0 +1,82 @@
//go:build darwin
package internal
import (
"fmt"
"os/exec"
"strings"
)
// readSystemProxy 从 macOS 系统偏好设置读取代理配置
// 使用 scutil --proxy 命令获取系统代理信息
func readSystemProxy(info *ProxyInfo) error {
out, err := exec.Command("scutil", "--proxy").Output()
if err != nil {
return fmt.Errorf("执行 scutil --proxy 失败: %w", err)
}
lines := strings.Split(string(out), "\n")
settings := make(map[string]string)
for _, line := range lines {
line = strings.TrimSpace(line)
parts := strings.SplitN(line, " : ", 2)
if len(parts) == 2 {
settings[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
// 检查 HTTP 代理
if settings["HTTPEnable"] == "1" {
host := settings["HTTPProxy"]
port := settings["HTTPPort"]
if host != "" {
info.Enabled = true
info.Server = host
if port != "" && port != "0" {
info.Server = host + ":" + port
}
info.Protocol = "http"
}
}
// 检查 HTTPS 代理
if !info.Enabled && settings["HTTPSEnable"] == "1" {
host := settings["HTTPSProxy"]
port := settings["HTTPSPort"]
if host != "" {
info.Enabled = true
info.Server = host
if port != "" && port != "0" {
info.Server = host + ":" + port
}
info.Protocol = "http"
}
}
// 检查 SOCKS 代理
if !info.Enabled && settings["SOCKSEnable"] == "1" {
host := settings["SOCKSProxy"]
port := settings["SOCKSPort"]
if host != "" {
info.Enabled = true
info.Server = host
if port != "" && port != "0" {
info.Server = host + ":" + port
}
info.Protocol = "socks5"
}
}
// 如果通过以上方式检测到了代理,根据端口再次猜测协议
if info.Enabled && info.Server != "" {
info.Protocol = guessProtocol(info.Server)
}
// 读取绕过列表
if exceptions, ok := settings["ExceptionsList"]; ok {
info.Bypass = exceptions
}
return nil
}

122
internal/proxy_linux.go Normal file
View File

@@ -0,0 +1,122 @@
//go:build linux
package internal
import (
"fmt"
"os/exec"
"strings"
)
// readSystemProxy 从 Linux 系统读取代理配置
// 优先尝试 GNOME (gsettings),失败则回退到环境变量
func readSystemProxy(info *ProxyInfo) error {
// 尝试通过 gsettings 读取 GNOME 系统代理
if err := readGnomeProxy(info); err == nil && info.Enabled {
return nil
}
// 尝试通过 KDE 配置读取代理
if err := readKDEProxy(info); err == nil && info.Enabled {
return nil
}
// Linux 上环境变量已由 GetCurrentProxy 中读取,此处无需额外操作
return nil
}
// readGnomeProxy 通过 gsettings 读取 GNOME 桌面环境的代理配置
func readGnomeProxy(info *ProxyInfo) error {
// 检查 gsettings 是否可用
if _, err := exec.LookPath("gsettings"); err != nil {
return fmt.Errorf("gsettings 不可用: %w", err)
}
// 读取代理模式
mode, err := gsettingsGet("org.gnome.system.proxy", "mode")
if err != nil {
return err
}
if mode != "'manual'" {
return nil // 代理未启用
}
// 读取 HTTP 代理
host, err := gsettingsGet("org.gnome.system.proxy.http", "host")
if err == nil && host != "" && host != "''" {
host = strings.Trim(host, "'")
port, _ := gsettingsGet("org.gnome.system.proxy.http", "port")
port = strings.TrimSpace(port)
if port != "" && port != "0" {
info.Enabled = true
info.Server = host + ":" + port
info.Protocol = guessProtocol(info.Server)
return nil
}
}
// 读取 SOCKS 代理
host, err = gsettingsGet("org.gnome.system.proxy.socks", "host")
if err == nil && host != "" && host != "''" {
host = strings.Trim(host, "'")
port, _ := gsettingsGet("org.gnome.system.proxy.socks", "port")
port = strings.TrimSpace(port)
if port != "" && port != "0" {
info.Enabled = true
info.Server = host + ":" + port
info.Protocol = "socks5"
return nil
}
}
return nil
}
// readKDEProxy 通过 kreadconfig5/kreadconfig6 读取 KDE 代理配置
func readKDEProxy(info *ProxyInfo) error {
// 尝试 kreadconfig5 或 kreadconfig6
var cmd string
if _, err := exec.LookPath("kreadconfig6"); err == nil {
cmd = "kreadconfig6"
} else if _, err := exec.LookPath("kreadconfig5"); err == nil {
cmd = "kreadconfig5"
} else {
return fmt.Errorf("kreadconfig 不可用")
}
// 读取代理类型
proxyType, err := kdeGet(cmd, "Proxy Settings", "ProxyType")
if err != nil || proxyType != "1" {
return nil // 0 = 无代理, 1 = 手动
}
// 读取 HTTP 代理
httpProxy, err := kdeGet(cmd, "Proxy Settings", "httpProxy")
if err == nil && httpProxy != "" {
info.Enabled = true
info.Server = extractServerFromURL(httpProxy)
info.Protocol = guessProtocol(info.Server)
return nil
}
return nil
}
// gsettingsGet 执行 gsettings get 命令
func gsettingsGet(schema, key string) (string, error) {
out, err := exec.Command("gsettings", "get", schema, key).Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// kdeGet 执行 kreadconfig 命令读取 KDE 配置
func kdeGet(cmd, group, key string) (string, error) {
out, err := exec.Command(cmd, "--file", "kioslaverc", "--group", group, "--key", key).Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}

43
internal/proxy_windows.go Normal file
View File

@@ -0,0 +1,43 @@
package internal
import (
"fmt"
"golang.org/x/sys/windows/registry"
)
// readSystemProxy 从 Windows 注册表读取系统代理设置
func readSystemProxy(info *ProxyInfo) error {
key, err := registry.OpenKey(
registry.CURRENT_USER,
`Software\Microsoft\Windows\CurrentVersion\Internet Settings`,
registry.QUERY_VALUE,
)
if err != nil {
return fmt.Errorf("无法打开注册表键: %w", err)
}
defer key.Close()
// 读取代理是否启用 (ProxyEnable: 0=关闭, 1=开启)
proxyEnable, _, err := key.GetIntegerValue("ProxyEnable")
if err != nil {
return fmt.Errorf("无法读取 ProxyEnable: %w", err)
}
info.Enabled = proxyEnable == 1
// 读取代理服务器地址
proxyServer, _, err := key.GetStringValue("ProxyServer")
if err == nil && proxyServer != "" {
info.Server = proxyServer
// 根据端口推断代理协议
info.Protocol = guessProtocol(proxyServer)
}
// 读取代理绕过列表
bypass, _, err := key.GetStringValue("ProxyOverride")
if err == nil {
info.Bypass = bypass
}
return nil
}