4 Commits

15 changed files with 1297 additions and 116 deletions

View File

@@ -9,7 +9,7 @@ info:
description: "Git Repository Management Tool" description: "Git Repository Management Tool"
copyright: "(c) 2025, GitPilot" copyright: "(c) 2025, GitPilot"
comments: "GitPilot - Git Repository Manager" comments: "GitPilot - Git Repository Manager"
version: "0.1.0" version: "0.3.1"
dev_mode: dev_mode:
root_path: . root_path: .

View File

@@ -10,11 +10,11 @@
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.gitpilot.app</string> <string>com.gitpilot.app</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.2.0</string> <string>0.3.1</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Git Repository Management Tool</string> <string>Git Repository Management Tool</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.2.0</string> <string>0.3.1</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>icons</string> <string>icons</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

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

View File

@@ -90,6 +90,15 @@ const newBranchName = ref('')
const createBranchLoading = ref(false) const createBranchLoading = ref(false)
const remoteBranches = ref([]) 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 贮藏管理 ---- // ---- Stash 贮藏管理 ----
const stashList = ref([]) const stashList = ref([])
const stashLoading = ref(false) const stashLoading = ref(false)
@@ -248,8 +257,15 @@ async function loadStatus() {
status.value = { status.value = {
branch: result.branch || '', branch: result.branch || '',
remoteUrl: result.remoteUrl || result.remoteURL || '', remoteUrl: result.remoteUrl || result.remoteURL || '',
remotes: Array.isArray(result.remotes) ? result.remotes : [],
currentRemote: result.currentRemote || 'origin',
changedFiles: [], 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 { } else {
errorMsg.value = '获取项目状态返回空值: ' + JSON.stringify(result) errorMsg.value = '获取项目状态返回空值: ' + JSON.stringify(result)
loadingBase.value = false loadingBase.value = false
@@ -512,11 +528,11 @@ async function gitAction(action) {
actionLoading.value = action actionLoading.value = action
try { try {
if (action === 'pull') { if (action === 'pull') {
await AppService.PullProject(props.project.path) await AppService.PullProject(props.project.path, currentRemote.value)
} else if (action === 'push') { } else if (action === 'push') {
await AppService.PushProject(props.project.path) await AppService.PushProject(props.project.path, currentRemote.value)
} else { } else {
await AppService.FetchProject(props.project.path) await AppService.FetchProject(props.project.path, currentRemote.value)
} }
await loadStatus() await loadStatus()
} catch (e) { } catch (e) {
@@ -675,7 +691,7 @@ async function mergeBranch(branchName) {
async function loadRemoteBranches() { async function loadRemoteBranches() {
if (!props.project?.path) return if (!props.project?.path) return
try { 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 : [] remoteBranches.value = Array.isArray(list) ? list : []
} catch (e) { } catch (e) {
console.error('获取远程分支失败:', e) console.error('获取远程分支失败:', e)
@@ -709,7 +725,7 @@ async function deleteRemoteBranch(remoteBranch) {
okButtonProps: { danger: true }, okButtonProps: { danger: true },
async onOk() { async onOk() {
try { try {
await AppService.DeleteRemoteBranch(props.project.path, remoteBranch) await AppService.DeleteRemoteBranch(props.project.path, remoteBranch, currentRemote.value)
await loadRemoteBranches() await loadRemoteBranches()
message.success(`远程分支 ${remoteBranch} 已删除`) message.success(`远程分支 ${remoteBranch} 已删除`)
} catch (e) { } catch (e) {
@@ -1130,7 +1146,7 @@ async function deleteTag(tag) {
okButtonProps: { danger: true }, okButtonProps: { danger: true },
async onOk() { async onOk() {
try { try {
await AppService.DeleteTag(props.project.path, tag.name) await AppService.DeleteTag(props.project.path, tag.name, currentRemote.value)
await loadTags() await loadTags()
} catch (e) { } catch (e) {
console.error('删除标签失败:', e) console.error('删除标签失败:', e)
@@ -1143,7 +1159,7 @@ async function deleteTag(tag) {
async function pushTag(tag) { async function pushTag(tag) {
if (!props.project?.path) return if (!props.project?.path) return
try { 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} 已推送到远程` }) Modal.success({ title: '推送成功', content: `标签 ${tag.name} 已推送到远程` })
} catch (e) { } catch (e) {
console.error('推送标签失败:', e) console.error('推送标签失败:', e)
@@ -1157,6 +1173,63 @@ watch(activeTab, (tab) => {
loadTags() 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 || ''
})
</script> </script>
<template> <template>
@@ -1271,7 +1344,58 @@ watch(activeTab, (tab) => {
</div> </div>
</template> </template>
</a-dropdown> </a-dropdown>
<span v-if="status.remoteUrl" class="remote-url" :title="status.remoteUrl">{{ status.remoteUrl }}</span> <span v-if="status.remoteUrl" class="remote-url" :title="currentRemoteUrl">
<a-dropdown :open="showRemoteDropdown" @openChange="v => showRemoteDropdown = v" :trigger="['click']">
<span class="remote-selector" @click.prevent="showRemoteDropdown = !showRemoteDropdown">
<GlobalOutlined style="margin-right: 4px;" />
{{ currentRemote }}
<CaretDownOutlined style="font-size: 10px; margin-left: 2px;" />
</span>
<template #overlay>
<div class="remote-dropdown">
<div class="branch-dropdown-title">远程仓库</div>
<div class="branch-dropdown-list">
<div
v-for="r in remotes"
:key="r.name"
class="branch-dropdown-item"
:class="{ active: r.name === currentRemote }"
>
<div class="branch-item-main" @click="switchRemote(r.name)">
<GlobalOutlined style="font-size: 12px; margin-right: 6px;" />
<span class="branch-item-name">{{ r.name }}</span>
<span v-if="r.name === currentRemote" style="margin-left: auto; color: var(--success, #a6e3a1);"></span>
</div>
<div class="branch-item-actions" @click.stop>
<a-tooltip :title="r.url">
<span class="branch-action-btn" style="cursor: default; opacity: 0.6; font-size: 10px;">URL</span>
</a-tooltip>
<a-tooltip title="删除远程仓库" v-if="r.name !== 'origin'">
<span class="branch-action-btn danger" @click="removeRemote(r.name)"><DeleteOutlined /></span>
</a-tooltip>
</div>
</div>
<div v-if="!remotes.length" style="padding: 12px; text-align: center; color: var(--text-muted);">
无远程仓库
</div>
</div>
<!-- 添加远程仓库 -->
<div v-if="!showAddRemote" class="remote-add-btn" @click="showAddRemote = true">
<PlusOutlined /> 添加远程仓库
</div>
<div v-else class="remote-add-form">
<a-input v-model:value="newRemoteName" placeholder="名称 (如 upstream)" size="small" style="margin-bottom: 4px;" />
<a-input v-model:value="newRemoteUrl" placeholder="仓库地址" size="small" style="margin-bottom: 4px;" />
<div style="display: flex; gap: 4px;">
<a-button size="small" type="primary" :loading="addRemoteLoading" :disabled="!newRemoteName.trim() || !newRemoteUrl.trim()" @click="addRemote" style="flex: 1;">添加</a-button>
<a-button size="small" @click="showAddRemote = false; newRemoteName = ''; newRemoteUrl = ''">取消</a-button>
</div>
</div>
</div>
</template>
</a-dropdown>
<span class="remote-url-text" :title="currentRemoteUrl">{{ currentRemoteUrl }}</span>
</span>
</template> </template>
</div> </div>
<a-space class="project-actions"> <a-space class="project-actions">
@@ -1898,6 +2022,56 @@ watch(activeTab, (tab) => {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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 { .project-actions {

View File

@@ -0,0 +1,376 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import {
WarningOutlined,
DownloadOutlined,
FolderOpenOutlined,
CheckCircleOutlined,
LoadingOutlined,
CloseCircleOutlined,
} from '@ant-design/icons-vue'
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
import { Events } from '@wailsio/runtime'
const emit = defineEmits(['ready'])
const checking = ref(true)
const gitInstalled = ref(false)
const gitVersion = ref('')
const gitPath = ref('')
// 安装相关
const installDir = ref('C:\\Program Files\\Git')
const installing = ref(false)
const installPhase = ref('') // downloading | installing | done | error
const installPercent = ref(0)
const installMessage = ref('')
let unsubscribe = null
onMounted(async () => {
// 监听安装进度事件
unsubscribe = Events.On('git-install-progress', (event) => {
const data = event.data?.[0] || event.data
if (data) {
installPhase.value = data.phase
installPercent.value = data.percent
installMessage.value = data.message
if (data.phase === 'done') {
installing.value = false
gitInstalled.value = true
setTimeout(() => emit('ready'), 1500)
} else if (data.phase === 'error') {
installing.value = false
}
}
})
// 检查 Git
try {
const status = await AppService.CheckGitInstalled()
gitInstalled.value = status.installed
gitVersion.value = status.version || ''
gitPath.value = status.path || ''
if (status.installed) {
emit('ready')
}
} catch (e) {
console.error('检查 Git 失败:', e)
gitInstalled.value = false
} finally {
checking.value = false
}
})
onBeforeUnmount(() => {
if (unsubscribe) unsubscribe()
})
async function selectInstallDir() {
try {
const dir = await AppService.SelectGitInstallDir()
if (dir) installDir.value = dir
} catch (e) {
console.error('选择目录失败:', e)
}
}
async function startInstall() {
installing.value = true
installPhase.value = 'downloading'
installPercent.value = 0
installMessage.value = '准备下载...'
try {
await AppService.InstallGit(installDir.value)
} catch (e) {
console.error('安装 Git 失败:', e)
installPhase.value = 'error'
installMessage.value = '安装失败: ' + String(e)
installing.value = false
}
}
</script>
<template>
<!-- 检查中 -->
<div v-if="checking" class="git-check-overlay">
<div class="git-check-card">
<LoadingOutlined :style="{ fontSize: '32px', color: 'var(--accent, #89b4fa)' }" spin />
<div class="git-check-title">正在检查 Git 环境...</div>
</div>
</div>
<!-- Git 未安装 -->
<div v-else-if="!gitInstalled" class="git-check-overlay">
<div class="git-check-card install-card">
<div class="git-check-icon">
<WarningOutlined :style="{ fontSize: '48px', color: '#fab387' }" />
</div>
<div class="git-check-title">未检测到 Git</div>
<div class="git-check-desc">GitPilot 需要 Git 才能正常工作请安装 Git 后使用</div>
<!-- 安装进度 -->
<template v-if="installing || installPhase === 'done'">
<div class="install-progress">
<div v-if="installPhase === 'downloading'" class="progress-section">
<div class="progress-bar-bg">
<div class="progress-bar-fill" :style="{ width: installPercent + '%' }"></div>
</div>
<div class="progress-text">
<DownloadOutlined /> {{ installMessage }}
</div>
</div>
<div v-else-if="installPhase === 'installing'" class="progress-section">
<div class="progress-bar-bg">
<div class="progress-bar-fill installing-anim" style="width: 100%"></div>
</div>
<div class="progress-text">
<LoadingOutlined spin /> {{ installMessage }}
</div>
</div>
<div v-else-if="installPhase === 'done'" class="progress-section done">
<CheckCircleOutlined :style="{ fontSize: '24px', color: '#a6e3a1' }" />
<div class="progress-text success">{{ installMessage }}</div>
</div>
<div v-else-if="installPhase === 'error'" class="progress-section error">
<CloseCircleOutlined :style="{ fontSize: '24px', color: '#f38ba8' }" />
<div class="progress-text error-text">{{ installMessage }}</div>
</div>
</div>
</template>
<!-- 安装表单 -->
<template v-else>
<div class="install-form">
<div class="install-field">
<label>安装路径</label>
<div class="install-dir-input">
<input
v-model="installDir"
class="dir-input"
placeholder="C:\Program Files\Git"
/>
<button class="dir-btn" @click="selectInstallDir" title="选择目录">
<FolderOpenOutlined />
</button>
</div>
</div>
</div>
<div class="install-actions">
<button class="install-btn primary" @click="startInstall">
<DownloadOutlined /> 自动下载并安装 Git
</button>
</div>
<div class="install-hint">
将从 GitHub 下载 Git for Windows 安装包 65MB需保持网络连接
</div>
</template>
</div>
</div>
</template>
<style scoped>
.git-check-overlay {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-primary, #1e1e2e);
}
.git-check-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 40px;
max-width: 480px;
width: 100%;
}
.install-card {
background: var(--bg-secondary, #181825);
border: 1px solid var(--border-color, #313244);
border-radius: 12px;
padding: 40px 36px;
}
.git-check-icon {
margin-bottom: 4px;
}
.git-check-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary, #cdd6f4);
}
.git-check-desc {
font-size: 14px;
color: var(--text-muted, #6c7086);
text-align: center;
line-height: 1.6;
}
.install-form {
width: 100%;
margin-top: 8px;
}
.install-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.install-field label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary, #a6adc8);
}
.install-dir-input {
display: flex;
gap: 6px;
}
.dir-input {
flex: 1;
padding: 8px 12px;
background: var(--bg-primary, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 6px;
color: var(--text-primary, #cdd6f4);
font-size: 13px;
font-family: 'Consolas', 'Courier New', monospace;
outline: none;
transition: border-color 0.15s;
}
.dir-input:focus {
border-color: var(--accent, #89b4fa);
}
.dir-btn {
padding: 8px 12px;
background: var(--bg-primary, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 6px;
color: var(--accent, #89b4fa);
cursor: pointer;
font-size: 15px;
display: flex;
align-items: center;
transition: all 0.15s;
}
.dir-btn:hover {
background: var(--bg-hover, #313244);
}
.install-actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}
.install-btn {
padding: 10px 20px;
border-radius: 8px;
border: 1px solid var(--border-color, #313244);
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.15s;
}
.install-btn.primary {
background: var(--accent, #89b4fa);
color: #1e1e2e;
border-color: var(--accent, #89b4fa);
}
.install-btn.primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.install-hint {
font-size: 12px;
color: var(--text-muted, #6c7086);
text-align: center;
line-height: 1.5;
}
/* 安装进度 */
.install-progress {
width: 100%;
margin-top: 8px;
}
.progress-section {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.progress-section.done,
.progress-section.error {
gap: 12px;
}
.progress-bar-bg {
width: 100%;
height: 6px;
background: var(--bg-primary, #1e1e2e);
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--accent, #89b4fa);
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-bar-fill.installing-anim {
background: linear-gradient(90deg, var(--accent, #89b4fa) 0%, #b4befe 50%, var(--accent, #89b4fa) 100%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.progress-text {
font-size: 13px;
color: var(--text-secondary, #a6adc8);
display: flex;
align-items: center;
gap: 6px;
}
.progress-text.success {
color: #a6e3a1;
font-weight: 600;
}
.progress-text.error-text {
color: #f38ba8;
font-size: 12px;
text-align: center;
word-break: break-all;
}
</style>

View File

@@ -457,7 +457,7 @@ async function doBatchPull() {
if (!selectedBatchPaths.value.length) return if (!selectedBatchPaths.value.length) return
batchActionLoading.value = 'pull' batchActionLoading.value = 'pull'
try { try {
const results = await AppService.BatchPull(selectedBatchPaths.value) const results = await AppService.BatchPull(selectedBatchPaths.value, 'origin')
batchResults.value = Array.isArray(results) ? results : [] batchResults.value = Array.isArray(results) ? results : []
const successCount = batchResults.value.filter(r => r.success).length const successCount = batchResults.value.filter(r => r.success).length
message.info(`批量 Pull 完成:${successCount}/${batchResults.value.length} 成功`) message.info(`批量 Pull 完成:${successCount}/${batchResults.value.length} 成功`)
@@ -473,7 +473,7 @@ async function doBatchPush() {
if (!selectedBatchPaths.value.length) return if (!selectedBatchPaths.value.length) return
batchActionLoading.value = 'push' batchActionLoading.value = 'push'
try { try {
const results = await AppService.BatchPush(selectedBatchPaths.value) const results = await AppService.BatchPush(selectedBatchPaths.value, 'origin')
batchResults.value = Array.isArray(results) ? results : [] batchResults.value = Array.isArray(results) ? results : []
const successCount = batchResults.value.filter(r => r.success).length const successCount = batchResults.value.filter(r => r.success).length
message.info(`批量 Push 完成:${successCount}/${batchResults.value.length} 成功`) message.info(`批量 Push 完成:${successCount}/${batchResults.value.length} 成功`)

View File

@@ -2,6 +2,11 @@
import { ref, onBeforeUnmount } from 'vue' import { ref, onBeforeUnmount } from 'vue'
const selectedProject = ref(null) const selectedProject = ref(null)
const gitReady = ref(false)
function onGitReady() {
gitReady.value = true
}
function onSelectProject(project) { function onSelectProject(project) {
selectedProject.value = project selectedProject.value = project
@@ -43,7 +48,11 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<div class="main-layout"> <!-- Git 环境检查 -->
<GitCheck v-if="!gitReady" @ready="onGitReady" />
<!-- 主界面 -->
<div v-else class="main-layout">
<Sidebar :selected-project="selectedProject" :style="{ width: sidebarWidth + 'px' }" @select-project="onSelectProject" /> <Sidebar :selected-project="selectedProject" :style="{ width: sidebarWidth + 'px' }" @select-project="onSelectProject" />
<div class="resize-handle" @mousedown="startSidebarResize"></div> <div class="resize-handle" @mousedown="startSidebarResize"></div>
<ContentArea :project="selectedProject" /> <ContentArea :project="selectedProject" />

View File

@@ -53,6 +53,17 @@ export function AddProject(platform, username, name, path) {
return $Call.ByID(2299402672, platform, username, name, path); return $Call.ByID(2299402672, platform, username, name, path);
} }
/**
* AddRemote 添加远程仓库
* @param {string} path
* @param {string} name
* @param {string} url
* @returns {$CancellablePromise<void>}
*/
export function AddRemote(path, name, url) {
return $Call.ByID(3697916441, path, name, url);
}
/** /**
* AddUser 添加用户到指定平台 * AddUser 添加用户到指定平台
* @param {string} platform * @param {string} platform
@@ -67,10 +78,11 @@ export function AddUser(platform, username, token) {
/** /**
* BatchPull 批量拉取指定项目 * BatchPull 批量拉取指定项目
* @param {string[]} paths * @param {string[]} paths
* @param {string} remote
* @returns {$CancellablePromise<$models.BatchPullResult[]>} * @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/ */
export function BatchPull(paths) { export function BatchPull(paths, remote) {
return $Call.ByID(758996647, paths).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(758996647, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result); return $$createType1($result);
})); }));
} }
@@ -78,14 +90,25 @@ export function BatchPull(paths) {
/** /**
* BatchPush 批量推送指定项目 * BatchPush 批量推送指定项目
* @param {string[]} paths * @param {string[]} paths
* @param {string} remote
* @returns {$CancellablePromise<$models.BatchPullResult[]>} * @returns {$CancellablePromise<$models.BatchPullResult[]>}
*/ */
export function BatchPush(paths) { export function BatchPush(paths, remote) {
return $Call.ByID(794082076, paths).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(794082076, paths, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType1($result); return $$createType1($result);
})); }));
} }
/**
* CheckGitInstalled 检查 Git 是否已安装
* @returns {$CancellablePromise<$models.GitStatus>}
*/
export function CheckGitInstalled() {
return $Call.ByID(464620426).then(/** @type {($result: any) => any} */(($result) => {
return $$createType2($result);
}));
}
/** /**
* CheckoutRemoteBranch 检出远程分支到本地 * CheckoutRemoteBranch 检出远程分支到本地
* @param {string} path * @param {string} path
@@ -155,20 +178,22 @@ export function DeleteBranch(path, name, force) {
* DeleteRemoteBranch 删除远程分支 * DeleteRemoteBranch 删除远程分支
* @param {string} path * @param {string} path
* @param {string} branch * @param {string} branch
* @param {string} remote
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function DeleteRemoteBranch(path, branch) { export function DeleteRemoteBranch(path, branch, remote) {
return $Call.ByID(2626609049, path, branch); return $Call.ByID(2626609049, path, branch, remote);
} }
/** /**
* DeleteTag 删除标签(本地+远程) * DeleteTag 删除标签(本地+远程)
* @param {string} path * @param {string} path
* @param {string} name * @param {string} name
* @param {string} remote
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function DeleteTag(path, name) { export function DeleteTag(path, name, remote) {
return $Call.ByID(873653241, path, name); return $Call.ByID(873653241, path, name, remote);
} }
/** /**
@@ -182,12 +207,13 @@ export function DiscardFiles(path, files) {
} }
/** /**
* FetchProject 拉取远程信息 * FetchProject 拉取远程信息(指定 remote空则 fetch --all
* @param {string} path * @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>} * @returns {$CancellablePromise<string>}
*/ */
export function FetchProject(path) { export function FetchProject(path, remote) {
return $Call.ByID(3541106829, path); return $Call.ByID(3541106829, path, remote);
} }
/** /**
@@ -196,7 +222,7 @@ export function FetchProject(path) {
*/ */
export function GetAllProjectOverview() { export function GetAllProjectOverview() {
return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2453130151).then(/** @type {($result: any) => any} */(($result) => {
return $$createType3($result); return $$createType4($result);
})); }));
} }
@@ -206,7 +232,7 @@ export function GetAllProjectOverview() {
*/ */
export function GetAppSettings() { export function GetAppSettings() {
return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(428589026).then(/** @type {($result: any) => any} */(($result) => {
return $$createType5($result); return $$createType6($result);
})); }));
} }
@@ -217,7 +243,7 @@ export function GetAppSettings() {
*/ */
export function GetBranches(path) { export function GetBranches(path) {
return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(1686190192, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType7($result); return $$createType8($result);
})); }));
} }
@@ -250,7 +276,7 @@ export function GetCommitFileDiff(path, hash, filePath) {
*/ */
export function GetCommitFiles(path, hash) { export function GetCommitFiles(path, hash) {
return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2420707876, path, hash).then(/** @type {($result: any) => any} */(($result) => {
return $$createType9($result); return $$createType10($result);
})); }));
} }
@@ -262,7 +288,7 @@ export function GetCommitFiles(path, hash) {
*/ */
export function GetCommitLog(path, count) { export function GetCommitLog(path, count) {
return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(1281870789, path, count).then(/** @type {($result: any) => any} */(($result) => {
return $$createType11($result); return $$createType12($result);
})); }));
} }
@@ -283,7 +309,7 @@ export function GetConflictFileContent(projectPath, filePath) {
*/ */
export function GetConflictFiles(path) { export function GetConflictFiles(path) {
return $Call.ByID(2446806137, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2446806137, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType13($result); return $$createType14($result);
})); }));
} }
@@ -323,7 +349,7 @@ export function GetFileDiffStaged(projectPath, filePath) {
*/ */
export function GetGitGlobalConfig() { export function GetGitGlobalConfig() {
return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(154811497).then(/** @type {($result: any) => any} */(($result) => {
return $$createType15($result); return $$createType16($result);
})); }));
} }
@@ -334,7 +360,7 @@ export function GetGitGlobalConfig() {
*/ */
export function GetPlatformInfo(name) { export function GetPlatformInfo(name) {
return $Call.ByID(2668095547, name).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2668095547, name).then(/** @type {($result: any) => any} */(($result) => {
return $$createType17($result); return $$createType18($result);
})); }));
} }
@@ -345,7 +371,7 @@ export function GetPlatformInfo(name) {
*/ */
export function GetProjectChangedFiles(path) { export function GetProjectChangedFiles(path) {
return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2302591462, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType19($result); return $$createType20($result);
})); }));
} }
@@ -356,7 +382,7 @@ export function GetProjectChangedFiles(path) {
*/ */
export function GetProjectStatus(path) { export function GetProjectStatus(path) {
return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(3451089027, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType21($result); return $$createType22($result);
})); }));
} }
@@ -366,18 +392,30 @@ export function GetProjectStatus(path) {
*/ */
export function GetProjectTree() { export function GetProjectTree() {
return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(651189689).then(/** @type {($result: any) => any} */(($result) => {
return $$createType23($result); return $$createType24($result);
})); }));
} }
/** /**
* GetRemoteBranches 获取远程分支列表 * GetRemoteBranches 获取远程分支列表
* @param {string} path * @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<$models.BranchInfo[]>} * @returns {$CancellablePromise<$models.BranchInfo[]>}
*/ */
export function GetRemoteBranches(path) { export function GetRemoteBranches(path, remote) {
return $Call.ByID(994796370, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(994796370, path, remote).then(/** @type {($result: any) => any} */(($result) => {
return $$createType7($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);
})); }));
} }
@@ -388,7 +426,7 @@ export function GetRemoteBranches(path) {
*/ */
export function GetStashList(path) { export function GetStashList(path) {
return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(1948257945, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType25($result); return $$createType28($result);
})); }));
} }
@@ -399,7 +437,7 @@ export function GetStashList(path) {
*/ */
export function GetTags(path) { export function GetTags(path) {
return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => {
return $$createType27($result); return $$createType30($result);
})); }));
} }
@@ -411,10 +449,20 @@ export function GetTags(path) {
*/ */
export function GetUserInfo(platform, username) { export function GetUserInfo(platform, username) {
return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => {
return $$createType29($result); return $$createType32($result);
})); }));
} }
/**
* InstallGit 下载并安装 Git
* installDir: 用户选择的安装目录Windows 有效)
* @param {string} installDir
* @returns {$CancellablePromise<void>}
*/
export function InstallGit(installDir) {
return $Call.ByID(1052641569, installDir);
}
/** /**
* IsMerging 检查是否处于合并状态 * IsMerging 检查是否处于合并状态
* @param {string} path * @param {string} path
@@ -435,31 +483,34 @@ export function MergeBranch(path, branch) {
} }
/** /**
* PullProject 拉取项目(当前分支) * PullProject 拉取项目(当前分支,指定 remote
* @param {string} path * @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>} * @returns {$CancellablePromise<string>}
*/ */
export function PullProject(path) { export function PullProject(path, remote) {
return $Call.ByID(4145813996, path); return $Call.ByID(4145813996, path, remote);
} }
/** /**
* PushProject 推送项目(当前分支) * PushProject 推送项目(当前分支,指定 remote
* @param {string} path * @param {string} path
* @param {string} remote
* @returns {$CancellablePromise<string>} * @returns {$CancellablePromise<string>}
*/ */
export function PushProject(path) { export function PushProject(path, remote) {
return $Call.ByID(3887901113, path); return $Call.ByID(3887901113, path, remote);
} }
/** /**
* PushTag 推送标签到远程 * PushTag 推送标签到远程
* @param {string} path * @param {string} path
* @param {string} name * @param {string} name
* @param {string} remote
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
*/ */
export function PushTag(path, name) { export function PushTag(path, name, remote) {
return $Call.ByID(1424810524, path, name); return $Call.ByID(1424810524, path, name, remote);
} }
/** /**
@@ -482,6 +533,16 @@ export function RemoveProject(platform, username, name) {
return $Call.ByID(2748109581, platform, username, name); return $Call.ByID(2748109581, platform, username, name);
} }
/**
* RemoveRemote 删除远程仓库
* @param {string} path
* @param {string} name
* @returns {$CancellablePromise<void>}
*/
export function RemoveRemote(path, name) {
return $Call.ByID(2915623022, path, name);
}
/** /**
* RemoveUser 从平台删除用户 * RemoveUser 从平台删除用户
* @param {string} platform * @param {string} platform
@@ -545,7 +606,7 @@ export function SaveConflictFile(projectPath, filePath, content) {
*/ */
export function SearchCommitLog(path, keyword, author, maxCount) { export function SearchCommitLog(path, keyword, author, maxCount) {
return $Call.ByID(2874450917, path, keyword, author, maxCount).then(/** @type {($result: any) => any} */(($result) => { return $Call.ByID(2874450917, path, keyword, author, maxCount).then(/** @type {($result: any) => any} */(($result) => {
return $$createType11($result); return $$createType12($result);
})); }));
} }
@@ -557,6 +618,14 @@ export function SelectDirectory() {
return $Call.ByID(2318416763); return $Call.ByID(2318416763);
} }
/**
* SelectGitInstallDir 打开文件夹选择器让用户选择 Git 安装路径
* @returns {$CancellablePromise<string>}
*/
export function SelectGitInstallDir() {
return $Call.ByID(2941074074);
}
/** /**
* @param {application$0.App | null} app * @param {application$0.App | null} app
* @returns {$CancellablePromise<void>} * @returns {$CancellablePromise<void>}
@@ -697,31 +766,34 @@ export function UpdateUser(platform, oldUsername, newUsername, token) {
// Private type creation functions // Private type creation functions
const $$createType0 = $models.BatchPullResult.createFrom; const $$createType0 = $models.BatchPullResult.createFrom;
const $$createType1 = $Create.Array($$createType0); const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $models.ProjectOverview.createFrom; const $$createType2 = $models.GitStatus.createFrom;
const $$createType3 = $Create.Array($$createType2); const $$createType3 = $models.ProjectOverview.createFrom;
const $$createType4 = config$0.Settings.createFrom; const $$createType4 = $Create.Array($$createType3);
const $$createType5 = $Create.Nullable($$createType4); const $$createType5 = config$0.Settings.createFrom;
const $$createType6 = $models.BranchInfo.createFrom; const $$createType6 = $Create.Nullable($$createType5);
const $$createType7 = $Create.Array($$createType6); const $$createType7 = $models.BranchInfo.createFrom;
const $$createType8 = $models.CommitFileInfo.createFrom; const $$createType8 = $Create.Array($$createType7);
const $$createType9 = $Create.Array($$createType8); const $$createType9 = $models.CommitFileInfo.createFrom;
const $$createType10 = $models.CommitLog.createFrom; const $$createType10 = $Create.Array($$createType9);
const $$createType11 = $Create.Array($$createType10); const $$createType11 = $models.CommitLog.createFrom;
const $$createType12 = $models.ConflictFileInfo.createFrom; const $$createType12 = $Create.Array($$createType11);
const $$createType13 = $Create.Array($$createType12); const $$createType13 = $models.ConflictFileInfo.createFrom;
const $$createType14 = $models.GitConfig.createFrom; const $$createType14 = $Create.Array($$createType13);
const $$createType15 = $Create.Nullable($$createType14); const $$createType15 = $models.GitConfig.createFrom;
const $$createType16 = $models.PlatformInfo.createFrom; const $$createType16 = $Create.Nullable($$createType15);
const $$createType17 = $Create.Nullable($$createType16); const $$createType17 = $models.PlatformInfo.createFrom;
const $$createType18 = $models.FileInfo.createFrom; const $$createType18 = $Create.Nullable($$createType17);
const $$createType19 = $Create.Array($$createType18); const $$createType19 = $models.FileInfo.createFrom;
const $$createType20 = $models.ProjectStatus.createFrom; const $$createType20 = $Create.Array($$createType19);
const $$createType21 = $Create.Nullable($$createType20); const $$createType21 = $models.ProjectStatus.createFrom;
const $$createType22 = $models.TreeNode.createFrom; const $$createType22 = $Create.Nullable($$createType21);
const $$createType23 = $Create.Array($$createType22); const $$createType23 = $models.TreeNode.createFrom;
const $$createType24 = $models.StashInfo.createFrom; const $$createType24 = $Create.Array($$createType23);
const $$createType25 = $Create.Array($$createType24); const $$createType25 = $models.RemoteItem.createFrom;
const $$createType26 = $models.TagInfo.createFrom; const $$createType26 = $Create.Array($$createType25);
const $$createType27 = $Create.Array($$createType26); const $$createType27 = $models.StashInfo.createFrom;
const $$createType28 = $models.UserInfo.createFrom; const $$createType28 = $Create.Array($$createType27);
const $$createType29 = $Create.Nullable($$createType28); const $$createType29 = $models.TagInfo.createFrom;
const $$createType30 = $Create.Array($$createType29);
const $$createType31 = $models.UserInfo.createFrom;
const $$createType32 = $Create.Nullable($$createType31);

View File

@@ -15,9 +15,11 @@ export {
ConflictFileInfo, ConflictFileInfo,
FileInfo, FileInfo,
GitConfig, GitConfig,
GitStatus,
PlatformInfo, PlatformInfo,
ProjectOverview, ProjectOverview,
ProjectStatus, ProjectStatus,
RemoteItem,
StashInfo, StashInfo,
TagInfo, TagInfo,
TreeNode, TreeNode,

View File

@@ -328,6 +328,51 @@ export class GitConfig {
} }
} }
/**
* GitStatus Git 安装状态
*/
export class GitStatus {
/**
* Creates a new GitStatus instance.
* @param {Partial<GitStatus>} [$$source = {}] - The source object to create the GitStatus.
*/
constructor($$source = {}) {
if (!("installed" in $$source)) {
/**
* @member
* @type {boolean}
*/
this["installed"] = false;
}
if (!("version" in $$source)) {
/**
* @member
* @type {string}
*/
this["version"] = "";
}
if (!("path" in $$source)) {
/**
* @member
* @type {string}
*/
this["path"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new GitStatus instance from a string or object.
* @param {any} [$$source = {}]
* @returns {GitStatus}
*/
static createFrom($$source = {}) {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new GitStatus(/** @type {Partial<GitStatus>} */($$parsedSource));
}
}
/** /**
* PlatformInfo 平台信息 * PlatformInfo 平台信息
*/ */
@@ -462,6 +507,20 @@ export class ProjectStatus {
*/ */
this["remoteUrl"] = ""; this["remoteUrl"] = "";
} }
if (!("remotes" in $$source)) {
/**
* @member
* @type {RemoteItem[]}
*/
this["remotes"] = [];
}
if (!("currentRemote" in $$source)) {
/**
* @member
* @type {string}
*/
this["currentRemote"] = "";
}
if (!("changedFiles" in $$source)) { if (!("changedFiles" in $$source)) {
/** /**
* @member * @member
@@ -480,14 +539,56 @@ export class ProjectStatus {
*/ */
static createFrom($$source = {}) { static createFrom($$source = {}) {
const $$createField2_0 = $$createType1; const $$createField2_0 = $$createType1;
const $$createField4_0 = $$createType3;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("remotes" in $$parsedSource) {
$$parsedSource["remotes"] = $$createField2_0($$parsedSource["remotes"]);
}
if ("changedFiles" in $$parsedSource) { if ("changedFiles" in $$parsedSource) {
$$parsedSource["changedFiles"] = $$createField2_0($$parsedSource["changedFiles"]); $$parsedSource["changedFiles"] = $$createField4_0($$parsedSource["changedFiles"]);
} }
return new ProjectStatus(/** @type {Partial<ProjectStatus>} */($$parsedSource)); return new ProjectStatus(/** @type {Partial<ProjectStatus>} */($$parsedSource));
} }
} }
/**
* RemoteItem 远程仓库信息
*/
export class RemoteItem {
/**
* Creates a new RemoteItem instance.
* @param {Partial<RemoteItem>} [$$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<RemoteItem>} */($$parsedSource));
}
}
/** /**
* StashInfo 贮藏信息 * StashInfo 贮藏信息
*/ */
@@ -647,7 +748,7 @@ export class TreeNode {
* @returns {TreeNode} * @returns {TreeNode}
*/ */
static createFrom($$source = {}) { static createFrom($$source = {}) {
const $$createField4_0 = $$createType3; const $$createField4_0 = $$createType5;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("children" in $$parsedSource) { if ("children" in $$parsedSource) {
$$parsedSource["children"] = $$createField4_0($$parsedSource["children"]); $$parsedSource["children"] = $$createField4_0($$parsedSource["children"]);
@@ -695,7 +796,9 @@ export class UserInfo {
} }
// Private type creation functions // Private type creation functions
const $$createType0 = FileInfo.createFrom; const $$createType0 = RemoteItem.createFrom;
const $$createType1 = $Create.Array($$createType0); const $$createType1 = $Create.Array($$createType0);
const $$createType2 = TreeNode.createFrom; const $$createType2 = FileInfo.createFrom;
const $$createType3 = $Create.Array($$createType2); const $$createType3 = $Create.Array($$createType2);
const $$createType4 = TreeNode.createFrom;
const $$createType5 = $Create.Array($$createType4);

241
internal/app/gitsetup.go Normal file
View File

@@ -0,0 +1,241 @@
package app
import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/zhuy1228/GitPilot/internal/git"
)
// GitStatus Git 安装状态
type GitStatus struct {
Installed bool `json:"installed"`
Version string `json:"version"`
Path string `json:"path"`
}
// GitInstallProgress 安装进度
type GitInstallProgress struct {
Phase string `json:"phase"` // downloading, installing, done, error
Percent float64 `json:"percent"` // 下载百分比 0-100
Message string `json:"message"`
}
// CheckGitInstalled 检查 Git 是否已安装
func (s *AppService) CheckGitInstalled() GitStatus {
path, err := exec.LookPath("git")
if err != nil {
log.Println("Git 未安装:", err)
return GitStatus{Installed: false}
}
// 获取版本号
cmd := exec.Command("git", "--version")
hideWindowCmd(cmd)
output, err := cmd.Output()
if err != nil {
log.Println("获取 Git 版本失败:", err)
return GitStatus{Installed: true, Path: path}
}
version := strings.TrimSpace(string(output))
// "git version 2.43.0.windows.1" -> "2.43.0"
version = strings.TrimPrefix(version, "git version ")
if idx := strings.Index(version, ".windows"); idx > 0 {
version = version[:idx]
}
return GitStatus{
Installed: true,
Version: version,
Path: path,
}
}
// SelectGitInstallDir 打开文件夹选择器让用户选择 Git 安装路径
func (s *AppService) SelectGitInstallDir() (string, error) {
if s.app == nil {
return "", fmt.Errorf("应用未初始化")
}
path, err := s.app.Dialog.OpenFile().
CanChooseDirectories(true).
CanChooseFiles(false).
SetTitle("选择 Git 安装路径").
PromptForSingleSelection()
if err != nil {
return "", fmt.Errorf("打开文件夹选择器失败: %w", err)
}
return path, nil
}
// getGitDownloadURL 根据平台返回 Git 下载地址
func getGitDownloadURL() string {
switch runtime.GOOS {
case "windows":
if runtime.GOARCH == "arm64" {
return "https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-arm64.exe"
}
return "https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-64-bit.exe"
case "darwin":
return "" // macOS 建议使用 brew 或 Xcode CLI
default:
return "" // Linux 建议使用包管理器
}
}
// InstallGit 下载并安装 Git
// installDir: 用户选择的安装目录Windows 有效)
func (s *AppService) InstallGit(installDir string) error {
if runtime.GOOS != "windows" {
return fmt.Errorf("自动安装仅支持 Windows请使用系统包管理器安装 Git")
}
url := getGitDownloadURL()
if url == "" {
return fmt.Errorf("无法获取 Git 下载地址")
}
// 1. 下载安装包
s.emitInstallProgress("downloading", 0, "正在下载 Git 安装包...")
log.Println("开始下载 Git:", url)
tmpDir := os.TempDir()
installerPath := filepath.Join(tmpDir, "Git-Installer.exe")
err := s.downloadFile(url, installerPath)
if err != nil {
s.emitInstallProgress("error", 0, "下载失败: "+err.Error())
return fmt.Errorf("下载 Git 安装包失败: %w", err)
}
defer os.Remove(installerPath)
// 2. 静默安装
s.emitInstallProgress("installing", 100, "正在安装 Git请稍候...")
log.Println("开始安装 Git 到:", installDir)
// Git for Windows 静默安装参数
args := []string{
"/VERYSILENT",
"/NORESTART",
"/NOCANCEL",
"/SP-",
"/CLOSEAPPLICATIONS",
"/RESTARTAPPLICATIONS",
"/COMPONENTS=icons,ext,ext\\shellhere,ext\\guihere,gitlfs,assoc,assoc_sh,autoupdate",
}
if installDir != "" {
args = append(args, "/DIR="+installDir)
}
cmd := exec.Command(installerPath, args...)
hideWindowCmd(cmd)
err = cmd.Run()
if err != nil {
s.emitInstallProgress("error", 0, "安装失败: "+err.Error())
return fmt.Errorf("安装 Git 失败: %w", err)
}
// 3. 验证安装
time.Sleep(2 * time.Second) // 等待 PATH 更新
status := s.CheckGitInstalled()
if !status.Installed {
// 尝试在指定目录查找
gitExe := filepath.Join(installDir, "bin", "git.exe")
if _, err := os.Stat(gitExe); err == nil {
s.emitInstallProgress("done", 100, "Git 安装成功!路径: "+gitExe)
// 重新初始化 gitClient
s.reinitGitClient()
return nil
}
s.emitInstallProgress("error", 0, "安装完成但未检测到 Git请重启应用重试")
return fmt.Errorf("安装完成但未检测到 Git")
}
s.emitInstallProgress("done", 100, "Git 安装成功!版本: "+status.Version)
s.reinitGitClient()
return nil
}
// reinitGitClient 重新初始化 Git 客户端
func (s *AppService) reinitGitClient() {
s.gitClient = git.NewGitClient()
}
// emitInstallProgress 向前端发送安装进度事件
func (s *AppService) emitInstallProgress(phase string, percent float64, message string) {
if s.app != nil {
s.app.Event.Emit("git-install-progress", GitInstallProgress{
Phase: phase,
Percent: percent,
Message: message,
})
}
}
// downloadFile 下载文件并通过事件报告进度
func (s *AppService) downloadFile(url, dest string) error {
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
totalSize := resp.ContentLength
var downloaded int64
buf := make([]byte, 32*1024)
lastReport := time.Now()
for {
n, readErr := resp.Body.Read(buf)
if n > 0 {
_, writeErr := out.Write(buf[:n])
if writeErr != nil {
return writeErr
}
downloaded += int64(n)
// 每 300ms 报告一次进度
if time.Since(lastReport) > 300*time.Millisecond {
var pct float64
if totalSize > 0 {
pct = float64(downloaded) / float64(totalSize) * 100
}
sizeMB := float64(downloaded) / 1024 / 1024
msg := fmt.Sprintf("正在下载... %.1f MB", sizeMB)
if totalSize > 0 {
totalMB := float64(totalSize) / 1024 / 1024
msg = fmt.Sprintf("正在下载... %.1f / %.1f MB", sizeMB, totalMB)
}
s.emitInstallProgress("downloading", pct, msg)
lastReport = time.Now()
}
}
if readErr != nil {
if readErr == io.EOF {
break
}
return readErr
}
}
return nil
}

View File

@@ -0,0 +1,8 @@
//go:build !windows
package app
import "os/exec"
// hideWindowCmd 非 Windows 平台无需处理
func hideWindowCmd(cmd *exec.Cmd) {}

View File

@@ -0,0 +1,14 @@
package app
import (
"os/exec"
"syscall"
)
// hideWindowCmd 在 Windows 上隐藏子进程的控制台窗口
func hideWindowCmd(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000,
}
}

View File

@@ -309,9 +309,17 @@ func (s *AppService) GetPlatformInfo(name string) (*PlatformInfo, error) {
type ProjectStatus struct { type ProjectStatus struct {
Branch string `json:"branch"` Branch string `json:"branch"`
RemoteURL string `json:"remoteUrl"` RemoteURL string `json:"remoteUrl"`
Remotes []RemoteItem `json:"remotes"`
CurrentRemote string `json:"currentRemote"`
ChangedFiles []FileInfo `json:"changedFiles"` ChangedFiles []FileInfo `json:"changedFiles"`
} }
// RemoteItem 远程仓库信息
type RemoteItem struct {
Name string `json:"name"`
URL string `json:"url"`
}
// FileInfo 文件信息 // FileInfo 文件信息
type FileInfo struct { type FileInfo struct {
Status string `json:"status"` Status string `json:"status"`
@@ -346,9 +354,24 @@ func (s *AppService) GetProjectStatus(path string) (*ProjectStatus, error) {
remoteURL, _ := s.gitClient.RemoteURL(path) 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{ return &ProjectStatus{
Branch: strings.TrimSpace(branch), Branch: strings.TrimSpace(branch),
RemoteURL: remoteURL, RemoteURL: remoteURL,
Remotes: remotes,
CurrentRemote: currentRemote,
ChangedFiles: []FileInfo{}, ChangedFiles: []FileInfo{},
}, nil }, nil
} }
@@ -515,22 +538,28 @@ func (s *AppService) GetFileDiffStaged(projectPath, filePath string) (string, er
return s.gitClient.DiffStagedFile(projectPath, filePath) return s.gitClient.DiffStagedFile(projectPath, filePath)
} }
// PullProject 拉取项目(当前分支) // PullProject 拉取项目(当前分支,指定 remote
func (s *AppService) PullProject(path string) (string, error) { func (s *AppService) PullProject(path, remote string) (string, error) {
if remote == "" {
remote = "origin"
}
branch, err := s.gitClient.Branch(path) branch, err := s.gitClient.Branch(path)
if err != nil { if err != nil {
return "", fmt.Errorf("获取当前分支失败: %w", err) 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 推送项目(当前分支) // PushProject 推送项目(当前分支,指定 remote
func (s *AppService) PushProject(path string) (string, error) { func (s *AppService) PushProject(path, remote string) (string, error) {
if remote == "" {
remote = "origin"
}
branch, err := s.gitClient.Branch(path) branch, err := s.gitClient.Branch(path)
if err != nil { if err != nil {
return "", fmt.Errorf("获取当前分支失败: %w", err) 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 // GetCommitDiff 获取指定提交的 diff
@@ -581,9 +610,12 @@ func (s *AppService) GetCommitFileDiff(path, hash, filePath string) (string, err
return s.gitClient.CommitFileDiff(path, hash, filePath) return s.gitClient.CommitFileDiff(path, hash, filePath)
} }
// FetchProject 拉取远程信息 // FetchProject 拉取远程信息(指定 remote空则 fetch --all
func (s *AppService) FetchProject(path string) (string, error) { func (s *AppService) FetchProject(path, remote string) (string, error) {
if remote == "" {
return s.gitClient.Fetch(path) return s.gitClient.Fetch(path)
}
return s.gitClient.FetchRemote(path, remote)
} }
// CommitLog 提交记录 // CommitLog 提交记录
@@ -791,7 +823,7 @@ func (s *AppService) CreateTag(path, name, message string) error {
} }
// DeleteTag 删除标签(本地+远程) // 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) { if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path) return fmt.Errorf("项目路径不存在: %s", path)
} }
@@ -799,17 +831,20 @@ func (s *AppService) DeleteTag(path, name string) error {
if name == "" { if name == "" {
return fmt.Errorf("标签名不能为空") return fmt.Errorf("标签名不能为空")
} }
if remote == "" {
remote = "origin"
}
// 删除本地标签 // 删除本地标签
if _, err := s.gitClient.DeleteTag(path, name); err != nil { if _, err := s.gitClient.DeleteTag(path, name); err != nil {
return fmt.Errorf("删除本地标签失败: %w", err) return fmt.Errorf("删除本地标签失败: %w", err)
} }
// 尝试删除远程标签(忽略错误,可能未推送过) // 尝试删除远程标签(忽略错误,可能未推送过)
s.gitClient.DeleteRemoteTag(path, name) s.gitClient.DeleteRemoteTagFrom(path, remote, name)
return nil return nil
} }
// PushTag 推送标签到远程 // 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) { if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path) return fmt.Errorf("项目路径不存在: %s", path)
} }
@@ -817,7 +852,10 @@ func (s *AppService) PushTag(path, name string) error {
if name == "" { if name == "" {
return fmt.Errorf("标签名不能为空") return fmt.Errorf("标签名不能为空")
} }
_, err := s.gitClient.PushTag(path, name) if remote == "" {
remote = "origin"
}
_, err := s.gitClient.PushTagTo(path, remote, name)
return err return err
} }
@@ -869,11 +907,17 @@ func (s *AppService) MergeBranch(path, branch string) (string, error) {
// --- 远程分支管理 --- // --- 远程分支管理 ---
// GetRemoteBranches 获取远程分支列表 // 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) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, fmt.Errorf("项目路径不存在: %s", path) 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 { if err != nil {
return nil, fmt.Errorf("获取远程分支列表失败: %w", err) return nil, fmt.Errorf("获取远程分支列表失败: %w", err)
} }
@@ -907,7 +951,7 @@ func (s *AppService) CheckoutRemoteBranch(path, remoteBranch string) error {
} }
// DeleteRemoteBranch 删除远程分支 // 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) { if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("项目路径不存在: %s", path) return fmt.Errorf("项目路径不存在: %s", path)
} }
@@ -915,12 +959,15 @@ func (s *AppService) DeleteRemoteBranch(path, branch string) error {
if branch == "" { if branch == "" {
return fmt.Errorf("分支名不能为空") return fmt.Errorf("分支名不能为空")
} }
if remote == "" {
remote = "origin"
}
// origin/feature -> feature // origin/feature -> feature
localName := branch localName := branch
if idx := strings.Index(branch, "/"); idx != -1 { if idx := strings.Index(branch, "/"); idx != -1 {
localName = branch[idx+1:] localName = branch[idx+1:]
} }
_, err := s.gitClient.DeleteRemoteBranch(path, localName) _, err := s.gitClient.DeleteRemoteBranchFrom(path, remote, localName)
return err return err
} }
@@ -1050,6 +1097,54 @@ func (s *AppService) UpdateAppSettings(logLevel string) error {
return config.SaveConfig(s.config) 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 冲突文件信息 // ConflictFileInfo 冲突文件信息
@@ -1211,7 +1306,10 @@ type BatchPullResult struct {
} }
// BatchPull 批量拉取指定项目 // BatchPull 批量拉取指定项目
func (s *AppService) BatchPull(paths []string) []BatchPullResult { func (s *AppService) BatchPull(paths []string, remote string) []BatchPullResult {
if remote == "" {
remote = "origin"
}
var results []BatchPullResult var results []BatchPullResult
for _, path := range paths { for _, path := range paths {
result := BatchPullResult{Path: path} result := BatchPullResult{Path: path}
@@ -1229,7 +1327,7 @@ func (s *AppService) BatchPull(paths []string) []BatchPullResult {
results = append(results, result) results = append(results, result)
continue continue
} }
_, err = s.gitClient.Run(path, "pull", "origin", strings.TrimSpace(branch)) _, err = s.gitClient.PullFrom(path, remote, strings.TrimSpace(branch))
if err != nil { if err != nil {
result.Message = err.Error() result.Message = err.Error()
} else { } else {
@@ -1242,7 +1340,10 @@ func (s *AppService) BatchPull(paths []string) []BatchPullResult {
} }
// BatchPush 批量推送指定项目 // BatchPush 批量推送指定项目
func (s *AppService) BatchPush(paths []string) []BatchPullResult { func (s *AppService) BatchPush(paths []string, remote string) []BatchPullResult {
if remote == "" {
remote = "origin"
}
var results []BatchPullResult var results []BatchPullResult
for _, path := range paths { for _, path := range paths {
result := BatchPullResult{Path: path} result := BatchPullResult{Path: path}
@@ -1259,7 +1360,7 @@ func (s *AppService) BatchPush(paths []string) []BatchPullResult {
results = append(results, result) results = append(results, result)
continue continue
} }
_, err = s.gitClient.Run(path, "push", "origin", strings.TrimSpace(branch)) _, err = s.gitClient.PushTo(path, remote, strings.TrimSpace(branch))
if err != nil { if err != nil {
result.Message = err.Error() result.Message = err.Error()
} else { } else {

View File

@@ -60,6 +60,21 @@ func (g *GitClient) Push(path string) (string, error) {
return g.Run(path, "push") 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) { func (g *GitClient) Status(path string) (string, error) {
return g.Run(path, "status") return g.Run(path, "status")
} }
@@ -93,6 +108,47 @@ func (g *GitClient) Fetch(path string) (string, error) {
return g.Run(path, "fetch", "--all") 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) { func (g *GitClient) RemoteURL(path string) (string, error) {
out, err := g.Run(path, "config", "--get", "remote.origin.url") out, err := g.Run(path, "config", "--get", "remote.origin.url")
if err != nil { 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) { func (g *GitClient) UnpushedCommits(path, branch string) (string, error) {
return g.Run(path, "rev-list", "origin/"+branch+"..HEAD") 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 撤回指定提交(创建一个反向提交) // RevertCommit 撤回指定提交(创建一个反向提交)
func (g *GitClient) RevertCommit(path, hash string) (string, error) { func (g *GitClient) RevertCommit(path, hash string) (string, error) {
return g.Run(path, "revert", "--no-edit", hash) 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) return g.Run(path, "tag", "-d", name)
} }
// PushTag 推送标签到远程 // PushTag 推送标签到远程(默认 origin
func (g *GitClient) PushTag(path, name string) (string, error) { func (g *GitClient) PushTag(path, name string) (string, error) {
return g.Run(path, "push", "origin", name) 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) { func (g *GitClient) DeleteRemoteTag(path, name string) (string, error) {
return g.Run(path, "push", "origin", "--delete", name) 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 获取所有本地分支 // BranchList 获取所有本地分支
func (g *GitClient) BranchList(path string) (string, error) { func (g *GitClient) BranchList(path string) (string, error) {
return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)") 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) return g.Run(path, "merge", branch)
} }
// DeleteRemoteBranch 删除远程分支 // DeleteRemoteBranch 删除远程分支(默认 origin
func (g *GitClient) DeleteRemoteBranch(path, branch string) (string, error) { func (g *GitClient) DeleteRemoteBranch(path, branch string) (string, error) {
return g.Run(path, "push", "origin", "--delete", branch) 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 获取所有远程分支 // RemoteBranchList 获取所有远程分支
func (g *GitClient) RemoteBranchList(path string) (string, error) { func (g *GitClient) RemoteBranchList(path string) (string, error) {
return g.Run(path, "branch", "-r", "--format=%(refname:short)") 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 从远程分支检出新本地分支 // CheckoutNewBranch 从远程分支检出新本地分支
func (g *GitClient) CheckoutNewBranch(path, localBranch, remoteBranch string) (string, error) { func (g *GitClient) CheckoutNewBranch(path, localBranch, remoteBranch string) (string, error) {
return g.Run(path, "checkout", "-b", localBranch, remoteBranch) return g.Run(path, "checkout", "-b", localBranch, remoteBranch)