From 3ec453cc38e9b18706651a81af6e67d34bfa3466 Mon Sep 17 00:00:00 2001 From: zyj Date: Thu, 12 Mar 2026 10:57:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=20remote=20=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E6=94=AF=E6=8C=81=20(v0.3.1)\n\n=E5=90=8E?= =?UTF-8?q?=E7=AB=AF:\n-=20=E6=96=B0=E5=A2=9E=20RemoteList/AddRemote/Remov?= =?UTF-8?q?eRemote=20=E8=BF=9C=E7=A8=8B=E4=BB=93=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86\n-=20=E6=89=80=E6=9C=89=E8=BF=9C=E7=A8=8B=E6=93=8D?= =?UTF-8?q?=E4=BD=9C(Push/Pull/Fetch/PushTag/DeleteTag/DeleteRemoteBranch?= =?UTF-8?q?=E7=AD=89)=E6=94=AF=E6=8C=81=E6=8C=87=E5=AE=9A=20remote\n-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20PushTo/PullFrom/FetchRemote/PushTagTo/Dele?= =?UTF-8?q?teRemoteTagFrom=20=E7=AD=89=E6=96=B9=E6=B3=95\n-=20BatchPull/Ba?= =?UTF-8?q?tchPush=20=E6=94=AF=E6=8C=81=20remote=20=E5=8F=82=E6=95=B0\n\n?= =?UTF-8?q?=E5=89=8D=E7=AB=AF:\n-=20=E9=A1=B6=E9=83=A8=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E6=A0=8F=E6=96=B0=E5=A2=9E=20remote=20=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=E4=B8=8B=E6=8B=89=E8=8F=9C=E5=8D=95\n-=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=88=87=E6=8D=A2=E5=BD=93=E5=89=8D=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E7=9A=84=E8=BF=9C=E7=A8=8B=E4=BB=93=E5=BA=93\n-=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=B7=BB=E5=8A=A0/=E5=88=A0=E9=99=A4=E8=BF=9C?= =?UTF-8?q?=E7=A8=8B=E4=BB=93=E5=BA=93\n-=20Pull/Push/Fetch/=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E6=8E=A8=E9=80=81/=E8=BF=9C=E7=A8=8B=E5=88=86?= =?UTF-8?q?=E6=94=AF=E5=88=A0=E9=99=A4=E7=AD=89=E6=93=8D=E4=BD=9C=E5=9D=87?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E9=80=89=E4=B8=AD=E7=9A=84=20remote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/config.yml | 2 +- build/darwin/Info.plist | 4 +- build/windows/info.json | 4 +- frontend/app/components/ContentArea.vue | 190 +++++++++++++++++- frontend/app/components/Sidebar.vue | 4 +- .../GitPilot/internal/app/appservice.js | 99 ++++++--- .../zhuy1228/GitPilot/internal/app/index.js | 1 + .../zhuy1228/GitPilot/internal/app/models.js | 66 +++++- internal/app/service.go | 155 +++++++++++--- internal/git/client.go | 89 +++++++- 10 files changed, 536 insertions(+), 78 deletions(-) diff --git a/build/config.yml b/build/config.yml index 1490c3b..6ee37c4 100644 --- a/build/config.yml +++ b/build/config.yml @@ -9,7 +9,7 @@ info: description: "Git Repository Management Tool" copyright: "(c) 2025, GitPilot" comments: "GitPilot - Git Repository Manager" - version: "0.3.0" + version: "0.3.1" dev_mode: root_path: . diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index fe3ed90..29d014f 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -10,11 +10,11 @@ CFBundleIdentifier com.gitpilot.app CFBundleVersion - 0.3.0 + 0.3.1 CFBundleGetInfoString Git Repository Management Tool CFBundleShortVersionString - 0.3.0 + 0.3.1 CFBundleIconFile icons LSMinimumSystemVersion diff --git a/build/windows/info.json b/build/windows/info.json index 3d9697b..a8528dd 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -1,10 +1,10 @@ { "fixed": { - "file_version": "0.3.0" + "file_version": "0.3.1" }, "info": { "0000": { - "ProductVersion": "0.3.0", + "ProductVersion": "0.3.1", "CompanyName": "GitPilot", "FileDescription": "Git Repository Management Tool", "LegalCopyright": "© 2025, GitPilot", diff --git a/frontend/app/components/ContentArea.vue b/frontend/app/components/ContentArea.vue index e2d659b..7260aad 100644 --- a/frontend/app/components/ContentArea.vue +++ b/frontend/app/components/ContentArea.vue @@ -90,6 +90,15 @@ const newBranchName = ref('') const createBranchLoading = ref(false) const remoteBranches = ref([]) +// ---- Remote 远程仓库 ---- +const remotes = ref([]) +const currentRemote = ref('origin') +const showRemoteDropdown = ref(false) +const showAddRemote = ref(false) +const newRemoteName = ref('') +const newRemoteUrl = ref('') +const addRemoteLoading = ref(false) + // ---- Stash 贮藏管理 ---- const stashList = ref([]) const stashLoading = ref(false) @@ -248,8 +257,15 @@ async function loadStatus() { status.value = { branch: result.branch || '', remoteUrl: result.remoteUrl || result.remoteURL || '', + remotes: Array.isArray(result.remotes) ? result.remotes : [], + currentRemote: result.currentRemote || 'origin', changedFiles: [], } + // 同步 remotes + remotes.value = status.value.remotes + if (remotes.value.length && !remotes.value.find(r => r.name === currentRemote.value)) { + currentRemote.value = remotes.value[0].name + } } else { errorMsg.value = '获取项目状态返回空值: ' + JSON.stringify(result) loadingBase.value = false @@ -512,11 +528,11 @@ async function gitAction(action) { actionLoading.value = action try { if (action === 'pull') { - await AppService.PullProject(props.project.path) + await AppService.PullProject(props.project.path, currentRemote.value) } else if (action === 'push') { - await AppService.PushProject(props.project.path) + await AppService.PushProject(props.project.path, currentRemote.value) } else { - await AppService.FetchProject(props.project.path) + await AppService.FetchProject(props.project.path, currentRemote.value) } await loadStatus() } catch (e) { @@ -675,7 +691,7 @@ async function mergeBranch(branchName) { async function loadRemoteBranches() { if (!props.project?.path) return try { - const list = await AppService.GetRemoteBranches(props.project.path) + const list = await AppService.GetRemoteBranches(props.project.path, currentRemote.value) remoteBranches.value = Array.isArray(list) ? list : [] } catch (e) { console.error('获取远程分支失败:', e) @@ -709,7 +725,7 @@ async function deleteRemoteBranch(remoteBranch) { okButtonProps: { danger: true }, async onOk() { try { - await AppService.DeleteRemoteBranch(props.project.path, remoteBranch) + await AppService.DeleteRemoteBranch(props.project.path, remoteBranch, currentRemote.value) await loadRemoteBranches() message.success(`远程分支 ${remoteBranch} 已删除`) } catch (e) { @@ -1130,7 +1146,7 @@ async function deleteTag(tag) { okButtonProps: { danger: true }, async onOk() { try { - await AppService.DeleteTag(props.project.path, tag.name) + await AppService.DeleteTag(props.project.path, tag.name, currentRemote.value) await loadTags() } catch (e) { console.error('删除标签失败:', e) @@ -1143,7 +1159,7 @@ async function deleteTag(tag) { async function pushTag(tag) { if (!props.project?.path) return try { - await AppService.PushTag(props.project.path, tag.name) + await AppService.PushTag(props.project.path, tag.name, currentRemote.value) Modal.success({ title: '推送成功', content: `标签 ${tag.name} 已推送到远程` }) } catch (e) { console.error('推送标签失败:', e) @@ -1157,6 +1173,63 @@ watch(activeTab, (tab) => { loadTags() } }) + +// ---- Remote 远程仓库管理 ---- +async function switchRemote(remoteName) { + currentRemote.value = remoteName + showRemoteDropdown.value = false + // 切换 remote 后刷新远程分支 + await loadRemoteBranches() +} + +async function addRemote() { + if (!props.project?.path || !newRemoteName.value.trim() || !newRemoteUrl.value.trim()) return + addRemoteLoading.value = true + try { + await AppService.AddRemote(props.project.path, newRemoteName.value.trim(), newRemoteUrl.value.trim()) + newRemoteName.value = '' + newRemoteUrl.value = '' + showAddRemote.value = false + message.success('远程仓库添加成功') + await loadStatus() + } catch (e) { + Modal.error({ title: '添加远程仓库失败', content: String(e) }) + } finally { + addRemoteLoading.value = false + } +} + +async function removeRemote(remoteName) { + 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;' }, remoteName), + ]), + okText: '删除', + cancelText: '取消', + okButtonProps: { danger: true }, + async onOk() { + try { + await AppService.RemoveRemote(props.project.path, remoteName) + message.success(`远程仓库 ${remoteName} 已删除`) + if (currentRemote.value === remoteName) { + currentRemote.value = 'origin' + } + await loadStatus() + } catch (e) { + Modal.error({ title: '删除远程仓库失败', content: String(e) }) + } + }, + }) +} + +const currentRemoteUrl = computed(() => { + const r = remotes.value.find(r => r.name === currentRemote.value) + return r ? r.url : status.value?.remoteUrl || '' +}) - {{ status.remoteUrl }} + + + + + {{ currentRemote }} + + + + + {{ currentRemoteUrl }} + @@ -1898,6 +2022,56 @@ watch(activeTab, (tab) => { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.remote-selector { + display: inline-flex; + align-items: center; + cursor: pointer; + padding: 1px 6px; + border-radius: 4px; + color: var(--accent, #89b4fa); + font-weight: 500; + transition: background 0.2s; +} +.remote-selector:hover { + background: var(--hover-bg, rgba(137, 180, 250, 0.1)); +} + +.remote-url-text { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.remote-dropdown { + background: var(--dropdown-bg, #1e1e2e); + border: 1px solid var(--border-color, #313244); + border-radius: 8px; + padding: 8px 0; + min-width: 280px; + max-width: 400px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); +} +.remote-add-btn { + padding: 8px 12px; + color: var(--accent, #89b4fa); + cursor: pointer; + font-size: 12px; + border-top: 1px solid var(--border-color, #313244); + margin-top: 4px; +} +.remote-add-btn:hover { + background: var(--hover-bg, rgba(137, 180, 250, 0.08)); +} +.remote-add-form { + padding: 8px 12px; + border-top: 1px solid var(--border-color, #313244); + margin-top: 4px; } .project-actions { diff --git a/frontend/app/components/Sidebar.vue b/frontend/app/components/Sidebar.vue index ff1ceb6..02f384d 100644 --- a/frontend/app/components/Sidebar.vue +++ b/frontend/app/components/Sidebar.vue @@ -457,7 +457,7 @@ async function doBatchPull() { if (!selectedBatchPaths.value.length) return batchActionLoading.value = 'pull' try { - const results = await AppService.BatchPull(selectedBatchPaths.value) + const results = await AppService.BatchPull(selectedBatchPaths.value, 'origin') batchResults.value = Array.isArray(results) ? results : [] const successCount = batchResults.value.filter(r => r.success).length message.info(`批量 Pull 完成:${successCount}/${batchResults.value.length} 成功`) @@ -473,7 +473,7 @@ async function doBatchPush() { if (!selectedBatchPaths.value.length) return batchActionLoading.value = 'push' try { - const results = await AppService.BatchPush(selectedBatchPaths.value) + const results = await AppService.BatchPush(selectedBatchPaths.value, 'origin') batchResults.value = Array.isArray(results) ? results : [] const successCount = batchResults.value.filter(r => r.success).length message.info(`批量 Push 完成:${successCount}/${batchResults.value.length} 成功`) 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 e13d36d..beae97e 100644 --- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js @@ -53,6 +53,17 @@ export function AddProject(platform, username, name, path) { return $Call.ByID(2299402672, platform, username, name, path); } +/** + * AddRemote 添加远程仓库 + * @param {string} path + * @param {string} name + * @param {string} url + * @returns {$CancellablePromise} + */ +export function AddRemote(path, name, url) { + return $Call.ByID(3697916441, path, name, url); +} + /** * AddUser 添加用户到指定平台 * @param {string} platform @@ -67,10 +78,11 @@ export function AddUser(platform, username, token) { /** * BatchPull 批量拉取指定项目 * @param {string[]} paths + * @param {string} remote * @returns {$CancellablePromise<$models.BatchPullResult[]>} */ -export function BatchPull(paths) { - return $Call.ByID(758996647, paths).then(/** @type {($result: any) => any} */(($result) => { +export function BatchPull(paths, remote) { + return $Call.ByID(758996647, paths, remote).then(/** @type {($result: any) => any} */(($result) => { return $$createType1($result); })); } @@ -78,10 +90,11 @@ export function BatchPull(paths) { /** * BatchPush 批量推送指定项目 * @param {string[]} paths + * @param {string} remote * @returns {$CancellablePromise<$models.BatchPullResult[]>} */ -export function BatchPush(paths) { - return $Call.ByID(794082076, paths).then(/** @type {($result: any) => any} */(($result) => { +export function BatchPush(paths, remote) { + return $Call.ByID(794082076, paths, remote).then(/** @type {($result: any) => any} */(($result) => { return $$createType1($result); })); } @@ -165,20 +178,22 @@ export function DeleteBranch(path, name, force) { * DeleteRemoteBranch 删除远程分支 * @param {string} path * @param {string} branch + * @param {string} remote * @returns {$CancellablePromise} */ -export function DeleteRemoteBranch(path, branch) { - return $Call.ByID(2626609049, path, branch); +export function DeleteRemoteBranch(path, branch, remote) { + return $Call.ByID(2626609049, path, branch, remote); } /** * DeleteTag 删除标签(本地+远程) * @param {string} path * @param {string} name + * @param {string} remote * @returns {$CancellablePromise} */ -export function DeleteTag(path, name) { - return $Call.ByID(873653241, path, name); +export function DeleteTag(path, name, remote) { + return $Call.ByID(873653241, path, name, remote); } /** @@ -192,12 +207,13 @@ export function DiscardFiles(path, files) { } /** - * FetchProject 拉取远程信息 + * FetchProject 拉取远程信息(指定 remote,空则 fetch --all) * @param {string} path + * @param {string} remote * @returns {$CancellablePromise} */ -export function FetchProject(path) { - return $Call.ByID(3541106829, path); +export function FetchProject(path, remote) { + return $Call.ByID(3541106829, path, remote); } /** @@ -383,14 +399,26 @@ export function GetProjectTree() { /** * GetRemoteBranches 获取远程分支列表 * @param {string} path + * @param {string} remote * @returns {$CancellablePromise<$models.BranchInfo[]>} */ -export function GetRemoteBranches(path) { - return $Call.ByID(994796370, path).then(/** @type {($result: any) => any} */(($result) => { +export function GetRemoteBranches(path, remote) { + return $Call.ByID(994796370, path, remote).then(/** @type {($result: any) => any} */(($result) => { return $$createType8($result); })); } +/** + * GetRemotes 获取项目所有远程仓库列表 + * @param {string} path + * @returns {$CancellablePromise<$models.RemoteItem[]>} + */ +export function GetRemotes(path) { + return $Call.ByID(975520027, path).then(/** @type {($result: any) => any} */(($result) => { + return $$createType26($result); + })); +} + /** * GetStashList 获取贮藏列表 * @param {string} path @@ -398,7 +426,7 @@ export function GetRemoteBranches(path) { */ export function GetStashList(path) { return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => { - return $$createType26($result); + return $$createType28($result); })); } @@ -409,7 +437,7 @@ export function GetStashList(path) { */ export function GetTags(path) { return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => { - return $$createType28($result); + return $$createType30($result); })); } @@ -421,7 +449,7 @@ export function GetTags(path) { */ export function GetUserInfo(platform, username) { return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => { - return $$createType30($result); + return $$createType32($result); })); } @@ -455,31 +483,34 @@ export function MergeBranch(path, branch) { } /** - * PullProject 拉取项目(当前分支) + * PullProject 拉取项目(当前分支,指定 remote) * @param {string} path + * @param {string} remote * @returns {$CancellablePromise} */ -export function PullProject(path) { - return $Call.ByID(4145813996, path); +export function PullProject(path, remote) { + return $Call.ByID(4145813996, path, remote); } /** - * PushProject 推送项目(当前分支) + * PushProject 推送项目(当前分支,指定 remote) * @param {string} path + * @param {string} remote * @returns {$CancellablePromise} */ -export function PushProject(path) { - return $Call.ByID(3887901113, path); +export function PushProject(path, remote) { + return $Call.ByID(3887901113, path, remote); } /** * PushTag 推送标签到远程 * @param {string} path * @param {string} name + * @param {string} remote * @returns {$CancellablePromise} */ -export function PushTag(path, name) { - return $Call.ByID(1424810524, path, name); +export function PushTag(path, name, remote) { + return $Call.ByID(1424810524, path, name, remote); } /** @@ -502,6 +533,16 @@ export function RemoveProject(platform, username, name) { return $Call.ByID(2748109581, platform, username, name); } +/** + * RemoveRemote 删除远程仓库 + * @param {string} path + * @param {string} name + * @returns {$CancellablePromise} + */ +export function RemoveRemote(path, name) { + return $Call.ByID(2915623022, path, name); +} + /** * RemoveUser 从平台删除用户 * @param {string} platform @@ -748,9 +789,11 @@ const $$createType21 = $models.ProjectStatus.createFrom; const $$createType22 = $Create.Nullable($$createType21); const $$createType23 = $models.TreeNode.createFrom; const $$createType24 = $Create.Array($$createType23); -const $$createType25 = $models.StashInfo.createFrom; +const $$createType25 = $models.RemoteItem.createFrom; const $$createType26 = $Create.Array($$createType25); -const $$createType27 = $models.TagInfo.createFrom; +const $$createType27 = $models.StashInfo.createFrom; const $$createType28 = $Create.Array($$createType27); -const $$createType29 = $models.UserInfo.createFrom; -const $$createType30 = $Create.Nullable($$createType29); +const $$createType29 = $models.TagInfo.createFrom; +const $$createType30 = $Create.Array($$createType29); +const $$createType31 = $models.UserInfo.createFrom; +const $$createType32 = $Create.Nullable($$createType31); 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 72f781d..5e6d162 100644 --- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js @@ -19,6 +19,7 @@ export { PlatformInfo, ProjectOverview, ProjectStatus, + RemoteItem, StashInfo, TagInfo, TreeNode, 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 a20406d..c1e18bb 100644 --- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js @@ -507,6 +507,20 @@ export class ProjectStatus { */ this["remoteUrl"] = ""; } + if (!("remotes" in $$source)) { + /** + * @member + * @type {RemoteItem[]} + */ + this["remotes"] = []; + } + if (!("currentRemote" in $$source)) { + /** + * @member + * @type {string} + */ + this["currentRemote"] = ""; + } if (!("changedFiles" in $$source)) { /** * @member @@ -525,14 +539,56 @@ export class ProjectStatus { */ static createFrom($$source = {}) { const $$createField2_0 = $$createType1; + const $$createField4_0 = $$createType3; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("remotes" in $$parsedSource) { + $$parsedSource["remotes"] = $$createField2_0($$parsedSource["remotes"]); + } if ("changedFiles" in $$parsedSource) { - $$parsedSource["changedFiles"] = $$createField2_0($$parsedSource["changedFiles"]); + $$parsedSource["changedFiles"] = $$createField4_0($$parsedSource["changedFiles"]); } return new ProjectStatus(/** @type {Partial} */($$parsedSource)); } } +/** + * RemoteItem 远程仓库信息 + */ +export class RemoteItem { + /** + * Creates a new RemoteItem instance. + * @param {Partial} [$$source = {}] - The source object to create the RemoteItem. + */ + constructor($$source = {}) { + if (!("name" in $$source)) { + /** + * @member + * @type {string} + */ + this["name"] = ""; + } + if (!("url" in $$source)) { + /** + * @member + * @type {string} + */ + this["url"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new RemoteItem instance from a string or object. + * @param {any} [$$source = {}] + * @returns {RemoteItem} + */ + static createFrom($$source = {}) { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new RemoteItem(/** @type {Partial} */($$parsedSource)); + } +} + /** * StashInfo 贮藏信息 */ @@ -692,7 +748,7 @@ export class TreeNode { * @returns {TreeNode} */ static createFrom($$source = {}) { - const $$createField4_0 = $$createType3; + const $$createField4_0 = $$createType5; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("children" in $$parsedSource) { $$parsedSource["children"] = $$createField4_0($$parsedSource["children"]); @@ -740,7 +796,9 @@ export class UserInfo { } // Private type creation functions -const $$createType0 = FileInfo.createFrom; +const $$createType0 = RemoteItem.createFrom; const $$createType1 = $Create.Array($$createType0); -const $$createType2 = TreeNode.createFrom; +const $$createType2 = FileInfo.createFrom; const $$createType3 = $Create.Array($$createType2); +const $$createType4 = TreeNode.createFrom; +const $$createType5 = $Create.Array($$createType4); diff --git a/internal/app/service.go b/internal/app/service.go index 02380e4..4fd5e5f 100644 --- a/internal/app/service.go +++ b/internal/app/service.go @@ -307,9 +307,17 @@ func (s *AppService) GetPlatformInfo(name string) (*PlatformInfo, error) { // ProjectStatus 项目状态信息 type ProjectStatus struct { - Branch string `json:"branch"` - RemoteURL string `json:"remoteUrl"` - ChangedFiles []FileInfo `json:"changedFiles"` + Branch string `json:"branch"` + RemoteURL string `json:"remoteUrl"` + Remotes []RemoteItem `json:"remotes"` + CurrentRemote string `json:"currentRemote"` + ChangedFiles []FileInfo `json:"changedFiles"` +} + +// RemoteItem 远程仓库信息 +type RemoteItem struct { + Name string `json:"name"` + URL string `json:"url"` } // FileInfo 文件信息 @@ -346,10 +354,25 @@ func (s *AppService) GetProjectStatus(path string) (*ProjectStatus, error) { remoteURL, _ := s.gitClient.RemoteURL(path) + // 获取所有 remote 列表 + var remotes []RemoteItem + remoteList, remoteErr := s.gitClient.RemoteList(path) + if remoteErr == nil { + for _, r := range remoteList { + remotes = append(remotes, RemoteItem{Name: r.Name, URL: r.URL}) + } + } + currentRemote := "origin" + if len(remotes) > 0 { + currentRemote = remotes[0].Name + } + return &ProjectStatus{ - Branch: strings.TrimSpace(branch), - RemoteURL: remoteURL, - ChangedFiles: []FileInfo{}, + Branch: strings.TrimSpace(branch), + RemoteURL: remoteURL, + Remotes: remotes, + CurrentRemote: currentRemote, + ChangedFiles: []FileInfo{}, }, nil } @@ -515,22 +538,28 @@ func (s *AppService) GetFileDiffStaged(projectPath, filePath string) (string, er return s.gitClient.DiffStagedFile(projectPath, filePath) } -// PullProject 拉取项目(当前分支) -func (s *AppService) PullProject(path string) (string, error) { +// PullProject 拉取项目(当前分支,指定 remote) +func (s *AppService) PullProject(path, remote string) (string, error) { + if remote == "" { + remote = "origin" + } branch, err := s.gitClient.Branch(path) if err != nil { return "", fmt.Errorf("获取当前分支失败: %w", err) } - return s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch)) + return s.gitClient.PullFrom(path, remote, strings.TrimSpace(branch)) } -// PushProject 推送项目(当前分支) -func (s *AppService) PushProject(path string) (string, error) { +// PushProject 推送项目(当前分支,指定 remote) +func (s *AppService) PushProject(path, remote string) (string, error) { + if remote == "" { + remote = "origin" + } branch, err := s.gitClient.Branch(path) if err != nil { return "", fmt.Errorf("获取当前分支失败: %w", err) } - return s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch)) + return s.gitClient.PushTo(path, remote, strings.TrimSpace(branch)) } // GetCommitDiff 获取指定提交的 diff @@ -581,9 +610,12 @@ func (s *AppService) GetCommitFileDiff(path, hash, filePath string) (string, err return s.gitClient.CommitFileDiff(path, hash, filePath) } -// FetchProject 拉取远程信息 -func (s *AppService) FetchProject(path string) (string, error) { - return s.gitClient.Fetch(path) +// FetchProject 拉取远程信息(指定 remote,空则 fetch --all) +func (s *AppService) FetchProject(path, remote string) (string, error) { + if remote == "" { + return s.gitClient.Fetch(path) + } + return s.gitClient.FetchRemote(path, remote) } // CommitLog 提交记录 @@ -791,7 +823,7 @@ func (s *AppService) CreateTag(path, name, message string) error { } // DeleteTag 删除标签(本地+远程) -func (s *AppService) DeleteTag(path, name string) error { +func (s *AppService) DeleteTag(path, name, remote string) error { if _, err := os.Stat(path); os.IsNotExist(err) { return fmt.Errorf("项目路径不存在: %s", path) } @@ -799,17 +831,20 @@ func (s *AppService) DeleteTag(path, name string) error { if name == "" { return fmt.Errorf("标签名不能为空") } + if remote == "" { + remote = "origin" + } // 删除本地标签 if _, err := s.gitClient.DeleteTag(path, name); err != nil { return fmt.Errorf("删除本地标签失败: %w", err) } // 尝试删除远程标签(忽略错误,可能未推送过) - s.gitClient.DeleteRemoteTag(path, name) + s.gitClient.DeleteRemoteTagFrom(path, remote, name) return nil } // PushTag 推送标签到远程 -func (s *AppService) PushTag(path, name string) error { +func (s *AppService) PushTag(path, name, remote string) error { if _, err := os.Stat(path); os.IsNotExist(err) { return fmt.Errorf("项目路径不存在: %s", path) } @@ -817,7 +852,10 @@ func (s *AppService) PushTag(path, name string) error { if name == "" { return fmt.Errorf("标签名不能为空") } - _, err := s.gitClient.PushTag(path, name) + if remote == "" { + remote = "origin" + } + _, err := s.gitClient.PushTagTo(path, remote, name) return err } @@ -869,11 +907,17 @@ func (s *AppService) MergeBranch(path, branch string) (string, error) { // --- 远程分支管理 --- // GetRemoteBranches 获取远程分支列表 -func (s *AppService) GetRemoteBranches(path string) ([]BranchInfo, error) { +func (s *AppService) GetRemoteBranches(path, remote string) ([]BranchInfo, error) { if _, err := os.Stat(path); os.IsNotExist(err) { return nil, fmt.Errorf("项目路径不存在: %s", path) } - out, err := s.gitClient.RemoteBranchList(path) + var out string + var err error + if remote == "" { + out, err = s.gitClient.RemoteBranchList(path) + } else { + out, err = s.gitClient.RemoteBranchListByRemote(path, remote) + } if err != nil { return nil, fmt.Errorf("获取远程分支列表失败: %w", err) } @@ -907,7 +951,7 @@ func (s *AppService) CheckoutRemoteBranch(path, remoteBranch string) error { } // DeleteRemoteBranch 删除远程分支 -func (s *AppService) DeleteRemoteBranch(path, branch string) error { +func (s *AppService) DeleteRemoteBranch(path, branch, remote string) error { if _, err := os.Stat(path); os.IsNotExist(err) { return fmt.Errorf("项目路径不存在: %s", path) } @@ -915,12 +959,15 @@ func (s *AppService) DeleteRemoteBranch(path, branch string) error { if branch == "" { return fmt.Errorf("分支名不能为空") } + if remote == "" { + remote = "origin" + } // origin/feature -> feature localName := branch if idx := strings.Index(branch, "/"); idx != -1 { localName = branch[idx+1:] } - _, err := s.gitClient.DeleteRemoteBranch(path, localName) + _, err := s.gitClient.DeleteRemoteBranchFrom(path, remote, localName) return err } @@ -1050,6 +1097,54 @@ func (s *AppService) UpdateAppSettings(logLevel string) error { return config.SaveConfig(s.config) } +// --- 远程仓库管理 --- + +// GetRemotes 获取项目所有远程仓库列表 +func (s *AppService) GetRemotes(path string) ([]RemoteItem, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, fmt.Errorf("项目路径不存在: %s", path) + } + remoteList, err := s.gitClient.RemoteList(path) + if err != nil { + return nil, fmt.Errorf("获取远程仓库列表失败: %w", err) + } + var remotes []RemoteItem + for _, r := range remoteList { + remotes = append(remotes, RemoteItem{Name: r.Name, URL: r.URL}) + } + return remotes, nil +} + +// AddRemote 添加远程仓库 +func (s *AppService) AddRemote(path, name, url string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return fmt.Errorf("项目路径不存在: %s", path) + } + name = strings.TrimSpace(name) + url = strings.TrimSpace(url) + if name == "" { + return fmt.Errorf("远程名称不能为空") + } + if url == "" { + return fmt.Errorf("远程地址不能为空") + } + _, err := s.gitClient.AddRemote(path, name, url) + return err +} + +// RemoveRemote 删除远程仓库 +func (s *AppService) RemoveRemote(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.RemoveRemote(path, name) + return err +} + // --- 冲突处理 --- // ConflictFileInfo 冲突文件信息 @@ -1211,7 +1306,10 @@ type BatchPullResult struct { } // BatchPull 批量拉取指定项目 -func (s *AppService) BatchPull(paths []string) []BatchPullResult { +func (s *AppService) BatchPull(paths []string, remote string) []BatchPullResult { + if remote == "" { + remote = "origin" + } var results []BatchPullResult for _, path := range paths { result := BatchPullResult{Path: path} @@ -1229,7 +1327,7 @@ func (s *AppService) BatchPull(paths []string) []BatchPullResult { results = append(results, result) continue } - _, err = s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch)) + _, err = s.gitClient.PullFrom(path, remote, strings.TrimSpace(branch)) if err != nil { result.Message = err.Error() } else { @@ -1242,7 +1340,10 @@ func (s *AppService) BatchPull(paths []string) []BatchPullResult { } // BatchPush 批量推送指定项目 -func (s *AppService) BatchPush(paths []string) []BatchPullResult { +func (s *AppService) BatchPush(paths []string, remote string) []BatchPullResult { + if remote == "" { + remote = "origin" + } var results []BatchPullResult for _, path := range paths { result := BatchPullResult{Path: path} @@ -1259,7 +1360,7 @@ func (s *AppService) BatchPush(paths []string) []BatchPullResult { results = append(results, result) continue } - _, err = s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch)) + _, err = s.gitClient.PushTo(path, remote, strings.TrimSpace(branch)) if err != nil { result.Message = err.Error() } else { diff --git a/internal/git/client.go b/internal/git/client.go index 9ccc3be..a445b76 100644 --- a/internal/git/client.go +++ b/internal/git/client.go @@ -60,6 +60,21 @@ func (g *GitClient) Push(path string) (string, error) { return g.Run(path, "push") } +// PushTo 推送到指定远程 +func (g *GitClient) PushTo(path, remote, branch string) (string, error) { + return g.Run(path, "push", remote, branch) +} + +// PullFrom 从指定远程拉取 +func (g *GitClient) PullFrom(path, remote, branch string) (string, error) { + return g.Run(path, "pull", remote, branch) +} + +// FetchRemote 拉取指定远程信息 +func (g *GitClient) FetchRemote(path, remote string) (string, error) { + return g.Run(path, "fetch", remote) +} + func (g *GitClient) Status(path string) (string, error) { return g.Run(path, "status") } @@ -93,6 +108,47 @@ func (g *GitClient) Fetch(path string) (string, error) { return g.Run(path, "fetch", "--all") } +// RemoteList 获取所有远程仓库列表 (git remote -v) +type RemoteInfo struct { + Name string + URL string +} + +func (g *GitClient) RemoteList(path string) ([]RemoteInfo, error) { + out, err := g.Run(path, "remote", "-v") + if err != nil { + return nil, err + } + seen := make(map[string]bool) + var remotes []RemoteInfo + for _, line := range strings.Split(strings.TrimSpace(out), "\n") { + if line == "" { + continue + } + parts := strings.Fields(line) + if len(parts) < 2 { + continue + } + name := parts[0] + if seen[name] { + continue + } + seen[name] = true + remotes = append(remotes, RemoteInfo{Name: name, URL: parts[1]}) + } + return remotes, nil +} + +// AddRemote 添加远程仓库 +func (g *GitClient) AddRemote(path, name, url string) (string, error) { + return g.Run(path, "remote", "add", name, url) +} + +// RemoveRemote 删除远程仓库 +func (g *GitClient) RemoveRemote(path, name string) (string, error) { + return g.Run(path, "remote", "remove", name) +} + func (g *GitClient) RemoteURL(path string) (string, error) { out, err := g.Run(path, "config", "--get", "remote.origin.url") if err != nil { @@ -141,11 +197,16 @@ func (g *GitClient) Log(path string, count int) (string, error) { ) } -// UnpushedCommits 获取当前分支上未推送到远程的提交哈希列表 +// UnpushedCommits 获取当前分支上未推送到远程的提交哈希列表(默认 origin) func (g *GitClient) UnpushedCommits(path, branch string) (string, error) { return g.Run(path, "rev-list", "origin/"+branch+"..HEAD") } +// UnpushedCommitsTo 获取当前分支上未推送到指定远程的提交哈希列表 +func (g *GitClient) UnpushedCommitsTo(path, remote, branch string) (string, error) { + return g.Run(path, "rev-list", remote+"/"+branch+"..HEAD") +} + // RevertCommit 撤回指定提交(创建一个反向提交) func (g *GitClient) RevertCommit(path, hash string) (string, error) { return g.Run(path, "revert", "--no-edit", hash) @@ -167,16 +228,26 @@ func (g *GitClient) DeleteTag(path, name string) (string, error) { return g.Run(path, "tag", "-d", name) } -// PushTag 推送标签到远程 +// PushTag 推送标签到远程(默认 origin) func (g *GitClient) PushTag(path, name string) (string, error) { return g.Run(path, "push", "origin", name) } -// DeleteRemoteTag 删除远程标签 +// PushTagTo 推送标签到指定远程 +func (g *GitClient) PushTagTo(path, remote, name string) (string, error) { + return g.Run(path, "push", remote, name) +} + +// DeleteRemoteTag 删除远程标签(默认 origin) func (g *GitClient) DeleteRemoteTag(path, name string) (string, error) { return g.Run(path, "push", "origin", "--delete", name) } +// DeleteRemoteTagFrom 删除指定远程的标签 +func (g *GitClient) DeleteRemoteTagFrom(path, remote, name string) (string, error) { + return g.Run(path, "push", remote, "--delete", name) +} + // BranchList 获取所有本地分支 func (g *GitClient) BranchList(path string) (string, error) { return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)") @@ -368,16 +439,26 @@ func (g *GitClient) MergeBranch(path, branch string) (string, error) { return g.Run(path, "merge", branch) } -// DeleteRemoteBranch 删除远程分支 +// DeleteRemoteBranch 删除远程分支(默认 origin) func (g *GitClient) DeleteRemoteBranch(path, branch string) (string, error) { return g.Run(path, "push", "origin", "--delete", branch) } +// DeleteRemoteBranchFrom 删除指定远程的分支 +func (g *GitClient) DeleteRemoteBranchFrom(path, remote, branch string) (string, error) { + return g.Run(path, "push", remote, "--delete", branch) +} + // RemoteBranchList 获取所有远程分支 func (g *GitClient) RemoteBranchList(path string) (string, error) { return g.Run(path, "branch", "-r", "--format=%(refname:short)") } +// RemoteBranchListByRemote 获取指定远程的分支 +func (g *GitClient) RemoteBranchListByRemote(path, remote string) (string, error) { + return g.Run(path, "branch", "-r", "--list", remote+"/*", "--format=%(refname:short)") +} + // CheckoutNewBranch 从远程分支检出新本地分支 func (g *GitClient) CheckoutNewBranch(path, localBranch, remoteBranch string) (string, error) { return g.Run(path, "checkout", "-b", localBranch, remoteBranch)