diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7dee897 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,212 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Wails CLI + run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest + + - name: Install frontend dependencies + run: npm install + working-directory: frontend + + - name: Build frontend + run: npm run generate + working-directory: frontend + + - name: Generate bindings + run: wails3 generate bindings + + - name: Generate icons + run: wails3 generate icons -input build/appicon.png -windowsDir build/windows -macDir build/darwin + + - name: Generate syso + working-directory: build + run: wails3 generate syso -arch amd64 -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_amd64.syso + + - name: Build + env: + CGO_ENABLED: 1 + GOOS: windows + GOARCH: amd64 + run: go build -tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui" -o bin/gitpilot.exe + + - name: Copy config.yaml + run: Copy-Item config.yaml -Destination bin/config.yaml + + - name: Remove syso + run: Remove-Item *.syso + + - name: Package + run: Compress-Archive -Path "bin/gitpilot.exe","bin/config.yaml" -DestinationPath "bin/gitpilot-windows-amd64.zip" -Force + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gitpilot-windows-amd64 + path: bin/gitpilot-windows-amd64.zip + + build-macos: + runs-on: macos-latest + strategy: + matrix: + arch: [amd64, arm64] + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Wails CLI + run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest + + - name: Install frontend dependencies + run: npm install + working-directory: frontend + + - name: Build frontend + run: npm run generate + working-directory: frontend + + - name: Generate bindings + run: wails3 generate bindings + + - name: Generate icons + run: wails3 generate icons -input build/appicon.png -windowsDir build/windows -macDir build/darwin + + - name: Build + env: + CGO_ENABLED: 1 + GOOS: darwin + GOARCH: ${{ matrix.arch }} + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + run: go build -tags "production,disable_onnx" -trimpath -buildvcs=false -ldflags="-w -s" -o bin/gitpilot + + - name: Create .app bundle + run: | + mkdir -p bin/gitpilot.app/Contents/{MacOS,Resources} + cp build/darwin/icons.icns bin/gitpilot.app/Contents/Resources/ + cp bin/gitpilot bin/gitpilot.app/Contents/MacOS/ + cp config.yaml bin/gitpilot.app/Contents/MacOS/config.yaml + cp build/darwin/Info.plist bin/gitpilot.app/Contents/ + codesign --force --deep --sign - bin/gitpilot.app + + - name: Package + run: | + cd bin + zip -r gitpilot-macos-${{ matrix.arch }}.zip gitpilot.app config.yaml + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gitpilot-macos-${{ matrix.arch }} + path: bin/gitpilot-macos-${{ matrix.arch }}.zip + + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev + + - name: Install Wails CLI + run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest + + - name: Install frontend dependencies + run: npm install + working-directory: frontend + + - name: Build frontend + run: npm run generate + working-directory: frontend + + - name: Generate bindings + run: wails3 generate bindings + + - name: Generate icons + run: wails3 generate icons -input build/appicon.png -windowsDir build/windows -macDir build/darwin + + - name: Build + env: + CGO_ENABLED: 1 + GOOS: linux + GOARCH: amd64 + run: go build -tags "production,disable_onnx" -trimpath -buildvcs=false -ldflags="-w -s" -o bin/gitpilot + + - name: Copy config.yaml + run: cp config.yaml bin/config.yaml + + - name: Package + run: | + cd bin + tar czf gitpilot-linux-amd64.tar.gz gitpilot config.yaml + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gitpilot-linux-amd64 + path: bin/gitpilot-linux-amd64.tar.gz + + release: + needs: [build-windows, build-macos, build-linux] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: | + artifacts/gitpilot-windows-amd64/gitpilot-windows-amd64.zip + artifacts/gitpilot-macos-amd64/gitpilot-macos-amd64.zip + artifacts/gitpilot-macos-arm64/gitpilot-macos-arm64.zip + artifacts/gitpilot-linux-amd64/gitpilot-linux-amd64.tar.gz diff --git a/.task/checksum/generate-bindings--BUILD_FLAGS--buildvcs-false--gcflags-all---l-- b/.task/checksum/generate-bindings--BUILD_FLAGS--buildvcs-false--gcflags-all---l-- index 85d7703..b20b9d5 100644 --- a/.task/checksum/generate-bindings--BUILD_FLAGS--buildvcs-false--gcflags-all---l-- +++ b/.task/checksum/generate-bindings--BUILD_FLAGS--buildvcs-false--gcflags-all---l-- @@ -1 +1 @@ -b3473eff2f1694108cc3ae68b4ae04f0 +e4fca8850949396013f268b14abf0d8a diff --git a/bin/config.yaml b/bin/config.yaml new file mode 100644 index 0000000..953c9f0 --- /dev/null +++ b/bin/config.yaml @@ -0,0 +1,41 @@ +platforms: + gitea: + base_url: http://192.168.1.10:3000 + users: [] + gitee: + base_url: "" + users: + - username: zhuyuj + token: "" + projects: [] + github: + base_url: "" + users: + - username: zhuy1228 + token: "" + projects: + - name: GitPilot + path: F:\Project\GitPilot + enabled: false + - name: nakama + path: F:\Project\nakama + enabled: false + - name: DevPack + path: F:\Project\DevPack + enabled: false + - name: go-desk-service + path: F:\Project\go-desk\go-desk-service + enabled: false + - name: go-desk-wails + path: F:\Project\go-desk\go-desk-wails + enabled: false + - name: go-game + path: F:\Project\go-game + enabled: false + - name: PeterAgent + path: F:\Project\PeterAgent + enabled: false +settings: + concurrency: 6 + network_check: true + log_level: info diff --git a/bin/gitpilot.exe b/bin/gitpilot.exe index 310787c..ee6cac5 100644 Binary files a/bin/gitpilot.exe and b/bin/gitpilot.exe differ diff --git a/build/darwin/Taskfile.yml b/build/darwin/Taskfile.yml index e4d0de2..423f92b 100644 --- a/build/darwin/Taskfile.yml +++ b/build/darwin/Taskfile.yml @@ -17,6 +17,7 @@ tasks: - task: common:generate:icons cmds: - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + - cp config.yaml {{.BIN_DIR}}/config.yaml vars: BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags "production,disable_onnx" -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags "disable_onnx" -buildvcs=false -gcflags=all="-l"{{end}}' DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' @@ -68,6 +69,7 @@ tasks: - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS + - cp {{.BIN_DIR}}/config.yaml {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS/config.yaml - cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app diff --git a/build/linux/Taskfile.yml b/build/linux/Taskfile.yml index 7b70718..f754fd9 100644 --- a/build/linux/Taskfile.yml +++ b/build/linux/Taskfile.yml @@ -17,6 +17,7 @@ tasks: - task: common:generate:icons cmds: - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} + - cp config.yaml {{.BIN_DIR}}/config.yaml vars: BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags "production,disable_onnx" -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags "disable_onnx" -buildvcs=false -gcflags=all="-l"{{end}}' env: diff --git a/build/windows/Taskfile.yml b/build/windows/Taskfile.yml index 85ed398..f2963c6 100644 --- a/build/windows/Taskfile.yml +++ b/build/windows/Taskfile.yml @@ -18,6 +18,10 @@ tasks: cmds: - task: generate:syso - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe + - cmd: powershell Copy-Item config.yaml -Destination {{.BIN_DIR}}/config.yaml -Force + platforms: [windows] + - cmd: cp config.yaml {{.BIN_DIR}}/config.yaml + platforms: [linux, darwin] - cmd: powershell Remove-item *.syso platforms: [windows] - cmd: rm -f *.syso @@ -31,9 +35,15 @@ tasks: PRODUCTION: '{{.PRODUCTION | default "false"}}' package: - summary: Packages a production build of the application + summary: Packages a production build of the application into a zip file + deps: + - task: build + vars: + PRODUCTION: "true" cmds: - - task: create:nsis:installer + - cmd: powershell Compress-Archive -Path "{{.BIN_DIR}}/{{.APP_NAME}}.exe","{{.BIN_DIR}}/config.yaml" -DestinationPath "{{.BIN_DIR}}/{{.APP_NAME}}-windows-{{.ARCH}}.zip" -Force + vars: + ARCH: '{{.ARCH | default ARCH}}' generate:syso: summary: Generates Windows `.syso` file diff --git a/config/config.go b/config/config.go index 9f50ee0..98d29c9 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,7 @@ package config import ( "fmt" "os" + "path/filepath" "gopkg.in/yaml.v3" ) @@ -35,9 +36,18 @@ type AppConfig struct { Settings Settings `yaml:"settings"` } +// configPath 返回 config.yaml 的绝对路径(与可执行文件同目录) +func configPath() string { + exe, err := os.Executable() + if err != nil { + return "config.yaml" + } + return filepath.Join(filepath.Dir(exe), "config.yaml") +} + // LoadConfig 从 config.yaml 文件加载配置 func LoadConfig() (*AppConfig, error) { - file, err := os.Open("config.yaml") + file, err := os.Open(configPath()) if err != nil { return nil, err } @@ -55,7 +65,7 @@ func LoadConfig() (*AppConfig, error) { // SaveConfig 将配置保存到 config.yaml 文件 func SaveConfig(config *AppConfig) error { - file, err := os.OpenFile("config.yaml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + file, err := os.OpenFile(configPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return err } diff --git a/frontend/app/components/ContentArea.vue b/frontend/app/components/ContentArea.vue index 91586e4..551a19b 100644 --- a/frontend/app/components/ContentArea.vue +++ b/frontend/app/components/ContentArea.vue @@ -20,6 +20,11 @@ import { RollbackOutlined, ExclamationCircleOutlined, CloseCircleOutlined, + TagOutlined, + TagsOutlined, + CloudUploadOutlined as PushTagIcon, + DeleteOutlined, + SendOutlined, } from '@ant-design/icons-vue' import { Modal } from 'ant-design-vue' import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app' @@ -62,6 +67,14 @@ const selectedCommitFile = ref(null) const commitFileDiff = ref('') const commitCollapsedDirs = ref(new Set()) +// ---- 标签管理 ---- +const tags = ref([]) +const tagsLoading = ref(false) +const showCreateTag = ref(false) +const newTagName = ref('') +const newTagMessage = ref('') +const createTagLoading = ref(false) + // ---- 文件列表拖拽调整宽度 ---- const fileListWidth = ref(300) const MIN_FILELIST = 180 @@ -653,6 +666,81 @@ function formatTime(ts) { if (diff < 604800) return Math.floor(diff / 86400) + ' 天前' return d.toLocaleDateString('zh-CN') } + +// ---- 标签管理 ---- +async function loadTags() { + if (!props.project?.path) return + tagsLoading.value = true + try { + const list = await AppService.GetTags(props.project.path) + tags.value = Array.isArray(list) ? list : [] + } catch (e) { + console.error('获取标签失败:', e) + tags.value = [] + } finally { + tagsLoading.value = false + } +} + +async function createTag() { + if (!props.project?.path || !newTagName.value.trim()) return + createTagLoading.value = true + try { + await AppService.CreateTag(props.project.path, newTagName.value.trim(), newTagMessage.value.trim() || newTagName.value.trim()) + newTagName.value = '' + newTagMessage.value = '' + showCreateTag.value = false + await loadTags() + } catch (e) { + console.error('创建标签失败:', e) + Modal.error({ title: '创建标签失败', content: String(e) }) + } finally { + createTagLoading.value = false + } +} + +async function deleteTag(tag) { + if (!props.project?.path) return + Modal.confirm({ + title: '确认删除标签', + icon: h(ExclamationCircleOutlined), + content: h('div', [ + h('p', `确定要删除标签吗?`), + h('p', { style: 'font-family: monospace; color: #89b4fa; font-size: 15px;' }, tag.name), + h('p', { style: 'color: var(--text-muted); font-size: 12px; margin-top: 8px;' }, '将同时删除本地和远程标签。'), + ]), + okText: '确认删除', + cancelText: '取消', + okButtonProps: { danger: true }, + async onOk() { + try { + await AppService.DeleteTag(props.project.path, tag.name) + await loadTags() + } catch (e) { + console.error('删除标签失败:', e) + Modal.error({ title: '删除失败', content: String(e) }) + } + }, + }) +} + +async function pushTag(tag) { + if (!props.project?.path) return + try { + await AppService.PushTag(props.project.path, tag.name) + Modal.success({ title: '推送成功', content: `标签 ${tag.name} 已推送到远程` }) + } catch (e) { + console.error('推送标签失败:', e) + Modal.error({ title: '推送失败', content: String(e) }) + } +} + +// 切换到标签 Tab 时加载标签 +watch(activeTab, (tab) => { + if (tab === 'tags' && !tags.value.length && !tagsLoading.value) { + loadTags() + } +}) @@ -748,6 +836,9 @@ function formatTime(ts) { 历史 + + 标签 + @@ -852,7 +943,7 @@ function formatTime(ts) { - + @@ -994,6 +1085,79 @@ function formatTime(ts) { + + + + + + + + + + 新建标签 + + + + + + + 取消 + 创建 + + + + + + + + + + + + + + 暂无标签 + + + + + + {{ tag.name }} + + + + + + + + + + + + + + + {{ tag.hash }} + {{ tag.message }} + {{ formatTime(tag.timestamp) }} + + + + + + + + + + 刷新标签 + + + + @@ -1023,7 +1187,7 @@ function formatTime(ts) { - + 点击左侧提交查看详情 @@ -1047,6 +1211,14 @@ function formatTime(ts) { + + + + + 在左侧管理项目标签 + 共 {{ tags.length }} 个标签 + + @@ -1583,4 +1755,107 @@ function formatTime(ts) { margin-left: 4px; flex-shrink: 0; } + +/* 标签管理面板 */ +.tags-panel { + display: flex; + flex-direction: column; +} + +.tag-create-box { + padding: 8px 10px; + border-bottom: 1px solid var(--border-color); +} + +.tag-list-section { + flex: 1; + overflow-y: auto; +} + +.tag-item { + padding: 8px 12px; + border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.04)); + transition: background 0.12s; +} + +.tag-item:hover { + background: var(--bg-hover); +} + +.tag-main-row { + display: flex; + align-items: center; + gap: 6px; +} + +.tag-icon { + color: var(--accent, #89b4fa); + font-size: 14px; + flex-shrink: 0; +} + +.tag-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.tag-action-btns { + display: flex; + align-items: center; + gap: 0; + flex-shrink: 0; + visibility: hidden; +} + +.tag-item:hover .tag-action-btns { + visibility: visible; +} + +.tag-action-btn { + width: 24px; + height: 24px; + color: var(--text-muted) !important; +} + +.tag-action-btn.push:hover { + color: var(--accent, #89b4fa) !important; +} + +.tag-action-btn.delete:hover { + color: var(--danger, #f38ba8) !important; +} + +.tag-meta { + display: flex; + align-items: center; + gap: 10px; + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; + padding-left: 20px; +} + +.tag-hash { + font-family: 'Consolas', 'Courier New', monospace; + color: var(--accent, #89b4fa); +} + +.tag-message { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.tag-time { + margin-left: auto; + flex-shrink: 0; +} 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 95bd4c5..02c47ef 100644 --- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js @@ -62,6 +62,27 @@ export function CommitChanges(path, message) { return $Call.ByID(921984546, path, message); } +/** + * CreateTag 创建标签 + * @param {string} path + * @param {string} name + * @param {string} message + * @returns {$CancellablePromise} + */ +export function CreateTag(path, name, message) { + return $Call.ByID(4209616956, path, name, message); +} + +/** + * DeleteTag 删除标签(本地+远程) + * @param {string} path + * @param {string} name + * @returns {$CancellablePromise} + */ +export function DeleteTag(path, name) { + return $Call.ByID(873653241, path, name); +} + /** * DiscardFiles 丢弃工作区指定文件的更改 * @param {string} path @@ -210,6 +231,17 @@ export function GetProjectTree() { })); } +/** + * GetTags 获取所有标签 + * @param {string} path + * @returns {$CancellablePromise<$models.TagInfo[]>} + */ +export function GetTags(path) { + return $Call.ByID(3979169621, path).then(/** @type {($result: any) => any} */(($result) => { + return $$createType15($result); + })); +} + /** * GetUserInfo 获取用户信息 * @param {string} platform @@ -218,7 +250,7 @@ export function GetProjectTree() { */ export function GetUserInfo(platform, username) { return $Call.ByID(2674655707, platform, username).then(/** @type {($result: any) => any} */(($result) => { - return $$createType15($result); + return $$createType17($result); })); } @@ -240,6 +272,16 @@ export function PushProject(path) { return $Call.ByID(3887901113, path); } +/** + * PushTag 推送标签到远程 + * @param {string} path + * @param {string} name + * @returns {$CancellablePromise} + */ +export function PushTag(path, name) { + return $Call.ByID(1424810524, path, name); +} + /** * RemovePlatform 删除平台 * @param {string} name @@ -393,5 +435,7 @@ const $$createType10 = $models.ProjectStatus.createFrom; const $$createType11 = $Create.Nullable($$createType10); const $$createType12 = $models.TreeNode.createFrom; const $$createType13 = $Create.Array($$createType12); -const $$createType14 = $models.UserInfo.createFrom; -const $$createType15 = $Create.Nullable($$createType14); +const $$createType14 = $models.TagInfo.createFrom; +const $$createType15 = $Create.Array($$createType14); +const $$createType16 = $models.UserInfo.createFrom; +const $$createType17 = $Create.Nullable($$createType16); 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 1fb73f0..111b80a 100644 --- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/index.js @@ -14,6 +14,7 @@ export { FileInfo, PlatformInfo, ProjectStatus, + TagInfo, TreeNode, UserInfo } from "./models.js"; 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 a8fc047..62667d0 100644 --- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js +++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js @@ -294,6 +294,58 @@ export class ProjectStatus { } } +/** + * TagInfo 标签信息 + */ +export class TagInfo { + /** + * Creates a new TagInfo instance. + * @param {Partial} [$$source = {}] - The source object to create the TagInfo. + */ + constructor($$source = {}) { + if (!("name" in $$source)) { + /** + * @member + * @type {string} + */ + this["name"] = ""; + } + if (!("hash" in $$source)) { + /** + * @member + * @type {string} + */ + this["hash"] = ""; + } + if (!("timestamp" in $$source)) { + /** + * @member + * @type {number} + */ + this["timestamp"] = 0; + } + if (!("message" in $$source)) { + /** + * @member + * @type {string} + */ + this["message"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new TagInfo instance from a string or object. + * @param {any} [$$source = {}] + * @returns {TagInfo} + */ + static createFrom($$source = {}) { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new TagInfo(/** @type {Partial} */($$parsedSource)); + } +} + /** * TreeNode 前端侧边栏树节点 */ diff --git a/internal/app/service.go b/internal/app/service.go index c7c17a0..2bc52b8 100644 --- a/internal/app/service.go +++ b/internal/app/service.go @@ -701,3 +701,97 @@ func (s *AppService) SwitchBranch(path, branch string) error { _, err := s.gitClient.Checkout(path, strings.TrimSpace(branch)) return err } + +// TagInfo 标签信息 +type TagInfo struct { + Name string `json:"name"` + Hash string `json:"hash"` + Timestamp int64 `json:"timestamp"` + Message string `json:"message"` +} + +// GetTags 获取所有标签 +func (s *AppService) GetTags(path string) ([]TagInfo, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, fmt.Errorf("项目路径不存在: %s", path) + } + out, err := s.gitClient.TagList(path) + if err != nil { + return nil, fmt.Errorf("获取标签列表失败: %w", err) + } + var tags []TagInfo + for _, line := range strings.Split(strings.TrimSpace(out), "\n") { + if strings.TrimSpace(line) == "" { + continue + } + parts := strings.SplitN(line, "\t", 5) + if len(parts) < 3 { + continue + } + var ts int64 + fmt.Sscanf(parts[2], "%d", &ts) + hash := parts[1] + // 注释标签的实际提交哈希在 *objectname + if len(parts) > 3 && parts[3] != "" { + hash = parts[3] + } + message := "" + if len(parts) > 4 { + message = parts[4] + } + tags = append(tags, TagInfo{ + Name: parts[0], + Hash: hash, + Timestamp: ts, + Message: message, + }) + } + return tags, nil +} + +// CreateTag 创建标签 +func (s *AppService) CreateTag(path, name, message string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return fmt.Errorf("项目路径不存在: %s", path) + } + name = strings.TrimSpace(name) + if name == "" { + return fmt.Errorf("标签名不能为空") + } + if strings.TrimSpace(message) == "" { + message = name + } + _, err := s.gitClient.CreateTag(path, name, message) + return err +} + +// DeleteTag 删除标签(本地+远程) +func (s *AppService) DeleteTag(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("标签名不能为空") + } + // 删除本地标签 + if _, err := s.gitClient.DeleteTag(path, name); err != nil { + return fmt.Errorf("删除本地标签失败: %w", err) + } + // 尝试删除远程标签(忽略错误,可能未推送过) + s.gitClient.DeleteRemoteTag(path, name) + return nil +} + +// PushTag 推送标签到远程 +func (s *AppService) PushTag(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.PushTag(path, name) + return err +} diff --git a/internal/git/client.go b/internal/git/client.go index a47a9d2..0d8330a 100644 --- a/internal/git/client.go +++ b/internal/git/client.go @@ -129,6 +129,32 @@ func (g *GitClient) RevertCommit(path, hash string) (string, error) { return g.Run(path, "revert", "--no-edit", hash) } +// TagList 获取所有标签(按版本号降序,带创建时间和提交哈希) +func (g *GitClient) TagList(path string) (string, error) { + return g.Run(path, "tag", "-l", "--sort=-version:refname", + "--format=%(refname:short)\t%(objectname:short)\t%(creatordate:unix)\t%(*objectname:short)\t%(contents:subject)") +} + +// CreateTag 创建注释标签 +func (g *GitClient) CreateTag(path, name, message string) (string, error) { + return g.Run(path, "tag", "-a", name, "-m", message) +} + +// DeleteTag 删除本地标签 +func (g *GitClient) DeleteTag(path, name string) (string, error) { + return g.Run(path, "tag", "-d", name) +} + +// PushTag 推送标签到远程 +func (g *GitClient) PushTag(path, name string) (string, error) { + return g.Run(path, "push", "origin", name) +} + +// DeleteRemoteTag 删除远程标签 +func (g *GitClient) DeleteRemoteTag(path, name string) (string, error) { + return g.Run(path, "push", "origin", "--delete", name) +} + // BranchList 获取所有本地分支 func (g *GitClient) BranchList(path string) (string, error) { return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)")