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 || ''
+})
@@ -1271,7 +1344,58 @@ watch(activeTab, (tab) => {
- {{ status.remoteUrl }}
+
+ showRemoteDropdown = v" :trigger="['click']">
+
+
+ {{ currentRemote }}
+
+
+
+
+
远程仓库
+
+
+
+
+ {{ r.name }}
+ ✓
+
+
+
+
+ 无远程仓库
+
+
+
+
+
+
+
+
+ {{ 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)