diff --git a/frontend/app/components/ContentArea.vue b/frontend/app/components/ContentArea.vue index ee75366..e10172a 100644 --- a/frontend/app/components/ContentArea.vue +++ b/frontend/app/components/ContentArea.vue @@ -25,8 +25,14 @@ import { CloudUploadOutlined as PushTagIcon, DeleteOutlined, SendOutlined, + MergeCellsOutlined, + InboxOutlined, + SettingOutlined, + GlobalOutlined, + SaveOutlined, + DownloadOutlined, } from '@ant-design/icons-vue' -import { Modal } from 'ant-design-vue' +import { Modal, message } from 'ant-design-vue' import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app' import FileTreeNode from './FileTreeNode.vue' import CommitFileTreeNode from './CommitFileTreeNode.vue' @@ -76,6 +82,24 @@ const newTagName = ref('') const newTagMessage = ref('') const createTagLoading = ref(false) +// ---- 分支管理 ---- +const newBranchName = ref('') +const createBranchLoading = ref(false) +const remoteBranches = ref([]) + +// ---- Stash 贮藏管理 ---- +const stashList = ref([]) +const stashLoading = ref(false) +const showStashSave = ref(false) +const stashMessage = ref('') +const stashSaveLoading = ref(false) +const showStashPanel = ref(false) + +// ---- 设置 ---- +const showSettings = ref(false) +const settingsLoading = ref(false) +const gitConfig = ref({ userName: '', userEmail: '' }) + // ---- 文件列表拖拽调整宽度 ---- const fileListWidth = ref(300) const MIN_FILELIST = 180 @@ -511,6 +535,8 @@ async function loadBranches() { } catch (e) { console.error('获取分支失败:', e) } + loadRemoteBranches() + loadStashList() } async function switchBranch(branchName) { @@ -527,6 +553,247 @@ async function switchBranch(branchName) { } } +async function createBranch() { + if (!props.project?.path || !newBranchName.value.trim()) return + createBranchLoading.value = true + try { + await AppService.CreateBranch(props.project.path, newBranchName.value.trim()) + newBranchName.value = '' + await loadBranches() + message.success('分支创建成功') + } catch (e) { + console.error('创建分支失败:', e) + Modal.error({ title: '创建分支失败', content: String(e) }) + } finally { + createBranchLoading.value = false + } +} + +async function deleteBranch(branchName, force = false) { + if (!props.project?.path) return + Modal.confirm({ + title: '确认删除分支', + icon: h(ExclamationCircleOutlined), + content: h('div', [ + h('p', `确定要删除分支吗?`), + h('p', { style: 'font-family: monospace; color: #89b4fa; font-size: 15px;' }, branchName), + ]), + okText: '删除', + cancelText: '取消', + okButtonProps: { danger: true }, + async onOk() { + try { + await AppService.DeleteBranch(props.project.path, branchName, force) + await loadBranches() + message.success(`分支 ${branchName} 已删除`) + } catch (e) { + // 如果安全删除失败,提示是否强制删除 + if (!force && String(e).includes('not fully merged')) { + Modal.confirm({ + title: '分支未完全合并', + icon: h(ExclamationCircleOutlined), + content: h('div', [ + h('p', `分支 ${branchName} 尚未完全合并到当前分支。`), + h('p', { style: 'color: #f38ba8;' }, '是否强制删除?'), + ]), + okText: '强制删除', + cancelText: '取消', + okButtonProps: { danger: true }, + async onOk() { + await deleteBranch(branchName, true) + }, + }) + } else { + Modal.error({ title: '删除分支失败', content: String(e) }) + } + } + }, + }) +} + +async function mergeBranch(branchName) { + if (!props.project?.path) return + Modal.confirm({ + title: '确认合并分支', + icon: h(MergeCellsOutlined), + content: h('div', [ + h('p', [ + '将 ', + h('span', { style: 'font-family: monospace; color: #89b4fa;' }, branchName), + ' 合并到 ', + h('span', { style: 'font-family: monospace; color: #a6e3a1;' }, status.value?.branch || '当前分支'), + ]), + ]), + okText: '合并', + cancelText: '取消', + async onOk() { + try { + const result = await AppService.MergeBranch(props.project.path, branchName) + message.success('合并成功') + await loadStatus() + } catch (e) { + console.error('合并分支失败:', e) + Modal.error({ title: '合并失败', content: String(e) }) + } + }, + }) +} + +// ---- 远程分支 ---- +async function loadRemoteBranches() { + if (!props.project?.path) return + try { + const list = await AppService.GetRemoteBranches(props.project.path) + remoteBranches.value = Array.isArray(list) ? list : [] + } catch (e) { + console.error('获取远程分支失败:', e) + } +} + +async function checkoutRemoteBranch(remoteBranch) { + if (!props.project?.path) return + showBranchDropdown.value = false + try { + await AppService.CheckoutRemoteBranch(props.project.path, remoteBranch) + message.success('检出成功') + await loadStatus() + } catch (e) { + console.error('检出远程分支失败:', e) + Modal.error({ title: '检出失败', content: String(e) }) + } +} + +async function deleteRemoteBranch(remoteBranch) { + if (!props.project?.path) return + Modal.confirm({ + title: '确认删除远程分支', + icon: h(ExclamationCircleOutlined), + content: h('div', [ + h('p', '确定要删除远程分支吗?此操作不可逆!'), + h('p', { style: 'font-family: monospace; color: #f38ba8; font-size: 15px;' }, remoteBranch), + ]), + okText: '删除', + cancelText: '取消', + okButtonProps: { danger: true }, + async onOk() { + try { + await AppService.DeleteRemoteBranch(props.project.path, remoteBranch) + await loadRemoteBranches() + message.success(`远程分支 ${remoteBranch} 已删除`) + } catch (e) { + Modal.error({ title: '删除远程分支失败', content: String(e) }) + } + }, + }) +} + +// ---- Stash 贮藏管理 ---- +async function loadStashList() { + if (!props.project?.path) return + stashLoading.value = true + try { + const list = await AppService.GetStashList(props.project.path) + stashList.value = Array.isArray(list) ? list : [] + } catch (e) { + console.error('获取贮藏列表失败:', e) + stashList.value = [] + } finally { + stashLoading.value = false + } +} + +async function stashSave() { + if (!props.project?.path) return + stashSaveLoading.value = true + try { + await AppService.StashSave(props.project.path, stashMessage.value.trim()) + stashMessage.value = '' + showStashSave.value = false + message.success('已贮藏当前变更') + await refreshFiles() + await loadStashList() + } catch (e) { + console.error('贮藏失败:', e) + Modal.error({ title: '贮藏失败', content: String(e) }) + } finally { + stashSaveLoading.value = false + } +} + +async function stashApply(index) { + if (!props.project?.path) return + try { + await AppService.StashApply(props.project.path, index) + message.success('已应用贮藏') + await refreshFiles() + } catch (e) { + Modal.error({ title: '应用贮藏失败', content: String(e) }) + } +} + +async function stashPop(index) { + if (!props.project?.path) return + try { + await AppService.StashPop(props.project.path, index) + message.success('已应用并删除贮藏') + await refreshFiles() + await loadStashList() + } catch (e) { + Modal.error({ title: '应用贮藏失败', content: String(e) }) + } +} + +async function stashDrop(index) { + if (!props.project?.path) return + Modal.confirm({ + title: '确认删除贮藏', + icon: h(ExclamationCircleOutlined), + content: `确定要删除 stash@{${index}} 吗?`, + okText: '删除', + cancelText: '取消', + okButtonProps: { danger: true }, + async onOk() { + try { + await AppService.StashDrop(props.project.path, index) + message.success('已删除贮藏') + await loadStashList() + } catch (e) { + Modal.error({ title: '删除贮藏失败', content: String(e) }) + } + }, + }) +} + +// ---- 设置 ---- +async function openSettings() { + showSettings.value = true + settingsLoading.value = true + try { + const config = await AppService.GetGitGlobalConfig() + gitConfig.value = { + userName: config?.userName || '', + userEmail: config?.userEmail || '', + } + } catch (e) { + console.error('获取 Git 配置失败:', e) + } finally { + settingsLoading.value = false + } +} + +async function saveSettings() { + settingsLoading.value = true + try { + await AppService.SetGitGlobalConfig(gitConfig.value.userName, gitConfig.value.userEmail) + message.success('设置已保存') + showSettings.value = false + } catch (e) { + Modal.error({ title: '保存设置失败', content: String(e) }) + } finally { + settingsLoading.value = false + } +} + // ---- 提交历史 ---- async function loadCommitLog() { if (!props.project?.path) return @@ -783,23 +1050,76 @@ watch(activeTab, (tab) => { @@ -822,6 +1142,9 @@ watch(activeTab, (tab) => { + + + @@ -864,6 +1187,53 @@ watch(activeTab, (tab) => { > Commit + + + + + + @@ -1189,6 +1559,29 @@ watch(activeTab, (tab) => { + + + + +
+ Git 全局配置 +
+ + + + + + +
+
@@ -1459,34 +1852,39 @@ watch(activeTab, (tab) => { background: var(--bg-surface, #252536); border: 1px solid var(--border-color, #313244); border-radius: 6px; - min-width: 200px; - max-height: 300px; + min-width: 260px; + max-height: 400px; overflow: hidden; box-shadow: 0 4px 16px rgba(0,0,0,0.4); } -.branch-dropdown-title { - padding: 8px 12px; - font-size: 12px; - font-weight: 600; - color: var(--text-secondary); +.branch-create-box { + display: flex; + gap: 6px; + padding: 8px 10px; border-bottom: 1px solid var(--border-color); +} + +.branch-dropdown-title { + padding: 8px 12px 4px; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.3px; } .branch-dropdown-list { - max-height: 250px; + max-height: 200px; overflow-y: auto; } .branch-dropdown-item { display: flex; align-items: center; - padding: 7px 12px; + padding: 0 4px 0 0; font-size: 13px; color: var(--text-primary); - cursor: pointer; transition: background 0.12s; } @@ -1499,6 +1897,159 @@ watch(activeTab, (tab) => { color: var(--success, #a6e3a1); } +.branch-item-main { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + padding: 7px 8px 7px 12px; + cursor: pointer; +} + +.branch-item-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +.branch-item-actions { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + visibility: hidden; +} + +.branch-dropdown-item:hover .branch-item-actions { + visibility: visible; +} + +.branch-action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 4px; + font-size: 12px; + color: var(--text-muted); + cursor: pointer; + transition: all 0.12s; +} + +.branch-action-btn:hover { + background: var(--bg-active, rgba(255,255,255,0.12)); + color: var(--accent, #89b4fa); +} + +.branch-action-btn.danger:hover { + background: rgba(243, 139, 168, 0.2); + color: var(--danger, #f38ba8); +} + +/* Stash 下拉 */ +.stash-dropdown { + background: var(--bg-surface, #252536); + border: 1px solid var(--border-color, #313244); + border-radius: 6px; + min-width: 300px; + max-height: 360px; + overflow: hidden; + box-shadow: 0 4px 16px rgba(0,0,0,0.4); +} + +.stash-save-section { + display: flex; + gap: 6px; + padding: 8px 10px; + border-bottom: 1px solid var(--border-color); +} + +.stash-list-title { + padding: 8px 12px 4px; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 4px; +} + +.stash-list { + max-height: 240px; + overflow-y: auto; +} + +.stash-item { + display: flex; + align-items: center; + padding: 6px 12px; + font-size: 12px; + border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.04)); + transition: background 0.12s; +} + +.stash-item:hover { + background: var(--bg-hover); +} + +.stash-item-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.stash-ref { + font-family: 'Consolas', 'Courier New', monospace; + color: var(--accent, #89b4fa); + font-size: 11px; +} + +.stash-msg { + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.stash-item-actions { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + visibility: hidden; +} + +.stash-item:hover .stash-item-actions { + visibility: visible; +} + +.stash-action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 4px; + font-size: 12px; + color: var(--text-muted); + cursor: pointer; + transition: all 0.12s; +} + +.stash-action-btn:hover { + background: var(--bg-active, rgba(255,255,255,0.12)); + color: var(--accent, #89b4fa); +} + +.stash-action-btn.danger:hover { + background: rgba(243, 139, 168, 0.2); + color: var(--danger, #f38ba8); +} + /* Tab 切换 */ .file-list-tabs { display: flex; diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/config/index.js b/frontend/bindings/github.com/zhuy1228/GitPilot/config/index.js new file mode 100644 index 0000000..471e641 --- /dev/null +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/config/index.js @@ -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"; diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/config/models.js b/frontend/bindings/github.com/zhuy1228/GitPilot/config/models.js new file mode 100644 index 0000000..6995763 --- /dev/null +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/config/models.js @@ -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} [$$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} */($$parsedSource)); + } +} diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js index cf0bd57..40b6ce8 100644 --- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js @@ -14,6 +14,9 @@ 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 @@ -52,6 +55,16 @@ export function AddUser(platform, username, token) { return $Call.ByID(2264426704, platform, username, token); } +/** + * CheckoutRemoteBranch 检出远程分支到本地 + * @param {string} path + * @param {string} remoteBranch + * @returns {$CancellablePromise} + */ +export function CheckoutRemoteBranch(path, remoteBranch) { + return $Call.ByID(3682237522, path, remoteBranch); +} + /** * CloneProject 克隆远程仓库到本地目录,并添加到项目树 * @param {string} platform @@ -75,6 +88,16 @@ export function CommitChanges(path, message) { return $Call.ByID(921984546, path, message); } +/** + * CreateBranch 创建新分支 + * @param {string} path + * @param {string} name + * @returns {$CancellablePromise} + */ +export function CreateBranch(path, name) { + return $Call.ByID(1422152924, path, name); +} + /** * CreateTag 创建标签 * @param {string} path @@ -86,6 +109,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} + */ +export function DeleteBranch(path, name, force) { + return $Call.ByID(580272035, path, name, force); +} + +/** + * DeleteRemoteBranch 删除远程分支 + * @param {string} path + * @param {string} branch + * @returns {$CancellablePromise} + */ +export function DeleteRemoteBranch(path, branch) { + return $Call.ByID(2626609049, path, branch); +} + /** * DeleteTag 删除标签(本地+远程) * @param {string} path @@ -115,6 +159,16 @@ export function FetchProject(path) { return $Call.ByID(3541106829, path); } +/** + * GetAppSettings 获取应用设置 + * @returns {$CancellablePromise} + */ +export function GetAppSettings() { + return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => { + return $$createType1($result); + })); +} + /** * GetBranches 获取所有本地分支 * @param {string} path @@ -122,7 +176,7 @@ export function FetchProject(path) { */ export function GetBranches(path) { return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => { - return $$createType1($result); + return $$createType3($result); })); } @@ -155,7 +209,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 $$createType5($result); })); } @@ -167,7 +221,7 @@ 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 $$createType7($result); })); } @@ -201,6 +255,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 $$createType9($result); + })); +} + /** * GetPlatformInfo 获取平台信息 * @param {string} name @@ -208,7 +272,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 $$createType11($result); })); } @@ -219,7 +283,7 @@ export function GetPlatformInfo(name) { */ export function GetProjectChangedFiles(path) { return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => { - return $$createType9($result); + return $$createType13($result); })); } @@ -230,7 +294,7 @@ export function GetProjectChangedFiles(path) { */ export function GetProjectStatus(path) { return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => { - return $$createType11($result); + return $$createType15($result); })); } @@ -240,7 +304,29 @@ export function GetProjectStatus(path) { */ export function GetProjectTree() { return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => { - return $$createType13($result); + return $$createType17($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 $$createType3($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 $$createType19($result); })); } @@ -251,7 +337,7 @@ export function GetProjectTree() { */ export function GetTags(path) { return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => { - return $$createType15($result); + return $$createType21($result); })); } @@ -263,10 +349,20 @@ 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 $$createType23($result); })); } +/** + * MergeBranch 合并指定分支到当前分支 + * @param {string} path + * @param {string} branch + * @returns {$CancellablePromise} + */ +export function MergeBranch(path, branch) { + return $Call.ByID(278161076, path, branch); +} + /** * PullProject 拉取项目(当前分支) * @param {string} path @@ -363,6 +459,16 @@ export function SetApplication(app) { return $Call.ByID(383695022, app); } +/** + * SetGitGlobalConfig 设置 git 全局配置 + * @param {string} name + * @param {string} email + * @returns {$CancellablePromise} + */ +export function SetGitGlobalConfig(name, email) { + return $Call.ByID(3875064981, name, email); +} + /** * StageAll 暂存所有变更文件 * @param {string} path @@ -382,6 +488,46 @@ export function StageFiles(path, files) { return $Call.ByID(4064506881, path, files); } +/** + * StashApply 应用贮藏(不删除) + * @param {string} path + * @param {number} index + * @returns {$CancellablePromise} + */ +export function StashApply(path, index) { + return $Call.ByID(721583959, path, index); +} + +/** + * StashDrop 删除贮藏 + * @param {string} path + * @param {number} index + * @returns {$CancellablePromise} + */ +export function StashDrop(path, index) { + return $Call.ByID(3821556536, path, index); +} + +/** + * StashPop 应用贮藏并删除 + * @param {string} path + * @param {number} index + * @returns {$CancellablePromise} + */ +export function StashPop(path, index) { + return $Call.ByID(4117964460, path, index); +} + +/** + * StashSave 保存当前变更到贮藏 + * @param {string} path + * @param {string} message + * @returns {$CancellablePromise} + */ +export function StashSave(path, message) { + return $Call.ByID(1178263140, path, message); +} + /** * SwitchBranch 切换分支 * @param {string} path @@ -411,6 +557,15 @@ export function UnstageFiles(path, files) { return $Call.ByID(3546473046, path, files); } +/** + * UpdateAppSettings 更新应用设置 + * @param {string} logLevel + * @returns {$CancellablePromise} + */ +export function UpdateAppSettings(logLevel) { + return $Call.ByID(718820989, logLevel); +} + /** * UpdatePlatform 修改平台信息(base_url) * @param {string} name @@ -434,21 +589,27 @@ export function UpdateUser(platform, oldUsername, newUsername, token) { } // Private type creation functions -const $$createType0 = $models.BranchInfo.createFrom; -const $$createType1 = $Create.Array($$createType0); -const $$createType2 = $models.CommitFileInfo.createFrom; +const $$createType0 = config$0.Settings.createFrom; +const $$createType1 = $Create.Nullable($$createType0); +const $$createType2 = $models.BranchInfo.createFrom; const $$createType3 = $Create.Array($$createType2); -const $$createType4 = $models.CommitLog.createFrom; +const $$createType4 = $models.CommitFileInfo.createFrom; const $$createType5 = $Create.Array($$createType4); -const $$createType6 = $models.PlatformInfo.createFrom; -const $$createType7 = $Create.Nullable($$createType6); -const $$createType8 = $models.FileInfo.createFrom; -const $$createType9 = $Create.Array($$createType8); -const $$createType10 = $models.ProjectStatus.createFrom; +const $$createType6 = $models.CommitLog.createFrom; +const $$createType7 = $Create.Array($$createType6); +const $$createType8 = $models.GitConfig.createFrom; +const $$createType9 = $Create.Nullable($$createType8); +const $$createType10 = $models.PlatformInfo.createFrom; const $$createType11 = $Create.Nullable($$createType10); -const $$createType12 = $models.TreeNode.createFrom; +const $$createType12 = $models.FileInfo.createFrom; const $$createType13 = $Create.Array($$createType12); -const $$createType14 = $models.TagInfo.createFrom; -const $$createType15 = $Create.Array($$createType14); -const $$createType16 = $models.UserInfo.createFrom; -const $$createType17 = $Create.Nullable($$createType16); +const $$createType14 = $models.ProjectStatus.createFrom; +const $$createType15 = $Create.Nullable($$createType14); +const $$createType16 = $models.TreeNode.createFrom; +const $$createType17 = $Create.Array($$createType16); +const $$createType18 = $models.StashInfo.createFrom; +const $$createType19 = $Create.Array($$createType18); +const $$createType20 = $models.TagInfo.createFrom; +const $$createType21 = $Create.Array($$createType20); +const $$createType22 = $models.UserInfo.createFrom; +const $$createType23 = $Create.Nullable($$createType22); diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js index 111b80a..baae10f 100644 --- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js @@ -12,8 +12,10 @@ export { CommitFileInfo, CommitLog, FileInfo, + GitConfig, PlatformInfo, ProjectStatus, + StashInfo, TagInfo, TreeNode, UserInfo diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js index 62667d0..2568e29 100644 --- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js @@ -207,6 +207,44 @@ export class FileInfo { } } +/** + * GitConfig Git 全局配置 + */ +export class GitConfig { + /** + * Creates a new GitConfig instance. + * @param {Partial} [$$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} */($$parsedSource)); + } +} + /** * PlatformInfo 平台信息 */ @@ -294,6 +332,58 @@ export class ProjectStatus { } } +/** + * StashInfo 贮藏信息 + */ +export class StashInfo { + /** + * Creates a new StashInfo instance. + * @param {Partial} [$$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} */($$parsedSource)); + } +} + /** * TagInfo 标签信息 */ diff --git a/internal/app/service.go b/internal/app/service.go index 5ef3c6f..b93b908 100644 --- a/internal/app/service.go +++ b/internal/app/service.go @@ -820,3 +820,232 @@ 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) +} diff --git a/internal/git/client.go b/internal/git/client.go index 9447dab..0a50bba 100644 --- a/internal/git/client.go +++ b/internal/git/client.go @@ -348,6 +348,101 @@ 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 +} + // StatusText 返回变更状态的中文描述 func (fc FileChange) StatusText() string { switch {