diff --git a/package.json b/package.json index d0233d0..c5a3b3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hyper-git-agentic-git", - "displayName": "Hyper Git - Agentic Git", + "displayName": "Hyper Git", "icon": "media/icon.png", "description": "为 VS Code 提供统一的 Git 变更管理与提交工作流:多变更分组、自绘提交面板、可视化提交图、Shelf、行级提交,并为未来 AI Agent 自主代理预留架构接缝。", "version": "0.0.6", @@ -96,151 +96,156 @@ "viewsWelcome": [ { "view": "hyperGit.changes", - "contents": "未检测到 Git 变更或仓库。\n在 Git 仓库中打开工作区,变更将按 Changelist 分组显示。\n[新建 Changelist](command:hyperGit.newChangelist)" + "contents": "No Git changes or repository detected.\nOpen a workspace inside a Git repository and changes will be grouped by changelist.\n[New Changelist…](command:hyperGit.newChangelist)" + }, + { + "view": "hyperGit.branches", + "contents": "No Git repository or branches detected.\n[New Branch…](command:hyperGit.branchCreate)" }, { "view": "hyperGit.stash", - "contents": "暂无 Stash。\n[Stash 当前变更](command:hyperGit.stashCreate)" + "contents": "No stashes yet.\n[Stash Current Changes](command:hyperGit.stashCreate)" + }, + { + "view": "hyperGit.shelf", + "contents": "No shelves yet. Shelves store patches independently of stash.\n[Shelve Changes…](command:hyperGit.shelveChanges)" }, { "view": "hyperGit.worktrees", - "contents": "暂无 Worktree。\n[新建 Worktree](command:hyperGit.worktreeAdd)" + "contents": "No worktrees.\n[New Worktree…](command:hyperGit.worktreeAdd)" } ], "commands": [ - { "command": "hyperGit.showVersion", "title": "显示版本", "category": "Hyper Git" }, - { "command": "hyperGit.refresh", "title": "刷新", "category": "Hyper Git", "icon": "$(refresh)" }, - { "command": "hyperGit.newChangelist", "title": "新建 Changelist", "category": "Hyper Git", "icon": "$(add)" }, - { "command": "hyperGit.setActiveChangelist", "title": "设为活动 Changelist", "category": "Hyper Git" }, - { "command": "hyperGit.renameChangelist", "title": "重命名 Changelist", "category": "Hyper Git" }, - { "command": "hyperGit.deleteChangelist", "title": "删除 Changelist", "category": "Hyper Git" }, - { "command": "hyperGit.moveChangelist", "title": "移至 Changelist…", "category": "Hyper Git" }, - { "command": "hyperGit.openDiff", "title": "打开 Diff", "category": "Hyper Git" }, - { "command": "hyperGit.commit", "title": "提交", "category": "Hyper Git", "icon": "$(check)" }, - { "command": "hyperGit.commitAndPush", "title": "提交并推送", "category": "Hyper Git", "icon": "$(cloud-upload)" }, - { "command": "hyperGit.refreshLog", "title": "刷新 Log", "category": "Hyper Git", "icon": "$(refresh)" }, - { "command": "hyperGit.ci.signIn", "title": "登录 GitHub 查看 CI 状态", "category": "Hyper Git" }, - { "command": "hyperGit.refreshBranches", "title": "刷新 Branches", "category": "Hyper Git", "icon": "$(refresh)" }, - { "command": "hyperGit.logFilterAuthor", "title": "按作者过滤", "category": "Hyper Git" }, - { "command": "hyperGit.logFilterPath", "title": "按路径过滤", "category": "Hyper Git" }, - { "command": "hyperGit.logClearFilter", "title": "清除 Log 过滤", "category": "Hyper Git" }, - { "command": "hyperGit.copyCommitHash", "title": "复制 Commit Hash", "category": "Hyper Git" }, - { "command": "hyperGit.showHistory", "title": "显示文件历史", "category": "Hyper Git" }, - { "command": "hyperGit.branchCreate", "title": "新建分支", "category": "Hyper Git", "icon": "$(add)" }, - { "command": "hyperGit.branchCheckout", "title": "检出分支", "category": "Hyper Git" }, - { "command": "hyperGit.branchDelete", "title": "删除分支", "category": "Hyper Git" }, - { "command": "hyperGit.deleteRemoteBranch", "title": "删除远程分支", "category": "Hyper Git" }, - { "command": "hyperGit.mergeBranch", "title": "合并到当前分支", "category": "Hyper Git" }, - { "command": "hyperGit.rebaseBranch", "title": "变基当前分支到…", "category": "Hyper Git" }, - { "command": "hyperGit.showBlame", "title": "显示 Blame", "category": "Hyper Git" }, - { "command": "hyperGit.stashCreate", "title": "Stash 变更", "category": "Hyper Git", "icon": "$(archive)" }, - { "command": "hyperGit.stashApply", "title": "应用 Stash", "category": "Hyper Git" }, - { "command": "hyperGit.stashPop", "title": "Pop Stash", "category": "Hyper Git" }, - { "command": "hyperGit.stashDrop", "title": "删除 Stash", "category": "Hyper Git" }, - { "command": "hyperGit.discardChanges", "title": "丢弃改动", "category": "Hyper Git" }, + { "command": "hyperGit.showVersion", "title": "Show Version", "category": "Hyper Git" }, + { "command": "hyperGit.refresh", "title": "Refresh", "category": "Hyper Git", "icon": "$(refresh)" }, + { "command": "hyperGit.newChangelist", "title": "New Changelist…", "category": "Hyper Git", "icon": "$(add)" }, + { "command": "hyperGit.setActiveChangelist", "title": "Set Active Changelist", "category": "Hyper Git" }, + { "command": "hyperGit.renameChangelist", "title": "Rename Changelist", "category": "Hyper Git" }, + { "command": "hyperGit.deleteChangelist", "title": "Delete Changelist", "category": "Hyper Git" }, + { "command": "hyperGit.moveChangelist", "title": "Move to Changelist…", "category": "Hyper Git" }, + { "command": "hyperGit.openDiff", "title": "Open Diff", "category": "Hyper Git" }, + { "command": "hyperGit.commit", "title": "Commit", "category": "Hyper Git", "icon": "$(check)" }, + { "command": "hyperGit.commitAndPush", "title": "Commit & Push", "category": "Hyper Git", "icon": "$(cloud-upload)" }, + { "command": "hyperGit.refreshLog", "title": "Refresh Log", "category": "Hyper Git", "icon": "$(refresh)" }, + { "command": "hyperGit.ci.signIn", "title": "Sign In to GitHub for CI Status", "category": "Hyper Git", "icon": "$(sign-in)" }, + { "command": "hyperGit.refreshBranches", "title": "Refresh Branches", "category": "Hyper Git", "icon": "$(refresh)" }, + { "command": "hyperGit.logFilter", "title": "Filter Log…", "category": "Hyper Git", "icon": "$(filter)" }, + { "command": "hyperGit.logFilterAuthor", "title": "Filter by Author…", "category": "Hyper Git" }, + { "command": "hyperGit.logFilterPath", "title": "Filter by Path…", "category": "Hyper Git" }, + { "command": "hyperGit.logClearFilter", "title": "Clear Log Filter", "category": "Hyper Git", "icon": "$(clear-all)" }, + { "command": "hyperGit.copyCommitHash", "title": "Copy Commit Hash", "category": "Hyper Git" }, + { "command": "hyperGit.showHistory", "title": "Show File History", "category": "Hyper Git" }, + { "command": "hyperGit.branchCreate", "title": "New Branch…", "category": "Hyper Git", "icon": "$(add)" }, + { "command": "hyperGit.branchCheckout", "title": "Checkout Branch", "category": "Hyper Git", "icon": "$(check)" }, + { "command": "hyperGit.branchDelete", "title": "Delete Branch", "category": "Hyper Git", "icon": "$(trash)" }, + { "command": "hyperGit.deleteRemoteBranch", "title": "Delete Remote Branch", "category": "Hyper Git", "icon": "$(trash)" }, + { "command": "hyperGit.mergeBranch", "title": "Merge into Current Branch", "category": "Hyper Git", "icon": "$(git-merge)" }, + { "command": "hyperGit.rebaseBranch", "title": "Rebase Current Branch onto…", "category": "Hyper Git", "icon": "$(call-out)" }, + { "command": "hyperGit.showBlame", "title": "Show Blame", "category": "Hyper Git" }, + { "command": "hyperGit.stashCreate", "title": "Stash Changes", "category": "Hyper Git", "icon": "$(archive)" }, + { "command": "hyperGit.stashApply", "title": "Apply Stash", "category": "Hyper Git", "icon": "$(archive)" }, + { "command": "hyperGit.stashPop", "title": "Pop Stash", "category": "Hyper Git", "icon": "$(arrow-down)" }, + { "command": "hyperGit.stashDrop", "title": "Drop Stash", "category": "Hyper Git", "icon": "$(trash)" }, + { "command": "hyperGit.stashView", "title": "View Stash Diff", "category": "Hyper Git" }, + { "command": "hyperGit.discardChanges", "title": "Discard Changes", "category": "Hyper Git", "icon": "$(discard)" }, { "command": "hyperGit.pull", "title": "Pull", "category": "Hyper Git", "icon": "$(arrow-down)" }, { "command": "hyperGit.push", "title": "Push", "category": "Hyper Git", "icon": "$(arrow-up)" }, { "command": "hyperGit.fetch", "title": "Fetch", "category": "Hyper Git", "icon": "$(repo-fetch)" }, { "command": "hyperGit.cherryPick", "title": "Cherry-Pick", "category": "Hyper Git", "icon": "$(repo-pull)" }, { "command": "hyperGit.revertCommit", "title": "Revert Commit", "category": "Hyper Git", "icon": "$(discard)" }, { "command": "hyperGit.resetHead", "title": "Reset HEAD…", "category": "Hyper Git", "icon": "$(history)" }, - { "command": "hyperGit.branchRename", "title": "重命名分支", "category": "Hyper Git" }, - { "command": "hyperGit.ignorePath", "title": "添加到 .gitignore", "category": "Hyper Git" }, - { "command": "hyperGit.compareBranches", "title": "比较分支", "category": "Hyper Git" }, - { "command": "hyperGit.toggleFavorite", "title": "收藏/取消收藏", "category": "Hyper Git", "icon": "$(star-full)" }, - { "command": "hyperGit.checkoutAsNew", "title": "从选中分支新建并检出", "category": "Hyper Git" }, - { "command": "hyperGit.compareWithCurrent", "title": "与当前分支比较", "category": "Hyper Git", "icon": "$(compare)" }, - { "command": "hyperGit.tagCreate", "title": "新建标签", "category": "Hyper Git", "icon": "$(tag)" }, - { "command": "hyperGit.tagDelete", "title": "删除标签", "category": "Hyper Git" }, - { "command": "hyperGit.tagCheckout", "title": "检出标签", "category": "Hyper Git" }, - { "command": "hyperGit.openCommitFileDiff", "title": "打开提交文件 Diff", "category": "Hyper Git" }, - { "command": "hyperGit.resetToHere", "title": "Reset 当前分支到此提交…", "category": "Hyper Git", "icon": "$(history)" }, - { "command": "hyperGit.createBranchFromCommit", "title": "从此提交新建分支", "category": "Hyper Git", "icon": "$(git-branch)" }, - { "command": "hyperGit.createTagFromCommit", "title": "从此提交新建标签", "category": "Hyper Git", "icon": "$(tag)" }, - { "command": "hyperGit.showContainingBranches", "title": "查看包含此提交的分支", "category": "Hyper Git" }, - { "command": "hyperGit.logFilterGrep", "title": "按 message 过滤(grep)", "category": "Hyper Git" }, - { "command": "hyperGit.logFilterMergeMode", "title": "合并提交过滤", "category": "Hyper Git" }, - { "command": "hyperGit.logFilterDate", "title": "按日期过滤", "category": "Hyper Git" }, - { "command": "hyperGit.logFilterRegex", "title": "按 message 正则过滤", "category": "Hyper Git" }, - { "command": "hyperGit.rewordCommit", "title": "改写最新提交信息", "category": "Hyper Git" }, - { "command": "hyperGit.showConsole", "title": "打开 Console", "category": "Hyper Git" }, - { "command": "hyperGit.partialStage", "title": "部分暂存(Hunk)…", "category": "Hyper Git" }, - { "command": "hyperGit.partialUnstage", "title": "部分取消暂存(Hunk)…", "category": "Hyper Git" }, - { "command": "hyperGit.stageHunkAtCursor", "title": "暂存光标处 Hunk", "category": "Hyper Git" }, - { "command": "hyperGit.undoLastCommit", "title": "撤销最近提交(soft)", "category": "Hyper Git" }, - { "command": "hyperGit.dropCommit", "title": "删除提交(rebase)", "category": "Hyper Git" }, - { "command": "hyperGit.fixupCommit", "title": "Fixup 到提交(autosquash)", "category": "Hyper Git" }, - { "command": "hyperGit.cleanupBranches", "title": "清理已合并分支", "category": "Hyper Git" }, + { "command": "hyperGit.branchRename", "title": "Rename Branch", "category": "Hyper Git", "icon": "$(edit)" }, + { "command": "hyperGit.ignorePath", "title": "Add to .gitignore", "category": "Hyper Git", "icon": "$(eye-closed)" }, + { "command": "hyperGit.compareBranches", "title": "Compare Branches", "category": "Hyper Git" }, + { "command": "hyperGit.toggleFavorite", "title": "Toggle Favorite", "category": "Hyper Git", "icon": "$(star-full)" }, + { "command": "hyperGit.checkoutAsNew", "title": "Checkout as New Branch…", "category": "Hyper Git" }, + { "command": "hyperGit.compareWithCurrent", "title": "Compare with Current Branch", "category": "Hyper Git", "icon": "$(compare)" }, + { "command": "hyperGit.tagCreate", "title": "New Tag…", "category": "Hyper Git", "icon": "$(tag)" }, + { "command": "hyperGit.tagDelete", "title": "Delete Tag", "category": "Hyper Git", "icon": "$(trash)" }, + { "command": "hyperGit.tagCheckout", "title": "Checkout Tag", "category": "Hyper Git" }, + { "command": "hyperGit.openCommitFileDiff", "title": "Open Commit File Diff", "category": "Hyper Git" }, + { "command": "hyperGit.resetToHere", "title": "Reset Current Branch to Here…", "category": "Hyper Git", "icon": "$(history)" }, + { "command": "hyperGit.createBranchFromCommit", "title": "Create Branch from Commit", "category": "Hyper Git", "icon": "$(git-branch)" }, + { "command": "hyperGit.createTagFromCommit", "title": "Create Tag from Commit", "category": "Hyper Git", "icon": "$(tag)" }, + { "command": "hyperGit.showContainingBranches", "title": "Show Branches Containing Commit", "category": "Hyper Git" }, + { "command": "hyperGit.logFilterGrep", "title": "Filter by Message (grep)…", "category": "Hyper Git" }, + { "command": "hyperGit.logFilterMergeMode", "title": "Filter Merge Commits…", "category": "Hyper Git" }, + { "command": "hyperGit.logFilterDate", "title": "Filter by Date…", "category": "Hyper Git" }, + { "command": "hyperGit.logFilterRegex", "title": "Filter by Message (regex)…", "category": "Hyper Git" }, + { "command": "hyperGit.rewordCommit", "title": "Reword Latest Commit", "category": "Hyper Git", "icon": "$(edit)" }, + { "command": "hyperGit.showConsole", "title": "Open Console", "category": "Hyper Git", "icon": "$(output)" }, + { "command": "hyperGit.partialStage", "title": "Stage Hunk…", "category": "Hyper Git" }, + { "command": "hyperGit.partialUnstage", "title": "Unstage Hunk…", "category": "Hyper Git" }, + { "command": "hyperGit.stageHunkAtCursor", "title": "Stage Hunk at Cursor", "category": "Hyper Git" }, + { "command": "hyperGit.undoLastCommit", "title": "Undo Last Commit (soft)", "category": "Hyper Git", "icon": "$(discard)" }, + { "command": "hyperGit.dropCommit", "title": "Drop Commit (rebase)", "category": "Hyper Git", "icon": "$(trash)" }, + { "command": "hyperGit.fixupCommit", "title": "Fixup Commit (autosquash)", "category": "Hyper Git" }, + { "command": "hyperGit.cleanupBranches", "title": "Cleanup Merged Branches", "category": "Hyper Git", "icon": "$(clear-all)" }, { "command": "hyperGit.pushDialog", "title": "Push…", "category": "Hyper Git", "icon": "$(repo-push)" }, { "command": "hyperGit.updateProject", "title": "Update Project…", "category": "Hyper Git", "icon": "$(sync)" }, { "command": "hyperGit.mergeDialog", "title": "Merge…", "category": "Hyper Git", "icon": "$(git-merge)" }, - { "command": "hyperGit.resolveConflicts", "title": "解决冲突(3-way Merge Editor)…", "category": "Hyper Git", "icon": "$(git-merge)" }, - { "command": "hyperGit.acceptOurs", "title": "冲突文件:采用 Ours", "category": "Hyper Git" }, - { "command": "hyperGit.acceptTheirs", "title": "冲突文件:采用 Theirs", "category": "Hyper Git" }, - { "command": "hyperGit.copyBranchRef", "title": "复制分支引用", "category": "Hyper Git" }, - { "command": "hyperGit.threeWayDiff", "title": "3-way Diff 概览", "category": "Hyper Git" }, - { "command": "hyperGit.inlineCommitHunk", "title": "行内提交 Hunk", "category": "Hyper Git" }, + { "command": "hyperGit.resolveConflicts", "title": "Resolve Conflicts (3-way Merge Editor)…", "category": "Hyper Git", "icon": "$(git-merge)" }, + { "command": "hyperGit.acceptOurs", "title": "Accept Ours (Conflict)", "category": "Hyper Git" }, + { "command": "hyperGit.acceptTheirs", "title": "Accept Theirs (Conflict)", "category": "Hyper Git" }, + { "command": "hyperGit.copyBranchRef", "title": "Copy Branch Reference", "category": "Hyper Git" }, + { "command": "hyperGit.threeWayDiff", "title": "3-way Diff Overview", "category": "Hyper Git" }, + { "command": "hyperGit.inlineCommitHunk", "title": "Inline Commit Hunk", "category": "Hyper Git" }, { "command": "hyperGit.shelveChanges", "title": "Shelve Changes…", "category": "Hyper Git", "icon": "$(inbox)" }, { "command": "hyperGit.unshelveSilently", "title": "Unshelve Silently", "category": "Hyper Git" }, { "command": "hyperGit.unshelveWithMerge", "title": "Unshelve with Merge", "category": "Hyper Git" }, - { "command": "hyperGit.deleteShelf", "title": "Delete Shelf", "category": "Hyper Git" }, - { "command": "hyperGit.startRebase", "title": "Interactive Rebase…", "category": "Hyper Git" }, + { "command": "hyperGit.deleteShelf", "title": "Delete Shelf", "category": "Hyper Git", "icon": "$(trash)" }, + { "command": "hyperGit.startRebase", "title": "Interactive Rebase…", "category": "Hyper Git", "icon": "$(call-out)" }, { "command": "hyperGit.moveHunkToChangelist", "title": "Move Hunk to Changelist", "category": "Hyper Git" }, - { "command": "hyperGit.stashKeepIndex", "title": "Stash(保留已暂存)", "category": "Hyper Git" }, - { "command": "hyperGit.stashClear", "title": "清空所有 Stash", "category": "Hyper Git" }, - { "command": "hyperGit.stashBranch", "title": "从 Stash 创建分支", "category": "Hyper Git" }, - { "command": "hyperGit.createPatch", "title": "导出 Patch…", "category": "Hyper Git", "icon": "$(diff)" }, - { "command": "hyperGit.applyPatch", "title": "应用 Patch…", "category": "Hyper Git", "icon": "$(diff)" }, - { "command": "hyperGit.showReflog", "title": "查看 Reflog", "category": "Hyper Git", "icon": "$(history)" }, - { "command": "hyperGit.toggleBlameAnnotation", "title": "切换 Blame 注解", "category": "Hyper Git", "icon": "$(eye)" }, - { "command": "hyperGit.worktreeRefresh", "title": "刷新 Worktrees", "category": "Hyper Git", "icon": "$(refresh)" }, - { "command": "hyperGit.worktreeAdd", "title": "新建 Worktree", "category": "Hyper Git", "icon": "$(add)" }, - { "command": "hyperGit.worktreeOpen", "title": "打开 Worktree", "category": "Hyper Git", "icon": "$(folder-opened)" }, - { "command": "hyperGit.worktreeRemove", "title": "删除 Worktree", "category": "Hyper Git", "icon": "$(trash)" }, - { "command": "hyperGit.worktreeCopyPath", "title": "复制 Worktree 路径", "category": "Hyper Git" }, - { "command": "hyperGit.worktreeLock", "title": "锁定 Worktree", "category": "Hyper Git", "icon": "$(lock)" }, - { "command": "hyperGit.worktreeUnlock", "title": "解锁 Worktree", "category": "Hyper Git" }, - { "command": "hyperGit.worktreeMove", "title": "移动 Worktree…", "category": "Hyper Git" }, - { "command": "hyperGit.worktreePrune", "title": "清理失效 Worktree", "category": "Hyper Git", "icon": "$(clear-all)" } + { "command": "hyperGit.stashKeepIndex", "title": "Stash (Keep Index)", "category": "Hyper Git" }, + { "command": "hyperGit.stashClear", "title": "Clear All Stashes", "category": "Hyper Git", "icon": "$(clear-all)" }, + { "command": "hyperGit.stashBranch", "title": "Create Branch from Stash", "category": "Hyper Git" }, + { "command": "hyperGit.createPatch", "title": "Create Patch…", "category": "Hyper Git", "icon": "$(diff)" }, + { "command": "hyperGit.applyPatch", "title": "Apply Patch…", "category": "Hyper Git", "icon": "$(diff)" }, + { "command": "hyperGit.showReflog", "title": "Show Reflog", "category": "Hyper Git", "icon": "$(clock)" }, + { "command": "hyperGit.toggleBlameAnnotation", "title": "Toggle Blame Annotation", "category": "Hyper Git", "icon": "$(eye)" }, + { "command": "hyperGit.worktreeRefresh", "title": "Refresh Worktrees", "category": "Hyper Git", "icon": "$(refresh)" }, + { "command": "hyperGit.worktreeAdd", "title": "New Worktree…", "category": "Hyper Git", "icon": "$(add)" }, + { "command": "hyperGit.worktreeOpen", "title": "Open Worktree", "category": "Hyper Git", "icon": "$(folder-opened)" }, + { "command": "hyperGit.worktreeRemove", "title": "Remove Worktree", "category": "Hyper Git", "icon": "$(trash)" }, + { "command": "hyperGit.worktreeCopyPath", "title": "Copy Worktree Path", "category": "Hyper Git" }, + { "command": "hyperGit.worktreeLock", "title": "Lock Worktree", "category": "Hyper Git", "icon": "$(lock)" }, + { "command": "hyperGit.worktreeUnlock", "title": "Unlock Worktree", "category": "Hyper Git" }, + { "command": "hyperGit.worktreeMove", "title": "Move Worktree…", "category": "Hyper Git" }, + { "command": "hyperGit.worktreePrune", "title": "Prune Stale Worktrees", "category": "Hyper Git", "icon": "$(clear-all)" } ], "menus": { "view/title": [ - { "command": "hyperGit.refresh", "when": "view == hyperGit.changes", "group": "navigation" }, - { "command": "hyperGit.newChangelist", "when": "view == hyperGit.changes", "group": "navigation" }, - { "command": "hyperGit.commit", "when": "view == hyperGit.changes", "group": "navigation" }, - { "command": "hyperGit.commitAndPush", "when": "view == hyperGit.changes", "group": "navigation" }, - { "command": "hyperGit.fetch", "when": "view == hyperGit.changes", "group": "navigation" }, - { "command": "hyperGit.pull", "when": "view == hyperGit.changes", "group": "navigation" }, - { "command": "hyperGit.push", "when": "view == hyperGit.changes", "group": "navigation" }, - { "command": "hyperGit.pushDialog", "when": "view == hyperGit.changes", "group": "navigation" }, - { "command": "hyperGit.updateProject", "when": "view == hyperGit.changes", "group": "navigation" }, - { "command": "hyperGit.createPatch", "when": "view == hyperGit.changes" }, - { "command": "hyperGit.applyPatch", "when": "view == hyperGit.changes" }, - { "command": "hyperGit.refreshLog", "when": "view == hyperGit.log", "group": "navigation" }, - { "command": "hyperGit.resetHead", "when": "view == hyperGit.log", "group": "navigation" }, - { "command": "hyperGit.logFilterAuthor", "when": "view == hyperGit.log" }, - { "command": "hyperGit.logFilterPath", "when": "view == hyperGit.log" }, - { "command": "hyperGit.logClearFilter", "when": "view == hyperGit.log" }, - { "command": "hyperGit.logFilterGrep", "when": "view == hyperGit.log" }, - { "command": "hyperGit.logFilterMergeMode", "when": "view == hyperGit.log" }, - { "command": "hyperGit.logFilterDate", "when": "view == hyperGit.log" }, - { "command": "hyperGit.logFilterRegex", "when": "view == hyperGit.log" }, - { "command": "hyperGit.showReflog", "when": "view == hyperGit.log", "group": "navigation" }, - { "command": "hyperGit.refreshBranches", "when": "view == hyperGit.branches", "group": "navigation" }, - { "command": "hyperGit.branchCreate", "when": "view == hyperGit.branches", "group": "navigation" }, - { "command": "hyperGit.pull", "when": "view == hyperGit.branches", "group": "navigation" }, - { "command": "hyperGit.push", "when": "view == hyperGit.branches", "group": "navigation" }, - { "command": "hyperGit.fetch", "when": "view == hyperGit.branches", "group": "navigation" }, - { "command": "hyperGit.mergeDialog", "when": "view == hyperGit.branches", "group": "navigation" }, - { "command": "hyperGit.updateProject", "when": "view == hyperGit.branches", "group": "navigation" }, - { "command": "hyperGit.tagCreate", "when": "view == hyperGit.branches", "group": "navigation" }, - { "command": "hyperGit.stashCreate", "when": "view == hyperGit.stash", "group": "navigation" }, - { "command": "hyperGit.stashKeepIndex", "when": "view == hyperGit.stash", "group": "navigation" }, - { "command": "hyperGit.stashClear", "when": "view == hyperGit.stash" }, - { "command": "hyperGit.shelveChanges", "when": "view == hyperGit.shelf", "group": "navigation" }, - { "command": "hyperGit.worktreeRefresh", "when": "view == hyperGit.worktrees", "group": "navigation" }, - { "command": "hyperGit.worktreeAdd", "when": "view == hyperGit.worktrees", "group": "navigation" }, - { "command": "hyperGit.worktreePrune", "when": "view == hyperGit.worktrees", "group": "navigation" } + { "command": "hyperGit.refresh", "when": "view == hyperGit.changes", "group": "navigation@1" }, + { "command": "hyperGit.newChangelist", "when": "view == hyperGit.changes", "group": "navigation@2" }, + { "command": "hyperGit.commit", "when": "view == hyperGit.changes", "group": "navigation@3" }, + { "command": "hyperGit.commitAndPush", "when": "view == hyperGit.changes", "group": "navigation@4" }, + { "command": "hyperGit.push", "when": "view == hyperGit.changes", "group": "navigation@5" }, + { "command": "hyperGit.pull", "when": "view == hyperGit.changes", "group": "1_sync@1" }, + { "command": "hyperGit.fetch", "when": "view == hyperGit.changes", "group": "1_sync@2" }, + { "command": "hyperGit.pushDialog", "when": "view == hyperGit.changes", "group": "1_sync@3" }, + { "command": "hyperGit.updateProject", "when": "view == hyperGit.changes", "group": "1_sync@4" }, + { "command": "hyperGit.createPatch", "when": "view == hyperGit.changes", "group": "1_sync@5" }, + { "command": "hyperGit.applyPatch", "when": "view == hyperGit.changes", "group": "1_sync@6" }, + { "command": "hyperGit.refreshLog", "when": "view == hyperGit.log", "group": "navigation@1" }, + { "command": "hyperGit.logFilter", "when": "view == hyperGit.log", "group": "navigation@2" }, + { "command": "hyperGit.logClearFilter", "when": "view == hyperGit.log", "group": "navigation@3" }, + { "command": "hyperGit.showReflog", "when": "view == hyperGit.log", "group": "navigation@4" }, + { "command": "hyperGit.resetHead", "when": "view == hyperGit.log", "group": "1_sync@1" }, + { "command": "hyperGit.refreshBranches", "when": "view == hyperGit.branches", "group": "navigation@1" }, + { "command": "hyperGit.branchCreate", "when": "view == hyperGit.branches", "group": "navigation@2" }, + { "command": "hyperGit.pull", "when": "view == hyperGit.branches", "group": "navigation@3" }, + { "command": "hyperGit.push", "when": "view == hyperGit.branches", "group": "navigation@4" }, + { "command": "hyperGit.mergeDialog", "when": "view == hyperGit.branches", "group": "navigation@5" }, + { "command": "hyperGit.fetch", "when": "view == hyperGit.branches", "group": "1_sync@1" }, + { "command": "hyperGit.updateProject", "when": "view == hyperGit.branches", "group": "1_sync@2" }, + { "command": "hyperGit.tagCreate", "when": "view == hyperGit.branches", "group": "1_sync@3" }, + { "command": "hyperGit.stashCreate", "when": "view == hyperGit.stash", "group": "navigation@1" }, + { "command": "hyperGit.stashKeepIndex", "when": "view == hyperGit.stash", "group": "navigation@2" }, + { "command": "hyperGit.stashClear", "when": "view == hyperGit.stash", "group": "1_sync@1" }, + { "command": "hyperGit.shelveChanges", "when": "view == hyperGit.shelf", "group": "navigation@1" }, + { "command": "hyperGit.worktreeRefresh", "when": "view == hyperGit.worktrees", "group": "navigation@1" }, + { "command": "hyperGit.worktreeAdd", "when": "view == hyperGit.worktrees", "group": "navigation@2" }, + { "command": "hyperGit.worktreePrune", "when": "view == hyperGit.worktrees", "group": "navigation@3" } ], "view/item/context": [ { "command": "hyperGit.setActiveChangelist", "when": "view == hyperGit.changes && viewItem == hyperGit.changelist", "group": "1_changelist@1" }, @@ -249,10 +254,10 @@ { "command": "hyperGit.moveChangelist", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "inline" }, { "command": "hyperGit.moveChangelist", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "1_file@1" }, { "command": "hyperGit.showHistory", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "1_file@2" }, - { "command": "hyperGit.discardChanges", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "1_file@3" }, + { "command": "hyperGit.discardChanges", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "9_dangerous@1" }, { "command": "hyperGit.branchCheckout", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/ && !listMultiSelection", "group": "1_branch@1" }, - { "command": "hyperGit.branchDelete", "when": "view == hyperGit.branches && viewItem == hyperGit.branch", "group": "1_branch@2" }, - { "command": "hyperGit.deleteRemoteBranch", "when": "view == hyperGit.branches && viewItem == hyperGit.remoteBranch", "group": "1_branch@2" }, + { "command": "hyperGit.branchDelete", "when": "view == hyperGit.branches && viewItem == hyperGit.branch", "group": "9_dangerous@1" }, + { "command": "hyperGit.deleteRemoteBranch", "when": "view == hyperGit.branches && viewItem == hyperGit.remoteBranch", "group": "9_dangerous@2" }, { "command": "hyperGit.mergeBranch", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/ && !listMultiSelection", "group": "1_branch@3" }, { "command": "hyperGit.rebaseBranch", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/ && !listMultiSelection", "group": "1_branch@4" }, { "command": "hyperGit.branchRename", "when": "view == hyperGit.branches && viewItem == hyperGit.branch && !listMultiSelection", "group": "1_branch@5" }, @@ -263,23 +268,23 @@ { "command": "hyperGit.checkoutAsNew", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/ && !listMultiSelection", "group": "1_branch@1.5" }, { "command": "hyperGit.compareWithCurrent", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch|hyperGit.tag/ && !listMultiSelection", "group": "1_branch@8" }, { "command": "hyperGit.tagCheckout", "when": "view == hyperGit.branches && viewItem == hyperGit.tag && !listMultiSelection", "group": "1_tag@1" }, - { "command": "hyperGit.tagDelete", "when": "view == hyperGit.branches && viewItem == hyperGit.tag", "group": "1_tag@2" }, + { "command": "hyperGit.tagDelete", "when": "view == hyperGit.branches && viewItem == hyperGit.tag", "group": "9_dangerous@1" }, { "command": "hyperGit.ignorePath", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "1_file@4" }, { "command": "hyperGit.partialStage", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "1_file@5" }, { "command": "hyperGit.partialUnstage", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "1_file@6" }, { "command": "hyperGit.stashApply", "when": "view == hyperGit.stash && viewItem == hyperGit.stash", "group": "1_stash@1" }, { "command": "hyperGit.stashPop", "when": "view == hyperGit.stash && viewItem == hyperGit.stash", "group": "1_stash@2" }, - { "command": "hyperGit.stashDrop", "when": "view == hyperGit.stash && viewItem == hyperGit.stash", "group": "1_stash@3" }, + { "command": "hyperGit.stashDrop", "when": "view == hyperGit.stash && viewItem == hyperGit.stash", "group": "9_dangerous@1" }, { "command": "hyperGit.stashBranch", "when": "view == hyperGit.stash && viewItem == hyperGit.stash", "group": "1_stash@4" }, { "command": "hyperGit.unshelveSilently", "when": "view == hyperGit.shelf && viewItem == hyperGit.shelf", "group": "1_shelf@1" }, { "command": "hyperGit.unshelveWithMerge", "when": "view == hyperGit.shelf && viewItem == hyperGit.shelf", "group": "1_shelf@2" }, - { "command": "hyperGit.deleteShelf", "when": "view == hyperGit.shelf && viewItem == hyperGit.shelf", "group": "1_shelf@3" }, + { "command": "hyperGit.deleteShelf", "when": "view == hyperGit.shelf && viewItem == hyperGit.shelf", "group": "9_dangerous@1" }, { "command": "hyperGit.worktreeOpen", "when": "view == hyperGit.worktrees && viewItem =~ /hyperGit.worktree|hyperGit.worktreeMain/", "group": "1_wt@1" }, { "command": "hyperGit.worktreeMove", "when": "view == hyperGit.worktrees && viewItem == hyperGit.worktree", "group": "1_wt@2" }, { "command": "hyperGit.worktreeLock", "when": "view == hyperGit.worktrees && viewItem == hyperGit.worktree", "group": "1_wt@3" }, { "command": "hyperGit.worktreeUnlock", "when": "view == hyperGit.worktrees && viewItem == hyperGit.worktree", "group": "1_wt@4" }, { "command": "hyperGit.worktreeCopyPath", "when": "view == hyperGit.worktrees && viewItem =~ /hyperGit.worktree|hyperGit.worktreeMain/", "group": "1_wt@5" }, - { "command": "hyperGit.worktreeRemove", "when": "view == hyperGit.worktrees && viewItem == hyperGit.worktree", "group": "1_wt@6" } + { "command": "hyperGit.worktreeRemove", "when": "view == hyperGit.worktrees && viewItem == hyperGit.worktree", "group": "9_dangerous@1" } ], "editor/context": [ { "command": "hyperGit.toggleBlameAnnotation", "when": "resourceScheme == file", "group": "hyperGit@1" } @@ -328,7 +333,15 @@ { "command": "hyperGit.worktreeLock", "when": "false" }, { "command": "hyperGit.worktreeUnlock", "when": "false" }, { "command": "hyperGit.worktreeMove", "when": "false" }, - { "command": "hyperGit.worktreeRefresh", "when": "false" } + { "command": "hyperGit.worktreeRefresh", "when": "false" }, + { "command": "hyperGit.stashView", "when": "false" }, + { "command": "hyperGit.stageHunkAtCursor", "when": "false" }, + { "command": "hyperGit.logFilterAuthor", "when": "view == hyperGit.log" }, + { "command": "hyperGit.logFilterPath", "when": "view == hyperGit.log" }, + { "command": "hyperGit.logFilterGrep", "when": "view == hyperGit.log" }, + { "command": "hyperGit.logFilterRegex", "when": "view == hyperGit.log" }, + { "command": "hyperGit.logFilterMergeMode", "when": "view == hyperGit.log" }, + { "command": "hyperGit.logFilterDate", "when": "view == hyperGit.log" } ] }, "configuration": { @@ -337,33 +350,38 @@ "hyperGit.commit.template": { "type": "string", "default": "", - "markdownDescription": "Commit message 模板(注入到 Commit 窗口编辑器)。" + "markdownDescription": "Commit message template (injected into the Commit view editor)." }, "hyperGit.commit.conventional": { "type": "boolean", "default": true, - "markdownDescription": "对提交信息执行 Conventional Commits 实时校验。" + "markdownDescription": "Run real-time Conventional Commits validation on commit messages." }, "hyperGit.ai.enabled": { "type": "boolean", "default": false, - "markdownDescription": "启用 AI 代理能力(M5,当前仅预留接缝,暂不生效)。" + "markdownDescription": "Enable AI agent capabilities (currently reserved as an architectural seam; not yet active)." }, "hyperGit.log.ci.enabled": { "type": "boolean", "default": true, - "markdownDescription": "在 Log 视图每条提交上显示 CI 最终状态(GitHub Actions + Commit Statuses)。绿勾=通过、红叉=失败、悬停查看各项检查与未通过原因。需远程为 GitHub。" + "markdownDescription": "Show the final CI status on each commit in the Log view (GitHub Actions + Commit Statuses). Green check = passed, red cross = failed; hover to see each check and any failure reasons. Requires a GitHub remote." }, "hyperGit.log.ci.remote": { "type": "string", "default": "", - "markdownDescription": "查询 CI 状态使用的远程名(留空=自动,优先 `origin`)。多远程仓库可指定如 `upstream`。" + "markdownDescription": "Remote name used to query CI status (empty = auto, prefers `origin`). For multi-remote repos, specify e.g. `upstream`." }, "hyperGit.log.ci.provider": { "type": "string", "enum": ["auto", "github.com", "github-enterprise"], + "enumDescriptions": [ + "Auto-detect by origin host", + "GitHub.com (public cloud)", + "GitHub Enterprise (self-hosted)" + ], "default": "auto", - "markdownDescription": "CI 数据来源。`auto` 按 origin 主机判定(github.com → github.com,其余 → GitHub Enterprise)。" + "markdownDescription": "CI data source. `auto` is resolved by the origin host (github.com → github.com, otherwise GitHub Enterprise)." } } } diff --git a/src/adapter/advanced-commands.ts b/src/adapter/advanced-commands.ts index 6e32579..bcfbc49 100644 --- a/src/adapter/advanced-commands.ts +++ b/src/adapter/advanced-commands.ts @@ -21,16 +21,16 @@ export function registerAdvancedCommands(service: GitRepositoryService, branches if (!repo) { return; } - const ok = await vscode.window.showWarningMessage('撤销最近一次提交(soft reset,保留改动到暂存区)?', { modal: true }, '撤销'); - if (ok !== '撤销') { + const ok = await vscode.window.showWarningMessage('Undo the last commit (soft reset, keeps changes in the index)?', { modal: true }, 'Undo'); + if (ok !== 'Undo') { return; } try { await service.execGit(['reset', '--soft', 'HEAD~1']); branchesTree.refresh(); - void vscode.window.showInformationMessage('已撤销最近提交(soft)'); + void vscode.window.showInformationMessage('Undo last commit complete (soft)'); } catch (e) { - void vscode.window.showErrorMessage(`撤销失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to undo: ${errMsg(e)}`); } }), ); @@ -46,19 +46,19 @@ export function registerAdvancedCommands(service: GitRepositoryService, branches return; } const ok = await vscode.window.showWarningMessage( - `删除提交 ${hash.slice(0, 7)}?将用 rebase 重写历史(可能冲突;已推送的提交勿用)。`, + `Drop commit ${hash.slice(0, 7)}? This rewrites history via rebase (may conflict; do not use on pushed commits).`, { modal: true }, - '删除提交', + 'Drop commit', ); - if (ok !== '删除提交') { + if (ok !== 'Drop commit') { return; } try { await service.execGit(['rebase', '--onto', `${hash}^`, hash]); branchesTree.refresh(); - void vscode.window.showInformationMessage(`已删除提交 ${hash.slice(0, 7)}`); + void vscode.window.showInformationMessage(`Dropped commit ${hash.slice(0, 7)}`); } catch (e) { - void vscode.window.showErrorMessage(`删除失败(可能需手动解冲突):${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to drop (may need manual conflict resolution): ${errMsg(e)}`); } }), ); @@ -74,7 +74,7 @@ export function registerAdvancedCommands(service: GitRepositoryService, branches return; } const ok = await vscode.window.showWarningMessage( - `将当前已暂存改动 fixup 到 ${hash.slice(0, 7)}?将重写历史(autosquash rebase)。`, + `Fixup the currently staged changes into ${hash.slice(0, 7)}? This rewrites history (autosquash rebase).`, { modal: true }, 'Fixup', ); @@ -87,9 +87,9 @@ export function registerAdvancedCommands(service: GitRepositoryService, branches env: { ...process.env, GIT_SEQUENCE_EDITOR: ':' }, }); branchesTree.refresh(); - void vscode.window.showInformationMessage(`已 fixup 到 ${hash.slice(0, 7)}`); + void vscode.window.showInformationMessage(`Fixup into ${hash.slice(0, 7)} complete`); } catch (e) { - void vscode.window.showErrorMessage(`Fixup 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Fixup failed: ${errMsg(e)}`); } }), ); @@ -107,16 +107,16 @@ export function registerAdvancedCommands(service: GitRepositoryService, branches const out = await service.execGit(['branch', '--merged', base]); merged = filterMergeable(out, base, headName ? [headName] : []); } catch (e) { - void vscode.window.showErrorMessage(`查询已合并分支失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to query merged branches: ${errMsg(e)}`); return; } if (merged.length === 0) { - void vscode.window.showInformationMessage('无可清理的已合并分支'); + void vscode.window.showInformationMessage('No merged branches to clean up'); return; } const picks = await vscode.window.showQuickPick( merged.map((b) => ({ label: b, picked: true })), - { canPickMany: true, title: `已合并到 ${base} 的本地分支(勾选删除)` }, + { canPickMany: true, title: `Local branches merged into ${base} (check to delete)` }, ); if (!picks || picks.length === 0) { return; @@ -131,7 +131,7 @@ export function registerAdvancedCommands(service: GitRepositoryService, branches } } branchesTree.refresh(); - void vscode.window.showInformationMessage(`已删除 ${deleted} 个已合并分支`); + void vscode.window.showInformationMessage(`Deleted ${deleted} merged branch(es)`); }), ); @@ -145,7 +145,7 @@ export function registerAdvancedCommands(service: GitRepositoryService, branches return; } await vscode.env.clipboard.writeText(names.join('\n')); - void vscode.window.showInformationMessage(names.length === 1 ? `已复制 ${names[0]}` : `已复制 ${names.length} 个引用`); + void vscode.window.showInformationMessage(names.length === 1 ? `Copied ${names[0]}` : `Copied ${names.length} refs`); }), ); @@ -159,21 +159,21 @@ export function registerAdvancedCommands(service: GitRepositoryService, branches const staged = await service.execGit(['diff', '--cached', '--stat']); const working = await service.execGit(['diff', '--stat']); const content = [ - '# 3-way Diff 概览(HEAD ↔ Staged ↔ Working)', + '# 3-way Diff Overview (HEAD ↔ Staged ↔ Working)', '', - '## 已暂存改动(HEAD ↔ Staged)', + '## Staged Changes (HEAD ↔ Staged)', '', - staged.trim() || '_(无)_)', + staged.trim() || '_(none)_)', '', - '## 未暂存改动(Staged ↔ Working)', + '## Unstaged Changes (Staged ↔ Working)', '', - working.trim() || '_(无)_', + working.trim() || '_(none)_', '', ].join('\n'); const doc = await vscode.workspace.openTextDocument({ content, language: 'markdown' }); await vscode.window.showTextDocument(doc, { preview: true }); } catch (e) { - void vscode.window.showErrorMessage(`3-way diff 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`3-way diff failed: ${errMsg(e)}`); } }), ); @@ -192,6 +192,6 @@ async function pickCommitHash(service: GitRepositoryService): Promise { - const name = await vscode.window.showInputBox({ prompt: '新建 Changelist 名称', placeHolder: '例如 feature-x' }); + const name = await vscode.window.showInputBox({ prompt: 'New Changelist name', placeHolder: 'e.g. feature-x' }); if (name && name.trim()) { registry.create(name.trim()); } @@ -43,7 +43,7 @@ export function registerChangesCommands( return; } const def = registry.getDef(node.id); - const name = await vscode.window.showInputBox({ prompt: '重命名 Changelist', value: def?.name }); + const name = await vscode.window.showInputBox({ prompt: 'Rename Changelist', value: def?.name }); if (name && name.trim()) { registry.rename(node.id, name.trim()); } @@ -56,11 +56,11 @@ export function registerChangesCommands( return; } const choice = await vscode.window.showWarningMessage( - `删除 Changelist「${node.name}」?其下文件将归入默认列表。`, + `Delete Changelist "${node.name}"? Files under it will be moved to the default list.`, { modal: true }, - '删除', + 'Delete', ); - if (choice === '删除') { + if (choice === 'Delete') { registry.remove(node.id); } }), @@ -75,7 +75,7 @@ export function registerChangesCommands( const picks = registry .listDefs() .map((d) => ({ label: d.name, id: d.id, description: d.id === active ? 'active' : undefined, picked: d.id === node.changelistId })); - const pick = await vscode.window.showQuickPick(picks, { placeHolder: '将文件移至 Changelist' }); + const pick = await vscode.window.showQuickPick(picks, { placeHolder: 'Move file to Changelist' }); if (pick) { registry.move(node.item.relativePath, pick.id); } @@ -103,11 +103,11 @@ export function registerChangesCommands( return; } const choice = await vscode.window.showWarningMessage( - `丢弃「${change.relativePath}」的改动?此操作不可撤销。`, + `Discard changes to "${change.relativePath}"? This action cannot be undone.`, { modal: true }, - '丢弃', + 'Discard', ); - if (choice !== '丢弃') { + if (choice !== 'Discard') { return; } try { @@ -119,7 +119,7 @@ export function registerChangesCommands( } tree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`丢弃失败:${e instanceof Error ? e.message : String(e)}`); + void vscode.window.showErrorMessage(`Failed to discard: ${e instanceof Error ? e.message : String(e)}`); } }), ); diff --git a/src/adapter/commit/commit-service.ts b/src/adapter/commit/commit-service.ts index 3004715..a88731b 100644 --- a/src/adapter/commit/commit-service.ts +++ b/src/adapter/commit/commit-service.ts @@ -103,18 +103,18 @@ export class CommitService implements vscode.Disposable { async executeCommit(req: CommitRequest): Promise { const repo = this.service.repo; if (!repo) { - return { ok: false, error: '未找到 Git 仓库' }; + return { ok: false, error: 'No Git repository found' }; } const message = req.message.trim(); if (!message) { - return { ok: false, error: '提交信息不能为空' }; + return { ok: false, error: 'Commit message cannot be empty' }; } // CC 即时校验(pipeline 内的 hook 亦会拦截,此处给出明确原因) if (this.conventionalEnabled()) { const v = validateConventional(message); if (v.severity === 'error') { - return { ok: false, error: v.reason ?? '提交信息不符合 Conventional Commits 规范' }; + return { ok: false, error: v.reason ?? 'Commit message does not conform to Conventional Commits' }; } } @@ -123,13 +123,13 @@ export class CommitService implements vscode.Disposable { const checkedSet = new Set(req.selectedPaths); const absPaths = this.resolveAbsolute(req.selectedPaths, changes); if (absPaths.length === 0) { - return { ok: false, error: '未选择任何待提交文件' }; + return { ok: false, error: 'No files selected to commit' }; } // Checkin hook 责任链(传绝对路径,供未来 AI hook 读取文件内容) const hookResult = await this.pipeline.run({ message, filePaths: absPaths }); if (hookResult === CheckinResult.Cancel) { - return { ok: false, error: '提交被检查拦截(Checkin hook)' }; + return { ok: false, error: 'Commit blocked by check (Checkin hook)' }; } try { @@ -146,7 +146,7 @@ export class CommitService implements vscode.Disposable { try { await repo.push(); } catch (e) { - return { ok: true, warning: `提交已成功,但推送失败:${this.normalizeError(e)}` }; + return { ok: true, warning: `Commit succeeded, but push failed: ${this.normalizeError(e)}` }; } } return { ok: true }; diff --git a/src/adapter/conflict-ui.ts b/src/adapter/conflict-ui.ts index 1a9d7eb..ad54121 100644 --- a/src/adapter/conflict-ui.ts +++ b/src/adapter/conflict-ui.ts @@ -39,18 +39,18 @@ export async function handleGitConflict(service: GitRepositoryService, opName: s const op = state.ongoingOperation; const canAbort = op !== 'none'; const choice = await vscode.window.showWarningMessage( - `${opName} 遇到 ${state.conflictedPaths.length} 个冲突文件。`, - { modal: true, detail: canAbort ? `正在进行:${op}。可解决冲突后继续,或中止恢复工作区。` : '请手动解决冲突后提交。' }, - ...(canAbort ? ['解决冲突', '中止操作'] : ['知道了']), + `${opName} encountered ${state.conflictedPaths.length} conflicted file(s).`, + { modal: true, detail: canAbort ? `In progress: ${op}. Resolve the conflicts to continue, or abort to restore the working tree.` : 'Please resolve the conflicts manually and commit.' }, + ...(canAbort ? ['Resolve conflicts', 'Abort'] : ['Got it']), ); - if (choice === '中止操作' && op !== 'none') { + if (choice === 'Abort' && op !== 'none') { try { await service.execGit(ABORT_ARGS[op]); - void vscode.window.showInformationMessage(`已中止 ${op},工作区已恢复`); + void vscode.window.showInformationMessage(`${op} aborted, working tree restored`); } catch { - void vscode.window.showErrorMessage('中止操作失败,请手动处理'); + void vscode.window.showErrorMessage('Failed to abort, please handle manually'); } - } else if (choice === '解决冲突') { + } else if (choice === 'Resolve conflicts') { // 打开自绘 3-way merge editor(resolveConflicts 列出冲突文件供选择) void vscode.commands.executeCommand('hyperGit.resolveConflicts'); } diff --git a/src/adapter/editor/blame-annotation.ts b/src/adapter/editor/blame-annotation.ts index 679a22d..b2639f3 100644 --- a/src/adapter/editor/blame-annotation.ts +++ b/src/adapter/editor/blame-annotation.ts @@ -37,7 +37,7 @@ export class BlameAnnotationController implements vscode.Disposable { const editor = vscode.window.activeTextEditor; const repo = this.service.repo; if (!editor || !repo) { - void vscode.window.showWarningMessage('请先打开一个文件'); + void vscode.window.showWarningMessage('Please open a file first'); return; } const key = editor.document.uri.toString(); @@ -47,7 +47,7 @@ export class BlameAnnotationController implements vscode.Disposable { } const rel = path.relative(repo.rootUri.fsPath, editor.document.uri.fsPath).split(path.sep).join('/'); if (rel.startsWith('..') || path.isAbsolute(rel)) { - void vscode.window.showWarningMessage('该文件不在当前仓库内'); + void vscode.window.showWarningMessage('This file is outside the current repository'); return; } let blame: BlameLineMap; @@ -55,7 +55,7 @@ export class BlameAnnotationController implements vscode.Disposable { const out = await this.service.execGit(['blame', '--line-porcelain', '--', rel]); blame = new Map(parseBlamePorcelain(out).map((b) => [b.line, b])); } catch (e) { - void vscode.window.showErrorMessage(`Blame 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Blame failed: ${errMsg(e)}`); return; } const options: vscode.DecorationOptions[] = []; diff --git a/src/adapter/editor/inline-commit-codelens.ts b/src/adapter/editor/inline-commit-codelens.ts index 6dcd487..d557b84 100644 --- a/src/adapter/editor/inline-commit-codelens.ts +++ b/src/adapter/editor/inline-commit-codelens.ts @@ -53,7 +53,7 @@ export class InlineCommitCodeLensProvider implements vscode.CodeLensProvider { const line = Math.max(0, r.startLine - 1); return new vscode.CodeLens(new vscode.Range(line, 0, line, 0), { command: 'hyperGit.inlineCommitHunk', - title: `✓ 提交此 Hunk (+${r.addedCount} -${r.removedCount})`, + title: `✓ Commit this Hunk (+${r.addedCount} -${r.removedCount})`, arguments: [rel, r.hunkIndex], }); }); @@ -74,16 +74,16 @@ export function registerInlineCommitCommand(service: GitRepositoryService, provi const otherStaged = service.getChanges().filter((c) => c.staged && c.relativePath !== rel); if (otherStaged.length > 0) { const ok = await vscode.window.showWarningMessage( - `当前已有其他 ${otherStaged.length} 个已暂存文件,将一并提交。继续?`, + `${otherStaged.length} other staged file(s) will be committed together. Continue?`, { modal: true }, - '继续提交', + 'Continue Commit', ); - if (ok !== '继续提交') { + if (ok !== 'Continue Commit') { return; } } const message = await vscode.window.showInputBox({ - prompt: `提交 Hunk(${rel} #${hunkIndex + 1})的 commit message`, + prompt: `Commit message for Hunk (${rel} #${hunkIndex + 1})`, placeHolder: 'feat(scope): description', }); if (!message || !message.trim()) { @@ -109,9 +109,9 @@ export function registerInlineCommitCommand(service: GitRepositoryService, provi } await service.execGit(['commit', '-m', message.trim()]); provider.refresh(); - void vscode.window.showInformationMessage('已提交该 Hunk'); + void vscode.window.showInformationMessage('Hunk committed'); } catch (e) { - void vscode.window.showErrorMessage(`Inline commit 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Inline commit failed: ${errMsg(e)}`); } }); } diff --git a/src/adapter/git-cli-commands.ts b/src/adapter/git-cli-commands.ts index fb7775e..f45ca9a 100644 --- a/src/adapter/git-cli-commands.ts +++ b/src/adapter/git-cli-commands.ts @@ -29,10 +29,10 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr await service.execGit(['cherry-pick', hash]); branchesTree.refresh(); logTree.refresh(); - void vscode.window.showInformationMessage(`Cherry-pick ${hash.slice(0, 7)} 完成`); + void vscode.window.showInformationMessage(`Cherry-pick ${hash.slice(0, 7)} complete`); } catch (e) { if (!(await handleGitConflict(service, 'Cherry-pick'))) { - void vscode.window.showErrorMessage(`Cherry-pick 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Cherry-pick failed: ${errMsg(e)}`); } } }), @@ -52,10 +52,10 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr await service.execGit(['revert', '--no-edit', hash]); branchesTree.refresh(); logTree.refresh(); - void vscode.window.showInformationMessage(`Revert ${hash.slice(0, 7)} 完成`); + void vscode.window.showInformationMessage(`Revert ${hash.slice(0, 7)} complete`); } catch (e) { if (!(await handleGitConflict(service, 'Revert'))) { - void vscode.window.showErrorMessage(`Revert 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Revert failed: ${errMsg(e)}`); } } }), @@ -75,17 +75,17 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr } // 2) 选择 reset 模式 const items = [ - { label: 'soft', description: '仅移动 HEAD,保留暂存区与工作区改动' }, - { label: 'mixed', description: '移动 HEAD + 取消暂存(默认),保留工作区改动' }, - { label: 'hard', description: '⚠ 移动 HEAD + 丢弃暂存区与工作区所有改动(不可撤销)' }, - { label: 'keep', description: '移动 HEAD + 保留已修改文件(遇冲突中止)' }, + { label: 'soft', description: 'Move HEAD only, keep staged and working changes' }, + { label: 'mixed', description: 'Move HEAD + unstage (default), keep working changes' }, + { label: 'hard', description: '⚠ Move HEAD + discard all staged and working changes (irreversible)' }, + { label: 'keep', description: 'Move HEAD + keep modified files (aborts on conflict)' }, ]; - const pick = await vscode.window.showQuickPick(items, { placeHolder: `选择 reset 模式(目标 ${target.slice(0, 7)})` }); + const pick = await vscode.window.showQuickPick(items, { placeHolder: `Select reset mode (target ${target.slice(0, 7)})` }); if (!pick) { return; } if (pick.label === 'hard') { - const ok = await vscode.window.showWarningMessage('hard reset 将丢弃所有改动,确认?', { modal: true }, '确认 hard reset'); + const ok = await vscode.window.showWarningMessage('hard reset will discard all changes. Confirm?', { modal: true }, 'Confirm hard reset'); if (!ok) { return; } @@ -94,9 +94,9 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr await service.execGit(['reset', `--${pick.label}`, target]); branchesTree.refresh(); logTree.refresh(); - void vscode.window.showInformationMessage(`Reset (--${pick.label} ${target.slice(0, 7)}) 完成`); + void vscode.window.showInformationMessage(`Reset (--${pick.label} ${target.slice(0, 7)}) complete`); } catch (e) { - void vscode.window.showErrorMessage(`Reset 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Reset failed: ${errMsg(e)}`); } }), ); @@ -108,7 +108,7 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr return; } const oldName = node.ref.shortName; - const newName = await vscode.window.showInputBox({ prompt: `重命名分支「${oldName}」`, value: oldName }); + const newName = await vscode.window.showInputBox({ prompt: `Rename branch "${oldName}"`, value: oldName }); if (!newName || !newName.trim() || newName === oldName) { return; } @@ -116,7 +116,7 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr await service.execGit(['branch', '-m', oldName, newName.trim()]); branchesTree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`重命名失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Rename failed: ${errMsg(e)}`); } }), ); @@ -136,9 +136,9 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr await new Promise((resolve, reject) => { fs.appendFile(ignoreFile, `${rel}\n`, (err) => (err ? reject(err) : resolve())); }); - void vscode.window.showInformationMessage(`已添加到 .gitignore:${rel}`); + void vscode.window.showInformationMessage(`Added to .gitignore: ${rel}`); } catch (e) { - void vscode.window.showErrorMessage(`Ignore 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Ignore failed: ${errMsg(e)}`); } }), ); @@ -150,13 +150,13 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr return; } const refs = repo.state.refs.filter((r) => r.name && (r.type === 0 || r.type === 1)); - const base = node?.kind === 'branch' ? node.ref.shortName : await vscode.window.showQuickPick(refs.map((r) => r.name!), { placeHolder: '选择 base 分支' }); + const base = node?.kind === 'branch' ? node.ref.shortName : await vscode.window.showQuickPick(refs.map((r) => r.name!), { placeHolder: 'Select base branch' }); if (!base) { return; } const target = await vscode.window.showQuickPick( refs.filter((r) => r.name !== base).map((r) => r.name!), - { placeHolder: `比较 ${base} 与...` }, + { placeHolder: `Compare ${base} with...` }, ); if (!target) { return; @@ -166,7 +166,7 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr const doc = await vscode.workspace.openTextDocument({ content: `$ git diff --stat ${base}...${target}\n\n${out}`, language: 'plaintext' }); await vscode.window.showTextDocument(doc, { preview: true }); } catch (e) { - void vscode.window.showErrorMessage(`比较失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Compare failed: ${errMsg(e)}`); } }), ); @@ -177,15 +177,15 @@ export function registerGitCliCommands(service: GitRepositoryService, branchesTr if (!repo) { return; } - const message = await vscode.window.showInputBox({ prompt: '改写最新提交信息(amend)', value: (await repo.log({ maxEntries: 1 }))[0]?.message ?? '' }); + const message = await vscode.window.showInputBox({ prompt: 'Rewrite latest commit message (amend)', value: (await repo.log({ maxEntries: 1 }))[0]?.message ?? '' }); if (!message || !message.trim()) { return; } try { await repo.commit(message.trim(), { amend: true }); - void vscode.window.showInformationMessage('已改写最新提交'); + void vscode.window.showInformationMessage('Rewrote latest commit'); } catch (e) { - void vscode.window.showErrorMessage(`改写失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Rewrite failed: ${errMsg(e)}`); } }), ); @@ -204,7 +204,7 @@ async function pickCommitHash(service: GitRepositoryService): Promise ({ label: c.relativePath })), { placeHolder: '选择要忽略的文件' }); + const pick = await vscode.window.showQuickPick(changes.map((c) => ({ label: c.relativePath })), { placeHolder: 'Select file to ignore' }); return pick?.label; } @@ -229,6 +229,6 @@ async function pickResetTarget(service: GitRepositoryService): Promise { - const author = await vscode.window.showInputBox({ prompt: '按作者过滤(Author)', placeHolder: '例如 John' }); + const author = await vscode.window.showInputBox({ prompt: 'Filter by Author', placeHolder: 'e.g. John' }); const f = logTree.getFilter(); logTree.setFilter({ ...f, author: author && author.trim() ? author.trim() : undefined }); }), @@ -58,11 +58,30 @@ export function registerHistoryCommands( subs.push(vscode.commands.registerCommand('hyperGit.logClearFilter', () => logTree.clearFilter())); + subs.push( + vscode.commands.registerCommand('hyperGit.logFilter', async () => { + // 过滤器聚合入口:QuickPick 分发到既有 6 个过滤命令(复用 handler,零重复实现)。 + const items = [ + { label: 'Filter by Author…', description: 'commits by author', action: 'hyperGit.logFilterAuthor' }, + { label: 'Filter by Path…', description: 'commits touching a path', action: 'hyperGit.logFilterPath' }, + { label: 'Filter by Message (grep)…', description: 'substring match', action: 'hyperGit.logFilterGrep' }, + { label: 'Filter by Message (regex)…', description: 'regular expression', action: 'hyperGit.logFilterRegex' }, + { label: 'Filter Merge Commits…', description: 'show or hide merge commits', action: 'hyperGit.logFilterMergeMode' }, + { label: 'Filter by Date…', description: 'commits within recent days', action: 'hyperGit.logFilterDate' }, + { label: 'Clear Filter', description: 'remove all active filters', action: 'hyperGit.logClearFilter' }, + ]; + const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Filter commit log' }); + if (pick) { + await vscode.commands.executeCommand(pick.action); + } + }), + ); + subs.push( vscode.commands.registerCommand('hyperGit.copyCommitHash', (node: LogNode) => { if (node?.kind === 'commit') { void vscode.env.clipboard.writeText(node.commit.hash); - void vscode.window.showInformationMessage(`已复制 ${node.commit.hash.slice(0, 7)}`); + void vscode.window.showInformationMessage(`Copied ${node.commit.hash.slice(0, 7)}`); } }), ); @@ -82,13 +101,13 @@ export function registerHistoryCommands( if (!repo) { return; } - const name = await vscode.window.showInputBox({ prompt: '新分支名称' }); + const name = await vscode.window.showInputBox({ prompt: 'New branch name' }); if (name && name.trim()) { try { await repo.createBranch(name.trim(), true); branchesTree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`创建分支失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to create branch: ${errMsg(e)}`); } } }), @@ -104,7 +123,7 @@ export function registerHistoryCommands( await repo.checkout(node.ref.shortName); branchesTree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`检出失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to checkout: ${errMsg(e)}`); } }), ); @@ -120,7 +139,7 @@ export function registerHistoryCommands( const names = refs.map((r) => r.shortName); if (names.length === 0) { if (node?.kind === 'branch' && node.ref.head) { - void vscode.window.showWarningMessage('当前分支无法删除'); + void vscode.window.showWarningMessage('Cannot delete the current branch'); } return; } @@ -151,9 +170,9 @@ export function registerHistoryCommands( } branchesTree.refresh(); if (failures.length === 0) { - void vscode.window.showInformationMessage(deleted === 1 ? `已删除分支 ${names[0]}` : `已删除 ${deleted} 个分支`); + void vscode.window.showInformationMessage(deleted === 1 ? `Deleted branch ${names[0]}` : `Deleted ${deleted} branches`); } else { - void vscode.window.showWarningMessage(`已删除 ${deleted} 个分支,${failures.length} 个失败:${truncateNames(failures)}`); + void vscode.window.showWarningMessage(`Deleted ${deleted} branches, ${failures.length} failed: ${truncateNames(failures)}`); } }), ); @@ -165,16 +184,16 @@ export function registerHistoryCommands( return; } const name = node.ref.shortName; - const ok = await vscode.window.showWarningMessage(`将「${name}」合并到当前分支?`, { modal: true }, '合并'); - if (ok !== '合并') { + const ok = await vscode.window.showWarningMessage(`Merge "${name}" into current branch?`, { modal: true }, 'Merge'); + if (ok !== 'Merge') { return; } try { await repo.merge(name); branchesTree.refresh(); } catch (e) { - if (!(await handleGitConflict(service, '合并'))) { - void vscode.window.showErrorMessage(`合并失败:${errMsg(e)}`); + if (!(await handleGitConflict(service, 'Merge'))) { + void vscode.window.showErrorMessage(`Failed to merge: ${errMsg(e)}`); } } }), @@ -187,16 +206,16 @@ export function registerHistoryCommands( return; } const name = node.ref.shortName; - const ok = await vscode.window.showWarningMessage(`将当前分支变基到「${name}」?(重写历史,可能冲突)`, { modal: true }, '变基'); - if (ok !== '变基') { + const ok = await vscode.window.showWarningMessage(`Rebase current branch onto "${name}"? (this rewrites history and may conflict)`, { modal: true }, 'Rebase'); + if (ok !== 'Rebase') { return; } try { await repo.rebase(name); branchesTree.refresh(); } catch (e) { - if (!(await handleGitConflict(service, '变基'))) { - void vscode.window.showErrorMessage(`变基失败:${errMsg(e)}`); + if (!(await handleGitConflict(service, 'Rebase'))) { + void vscode.window.showErrorMessage(`Failed to rebase: ${errMsg(e)}`); } } }), @@ -210,12 +229,12 @@ export function registerHistoryCommands( } const editor = vscode.window.activeTextEditor; if (!editor) { - void vscode.window.showWarningMessage('请先打开一个文件'); + void vscode.window.showWarningMessage('Please open a file first'); return; } const rel = path.relative(repo.rootUri.fsPath, editor.document.uri.fsPath).split(path.sep).join('/'); if (rel.startsWith('..') || path.isAbsolute(rel)) { - void vscode.window.showWarningMessage('该文件不在当前仓库内,无法 Blame'); + void vscode.window.showWarningMessage('This file is outside the current repository; cannot Blame'); return; } try { @@ -223,7 +242,7 @@ export function registerHistoryCommands( const doc = await vscode.workspace.openTextDocument({ content: blame, language: 'plaintext' }); await vscode.window.showTextDocument(doc, { preview: true }); } catch (e) { - void vscode.window.showErrorMessage(`Blame 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to Blame: ${errMsg(e)}`); } }), ); @@ -239,7 +258,7 @@ export function registerHistoryCommands( branchesTree.refresh(); } catch (e) { if (!(await handleGitConflict(service, 'Pull'))) { - void vscode.window.showErrorMessage(`Pull 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to Pull: ${errMsg(e)}`); } } }), @@ -253,7 +272,7 @@ export function registerHistoryCommands( } const head = repo.state.HEAD; if (!head?.name) { - void vscode.window.showWarningMessage('当前处于 detached HEAD,无法推送当前分支'); + void vscode.window.showWarningMessage('Currently in detached HEAD; cannot push the current branch'); return; } try { @@ -264,14 +283,14 @@ export function registerHistoryCommands( // 无上游:选定 remote 并以 -u 建立追踪(修复「Failed to execute git」根因)。 const remotes = repo.state.remotes.map((r) => r.name); if (remotes.length === 0) { - void vscode.window.showWarningMessage('未配置远程仓库(remote),无法推送'); + void vscode.window.showWarningMessage('No remote configured; cannot push'); return; } const remote = remotes.length === 1 ? remotes[0] : await vscode.window.showQuickPick(remotes, { - placeHolder: `为「${head.name}」选择推送目标 remote(将建立上游追踪 -u)`, + placeHolder: `Select a remote to push "${head.name}" to (will set upstream tracking -u)`, }); if (!remote) { return; @@ -279,9 +298,9 @@ export function registerHistoryCommands( await repo.push(remote, head.name, true); } branchesTree.refresh(); - void vscode.window.showInformationMessage(`已推送 ${head.name}`); + void vscode.window.showInformationMessage(`Pushed ${head.name}`); } catch (e) { - void vscode.window.showErrorMessage(`Push 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to Push: ${errMsg(e)}`); } }), ); @@ -293,8 +312,8 @@ export function registerHistoryCommands( return; } const pick = await vscode.window.showQuickPick( - ['普通 Fetch', 'Fetch + Prune(清理已删除的远程分支)'], - { placeHolder: 'Fetch 模式' }, + ['Fetch', 'Fetch + Prune (clean up deleted remote branches)'], + { placeHolder: 'Fetch mode' }, ); if (!pick) { return; @@ -303,7 +322,7 @@ export function registerHistoryCommands( await repo.fetch(pick.includes('Prune') ? { prune: true } : undefined); branchesTree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`Fetch 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to Fetch: ${errMsg(e)}`); } }), ); @@ -328,7 +347,7 @@ export function registerHistoryCommands( return; } const source = node.ref.shortName; - const name = await vscode.window.showInputBox({ prompt: `从「${source}」新建并检出本地分支`, placeHolder: '新分支名' }); + const name = await vscode.window.showInputBox({ prompt: `Create and checkout a new local branch from "${source}"`, placeHolder: 'New branch name' }); if (!name || !name.trim()) { return; } @@ -336,7 +355,7 @@ export function registerHistoryCommands( await repo.createBranch(name.trim(), true, source); branchesTree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`新建分支失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to create branch: ${errMsg(e)}`); } }), ); @@ -353,7 +372,7 @@ export function registerHistoryCommands( const doc = await vscode.workspace.openTextDocument({ content: `$ git diff --stat HEAD...${selected}\n\n${out}`, language: 'plaintext' }); await vscode.window.showTextDocument(doc, { preview: true }); } catch (e) { - void vscode.window.showErrorMessage(`比较失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to compare: ${errMsg(e)}`); } }), ); @@ -364,29 +383,29 @@ export function registerHistoryCommands( if (!repo) { return; } - const name = await vscode.window.showInputBox({ prompt: '标签名(如 v1.0.0)' }); + const name = await vscode.window.showInputBox({ prompt: 'Tag name (e.g. v1.0.0)' }); if (!name || !name.trim()) { return; } const commits = await repo.log({ maxEntries: 20 }); const items = [ - { label: 'HEAD', description: '当前提交', target: 'HEAD' }, + { label: 'HEAD', description: 'Current commit', target: 'HEAD' }, ...commits.map((c) => ({ label: (c.message.split('\n', 1)[0] ?? c.hash).slice(0, 50), description: c.hash.slice(0, 7), target: c.hash, })), ]; - const pick = await vscode.window.showQuickPick(items, { placeHolder: '在哪个 commit 上打标签' }); + const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Choose a commit to tag' }); if (!pick) { return; } try { await service.execGit(['tag', name.trim(), pick.target]); branchesTree.refresh(); - void vscode.window.showInformationMessage(`已创建标签 ${name.trim()} @ ${pick.target === 'HEAD' ? 'HEAD' : pick.target.slice(0, 7)}`); + void vscode.window.showInformationMessage(`Created tag ${name.trim()} @ ${pick.target === 'HEAD' ? 'HEAD' : pick.target.slice(0, 7)}`); } catch (e) { - void vscode.window.showErrorMessage(`创建标签失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to create tag: ${errMsg(e)}`); } }), ); @@ -398,9 +417,9 @@ export function registerHistoryCommands( if (names.length === 0) { return; } - const detail = names.length === 1 ? `删除标签「${names[0]}」?` : `将删除 ${names.length} 个标签:${truncateNames(names)}`; - const ok = await vscode.window.showWarningMessage(detail, { modal: true }, '删除'); - if (ok !== '删除') { + const detail = names.length === 1 ? `Delete tag "${names[0]}"?` : `Will delete ${names.length} tags: ${truncateNames(names)}`; + const ok = await vscode.window.showWarningMessage(detail, { modal: true }, 'Delete'); + if (ok !== 'Delete') { return; } const failures: string[] = []; @@ -415,9 +434,9 @@ export function registerHistoryCommands( } branchesTree.refresh(); if (failures.length === 0) { - void vscode.window.showInformationMessage(deleted === 1 ? `已删除标签 ${names[0]}` : `已删除 ${deleted} 个标签`); + void vscode.window.showInformationMessage(deleted === 1 ? `Deleted tag ${names[0]}` : `Deleted ${deleted} tags`); } else { - void vscode.window.showWarningMessage(`已删除 ${deleted} 个标签,${failures.length} 个失败:${truncateNames(failures)}`); + void vscode.window.showWarningMessage(`Deleted ${deleted} tags, ${failures.length} failed: ${truncateNames(failures)}`); } }), ); @@ -429,15 +448,15 @@ export function registerHistoryCommands( return; } const name = node.ref.shortName; - const ok = await vscode.window.showWarningMessage(`检出标签「${name}」(进入 detached HEAD)?`, { modal: true }, '检出'); - if (ok !== '检出') { + const ok = await vscode.window.showWarningMessage(`Checkout tag "${name}" (entering detached HEAD)?`, { modal: true }, 'Checkout'); + if (ok !== 'Checkout') { return; } try { await repo.checkout(name); branchesTree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`检出标签失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to checkout tag: ${errMsg(e)}`); } }), ); @@ -460,7 +479,7 @@ export function registerHistoryCommands( await vscode.commands.executeCommand('vscode.open', right); } } catch (e) { - void vscode.window.showErrorMessage(`打开 diff 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to open diff: ${errMsg(e)}`); } }), ); @@ -472,17 +491,17 @@ export function registerHistoryCommands( } const hash = node.commit.hash; const items = [ - { label: 'soft', description: '仅移动 HEAD,保留暂存区与工作区改动' }, - { label: 'mixed', description: '移动 HEAD + 取消暂存(默认),保留工作区改动' }, - { label: 'hard', description: '⚠ 移动 HEAD + 丢弃暂存区与工作区所有改动(不可撤销)' }, - { label: 'keep', description: '移动 HEAD + 保留已修改文件(遇冲突中止)' }, + { label: 'soft', description: 'Move HEAD only; keep staged and working changes' }, + { label: 'mixed', description: 'Move HEAD + unstage (default); keep working changes' }, + { label: 'hard', description: '⚠ Move HEAD + discard all staged and working changes (cannot be undone)' }, + { label: 'keep', description: 'Move HEAD + keep modified files (aborts on conflict)' }, ]; - const pick = await vscode.window.showQuickPick(items, { placeHolder: `Reset 当前分支到 ${hash.slice(0, 7)} 的模式` }); + const pick = await vscode.window.showQuickPick(items, { placeHolder: `Reset mode for current branch to ${hash.slice(0, 7)}` }); if (!pick) { return; } if (pick.label === 'hard') { - const ok = await vscode.window.showWarningMessage(`hard reset 到 ${hash.slice(0, 7)} 将丢弃所有改动,确认?`, { modal: true }, '确认 hard reset'); + const ok = await vscode.window.showWarningMessage(`Hard reset to ${hash.slice(0, 7)} will discard all changes. Confirm?`, { modal: true }, 'Confirm Hard Reset'); if (!ok) { return; } @@ -491,10 +510,10 @@ export function registerHistoryCommands( await service.execGit(['reset', `--${pick.label}`, hash]); branchesTree.refresh(); logTree.refresh(); - void vscode.window.showInformationMessage(`Reset (--${pick.label} ${hash.slice(0, 7)}) 完成`); + void vscode.window.showInformationMessage(`Reset (--${pick.label} ${hash.slice(0, 7)}) complete`); } catch (e) { if (!(await handleGitConflict(service, 'Reset'))) { - void vscode.window.showErrorMessage(`Reset 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to Reset: ${errMsg(e)}`); } } }), @@ -506,16 +525,16 @@ export function registerHistoryCommands( if (!repo || node?.kind !== 'commit') { return; } - const name = await vscode.window.showInputBox({ prompt: `从 ${node.commit.hash.slice(0, 7)} 新建并检出分支`, placeHolder: '新分支名' }); + const name = await vscode.window.showInputBox({ prompt: `Create and checkout a new branch from ${node.commit.hash.slice(0, 7)}`, placeHolder: 'New branch name' }); if (!name || !name.trim()) { return; } try { await repo.createBranch(name.trim(), true, node.commit.hash); branchesTree.refresh(); - void vscode.window.showInformationMessage(`已新建并检出 ${name.trim()} @ ${node.commit.hash.slice(0, 7)}`); + void vscode.window.showInformationMessage(`Created and checked out ${name.trim()} @ ${node.commit.hash.slice(0, 7)}`); } catch (e) { - void vscode.window.showErrorMessage(`新建分支失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to create branch: ${errMsg(e)}`); } }), ); @@ -525,16 +544,16 @@ export function registerHistoryCommands( if (node?.kind !== 'commit') { return; } - const name = await vscode.window.showInputBox({ prompt: `在 ${node.commit.hash.slice(0, 7)} 上新建标签`, placeHolder: '如 v1.0.0' }); + const name = await vscode.window.showInputBox({ prompt: `Create a new tag at ${node.commit.hash.slice(0, 7)}`, placeHolder: 'e.g. v1.0.0' }); if (!name || !name.trim()) { return; } try { await service.execGit(['tag', name.trim(), node.commit.hash]); branchesTree.refresh(); - void vscode.window.showInformationMessage(`已创建标签 ${name.trim()} @ ${node.commit.hash.slice(0, 7)}`); + void vscode.window.showInformationMessage(`Created tag ${name.trim()} @ ${node.commit.hash.slice(0, 7)}`); } catch (e) { - void vscode.window.showErrorMessage(`创建标签失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to create tag: ${errMsg(e)}`); } }), ); @@ -552,14 +571,14 @@ export function registerHistoryCommands( }); await vscode.window.showTextDocument(doc, { preview: true }); } catch (e) { - void vscode.window.showErrorMessage(`查询失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to query: ${errMsg(e)}`); } }), ); subs.push( vscode.commands.registerCommand('hyperGit.logFilterGrep', async () => { - const grep = await vscode.window.showInputBox({ prompt: '按 message 文本/正则过滤(--grep)', placeHolder: '例如 fix|bug' }); + const grep = await vscode.window.showInputBox({ prompt: 'Filter by message text/regex (--grep)', placeHolder: 'e.g. fix|bug' }); const f = logTree.getFilter(); logTree.setFilter({ ...f, grep: grep && grep.trim() ? grep.trim() : undefined }); }), @@ -569,11 +588,11 @@ export function registerHistoryCommands( vscode.commands.registerCommand('hyperGit.logFilterMergeMode', async () => { const pick = await vscode.window.showQuickPick( [ - { label: '全部提交', mode: 'all' as MergeMode }, - { label: '仅合并提交(merge)', mode: 'merge-only' as MergeMode }, - { label: '仅非合并提交', mode: 'no-merge' as MergeMode }, + { label: 'All Commits', mode: 'all' as MergeMode }, + { label: 'Merge Commits Only', mode: 'merge-only' as MergeMode }, + { label: 'Non-Merge Commits Only', mode: 'no-merge' as MergeMode }, ], - { placeHolder: '合并提交过滤模式' }, + { placeHolder: 'Merge commit filter mode' }, ); if (!pick) { return; @@ -588,12 +607,12 @@ export function registerHistoryCommands( const now = Date.now(); const pick = await vscode.window.showQuickPick( [ - { label: '最近 7 天', days: 7 }, - { label: '最近 30 天', days: 30 }, - { label: '最近 90 天', days: 90 }, - { label: '清除日期过滤', days: 0 }, + { label: 'Last 7 Days', days: 7 }, + { label: 'Last 30 Days', days: 30 }, + { label: 'Last 90 Days', days: 90 }, + { label: 'Clear Date Filter', days: 0 }, ], - { placeHolder: '提交日期范围' }, + { placeHolder: 'Commit date range' }, ); if (!pick) { return; @@ -605,7 +624,7 @@ export function registerHistoryCommands( subs.push( vscode.commands.registerCommand('hyperGit.logFilterRegex', async () => { - const re = await vscode.window.showInputBox({ prompt: '按 message 正则过滤(客户端)', placeHolder: '例如 ^feat:' }); + const re = await vscode.window.showInputBox({ prompt: 'Filter by message regex (client-side)', placeHolder: 'e.g. ^feat:' }); const f = logTree.getFilter(); logTree.setFilter({ ...f, messageRegex: re && re.trim() ? re.trim() : undefined }); }), diff --git a/src/adapter/misc-commands.ts b/src/adapter/misc-commands.ts index 84e2ee9..9527b9b 100644 --- a/src/adapter/misc-commands.ts +++ b/src/adapter/misc-commands.ts @@ -29,11 +29,11 @@ export function registerMiscCommands( } const scope = await vscode.window.showQuickPick( [ - { label: '未暂存改动', args: ['diff'] }, - { label: '已暂存改动', args: ['diff', '--cached'] }, - { label: '全部改动(HEAD↔工作区)', args: ['diff', 'HEAD'] }, + { label: 'Unstaged changes', args: ['diff'] }, + { label: 'Staged changes', args: ['diff', '--cached'] }, + { label: 'All changes (HEAD ↔ Working tree)', args: ['diff', 'HEAD'] }, ], - { placeHolder: '导出哪部分改动为 patch' }, + { placeHolder: 'Which changes to export as patch' }, ); if (!scope) { return; @@ -42,11 +42,11 @@ export function registerMiscCommands( try { patch = await service.execGit(scope.args); } catch (e) { - void vscode.window.showErrorMessage(`生成 patch 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to generate patch: ${errMsg(e)}`); return; } if (!patch.trim()) { - void vscode.window.showInformationMessage('无改动可导出'); + void vscode.window.showInformationMessage('No changes to export'); return; } const target = await vscode.window.showSaveDialog({ @@ -58,9 +58,9 @@ export function registerMiscCommands( } try { await fs.promises.writeFile(target.fsPath, patch, 'utf8'); - void vscode.window.showInformationMessage(`已导出 patch:${target.fsPath}`); + void vscode.window.showInformationMessage(`Patch exported: ${target.fsPath}`); } catch (e) { - void vscode.window.showErrorMessage(`写入 patch 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to write patch: ${errMsg(e)}`); } }), ); @@ -74,18 +74,18 @@ export function registerMiscCommands( const sel = await vscode.window.showOpenDialog({ canSelectMany: false, filters: { Patch: ['patch', 'diff'], All: ['*'] }, - title: '选择要应用的 patch 文件', + title: 'Select a patch file to apply', }); if (!sel?.[0]) { return; } const mode = await vscode.window.showQuickPick( [ - { label: '应用到工作区', args: [] as string[] }, - { label: '应用并暂存(--index)', args: ['--index'] }, - { label: '3-way 应用(冲突可解)', args: ['--3way'] }, + { label: 'Apply to working tree', args: [] as string[] }, + { label: 'Apply and stage (--index)', args: ['--index'] }, + { label: '3-way apply (resolves conflicts)', args: ['--3way'] }, ], - { placeHolder: 'Apply 模式' }, + { placeHolder: 'Apply mode' }, ); if (!mode) { return; @@ -94,10 +94,10 @@ export function registerMiscCommands( await service.execGit(['apply', ...mode.args, sel[0].fsPath]); branchesTree.refresh(); logTree.refresh(); - void vscode.window.showInformationMessage('已应用 patch'); + void vscode.window.showInformationMessage('Patch applied'); } catch (e) { if (!(await handleGitConflict(service, 'Apply Patch'))) { - void vscode.window.showErrorMessage(`应用 patch 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to apply patch: ${errMsg(e)}`); } } }), @@ -111,10 +111,10 @@ export function registerMiscCommands( } try { const out = await service.execGit(['reflog', '-n', '200', '--date=relative']); - const doc = await vscode.workspace.openTextDocument({ content: `# git reflog(最近 200 条)\n\n${out}`, language: 'plaintext' }); + const doc = await vscode.workspace.openTextDocument({ content: `# git reflog (latest 200 entries)\n\n${out}`, language: 'plaintext' }); await vscode.window.showTextDocument(doc, { preview: true }); } catch (e) { - void vscode.window.showErrorMessage(`读取 reflog 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to read reflog: ${errMsg(e)}`); } }), ); diff --git a/src/adapter/partial-commands.ts b/src/adapter/partial-commands.ts index f69f3aa..39134b1 100644 --- a/src/adapter/partial-commands.ts +++ b/src/adapter/partial-commands.ts @@ -40,7 +40,7 @@ export function registerPartialCommands(service: GitRepositoryService, registry: const pickHunks = async (diff: string, title: string): Promise<{ indices: number[] } | undefined> => { const files = parseUnifiedDiff(diff); if (files.length === 0) { - void vscode.window.showInformationMessage('无改动'); + void vscode.window.showInformationMessage('No changes'); return undefined; } const file = files[0]; @@ -50,7 +50,7 @@ export function registerPartialCommands(service: GitRepositoryService, registry: detail: h.body.slice(0, 6).join('\n'), index: i, })); - const sel = await vscode.window.showQuickPick(picks, { canPickMany: true, title, placeHolder: '勾选 hunk(详见 detail)' }); + const sel = await vscode.window.showQuickPick(picks, { canPickMany: true, title, placeHolder: 'Select hunks (see detail)' }); if (!sel || sel.length === 0) { return undefined; } @@ -70,18 +70,18 @@ export function registerPartialCommands(service: GitRepositoryService, registry: try { const diff = await service.execGit(['diff', '--', rel]); if (!diff.trim()) { - void vscode.window.showInformationMessage('该文件无未暂存改动'); + void vscode.window.showInformationMessage('No unstaged changes for this file'); return; } - const sel = await pickHunks(diff, `暂存 hunk:${rel}`); + const sel = await pickHunks(diff, `Stage hunks: ${rel}`); if (!sel) { return; } const file = parseUnifiedDiff(diff)[0]; await applyToIndex(buildPatch(file, sel.indices), false); - void vscode.window.showInformationMessage(`已暂存 ${sel.indices.length} 个 hunk`); + void vscode.window.showInformationMessage(`Staged ${sel.indices.length} hunk(s)`); } catch (e) { - void vscode.window.showErrorMessage(`暂存失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to stage: ${errMsg(e)}`); } }), ); @@ -99,18 +99,18 @@ export function registerPartialCommands(service: GitRepositoryService, registry: try { const diff = await service.execGit(['diff', '--cached', '--', rel]); if (!diff.trim()) { - void vscode.window.showInformationMessage('该文件无已暂存改动'); + void vscode.window.showInformationMessage('No staged changes for this file'); return; } - const sel = await pickHunks(diff, `取消暂存 hunk:${rel}`); + const sel = await pickHunks(diff, `Unstage hunks: ${rel}`); if (!sel) { return; } const file = parseUnifiedDiff(diff)[0]; await applyToIndex(buildPatch(file, sel.indices), true); - void vscode.window.showInformationMessage(`已取消暂存 ${sel.indices.length} 个 hunk`); + void vscode.window.showInformationMessage(`Unstaged ${sel.indices.length} hunk(s)`); } catch (e) { - void vscode.window.showErrorMessage(`取消暂存失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to unstage: ${errMsg(e)}`); } }), ); @@ -124,7 +124,7 @@ export function registerPartialCommands(service: GitRepositoryService, registry: } const rel = repoRelative(repo.rootUri.fsPath, editor.document.uri.fsPath); if (!rel) { - void vscode.window.showWarningMessage('文件不在当前仓库内'); + void vscode.window.showWarningMessage('File is not inside the current repository'); return; } const cursorLine = editor.selection.active.line + 1; @@ -132,7 +132,7 @@ export function registerPartialCommands(service: GitRepositoryService, registry: const diff = await service.execGit(['diff', '--', rel]); const files = parseUnifiedDiff(diff); if (files.length === 0) { - void vscode.window.showInformationMessage('无未暂存改动'); + void vscode.window.showInformationMessage('No unstaged changes'); return; } const file = files[0]; @@ -140,13 +140,13 @@ export function registerPartialCommands(service: GitRepositoryService, registry: .map((h, i) => ({ h, i })) .filter(({ h }) => cursorLine >= h.newStart && cursorLine < h.newStart + Math.max(h.newCount, 1)); if (overlapping.length === 0) { - void vscode.window.showInformationMessage('光标不在改动 hunk 内'); + void vscode.window.showInformationMessage('Cursor is not within a changed hunk'); return; } await applyToIndex(buildPatch(file, overlapping.map((o) => o.i)), false); - void vscode.window.showInformationMessage('已暂存光标处 hunk'); + void vscode.window.showInformationMessage('Staged hunk at cursor'); } catch (e) { - void vscode.window.showErrorMessage(`暂存失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to stage: ${errMsg(e)}`); } }), ); @@ -160,7 +160,7 @@ export function registerPartialCommands(service: GitRepositoryService, registry: } const rel = repoRelative(repo.rootUri.fsPath, editor.document.uri.fsPath); if (!rel) { - void vscode.window.showWarningMessage('文件不在当前仓库内'); + void vscode.window.showWarningMessage('File is not inside the current repository'); return; } const cursorLine = editor.selection.active.line + 1; @@ -175,20 +175,20 @@ export function registerPartialCommands(service: GitRepositoryService, registry: .map((h, i) => ({ h, i })) .filter(({ h }) => cursorLine >= h.newStart && cursorLine < h.newStart + Math.max(h.newCount, 1)); if (overlapping.length === 0) { - void vscode.window.showInformationMessage('光标不在改动 hunk 内'); + void vscode.window.showInformationMessage('Cursor is not within a changed hunk'); return; } const hunkIdx = overlapping[0].i; const defs = registry.listDefs(); const active = registry.activeChangelistId; const picks = defs.map((d) => ({ label: d.name, id: d.id, description: d.id === active ? 'active' : undefined })); - const pick = await vscode.window.showQuickPick(picks, { placeHolder: `将 Hunk ${hunkIdx + 1} 移至 Changelist` }); + const pick = await vscode.window.showQuickPick(picks, { placeHolder: `Move Hunk ${hunkIdx + 1} to Changelist` }); if (pick) { registry.moveHunk(rel, hunkIdx, pick.id); - void vscode.window.showInformationMessage(`已将 Hunk ${hunkIdx + 1}(${rel})分配到「${pick.label}」`); + void vscode.window.showInformationMessage(`Assigned Hunk ${hunkIdx + 1} (${rel}) to "${pick.label}"`); } } catch (e) { - void vscode.window.showErrorMessage(`分配失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to assign: ${errMsg(e)}`); } }), ); @@ -206,7 +206,7 @@ async function pickUnstagedFile(service: GitRepositoryService): Promise ({ label: c.relativePath })), { placeHolder: '选择文件' }); + const pick = await vscode.window.showQuickPick(items.map((c) => ({ label: c.relativePath })), { placeHolder: 'Select a file' }); return pick?.label; } @@ -215,6 +215,6 @@ async function pickStagedFile(service: GitRepositoryService): Promise ({ label: c.relativePath })), { placeHolder: '选择已暂存文件' }); + const pick = await vscode.window.showQuickPick(items.map((c) => ({ label: c.relativePath })), { placeHolder: 'Select a staged file' }); return pick?.label; } diff --git a/src/adapter/remote-commands.ts b/src/adapter/remote-commands.ts index a8c8949..62341b8 100644 --- a/src/adapter/remote-commands.ts +++ b/src/adapter/remote-commands.ts @@ -37,7 +37,7 @@ export function registerRemoteCommands( } const remotes = repo.state.remotes.map((r) => r.name); if (remotes.length === 0) { - void vscode.window.showWarningMessage('未配置远程仓库(remote)'); + void vscode.window.showWarningMessage('No remote repository configured'); return; } // 待推送提交预览(best-effort,无 upstream 时忽略) @@ -45,41 +45,41 @@ export function registerRemoteCommands( try { const out = (await service.execGit(['log', '@{u}..HEAD', '--oneline'])).trim(); if (out) { - previewCount = `(${out.split('\n').length} 个待推送提交)`; + previewCount = ` (${out.split('\n').length} commit(s) to push)`; } } catch { /* 无上游,忽略 */ } - const remote = await vscode.window.showQuickPick(remotes, { placeHolder: `选择目标 remote ${previewCount}` }); + const remote = await vscode.window.showQuickPick(remotes, { placeHolder: `Select target remote ${previewCount}` }); if (!remote) { return; } const mode = await vscode.window.showQuickPick( [ - { label: 'Normal', description: '普通推送', force: undefined as 0 | 1 | undefined }, - { label: 'Force-with-lease', description: '安全强推(推荐)', force: 1 as const }, - { label: 'Force', description: '⚠ 强制覆盖远程(危险)', force: 0 as const }, + { label: 'Normal', description: 'Normal push', force: undefined as 0 | 1 | undefined }, + { label: 'Force-with-lease', description: 'Safe force push (recommended)', force: 1 as const }, + { label: 'Force', description: '⚠ Force overwrite remote (dangerous)', force: 0 as const }, ], - { placeHolder: '推送模式' }, + { placeHolder: 'Push mode' }, ); if (!mode) { return; } - const tags = await vscode.window.showQuickPick(['否', '是'], { placeHolder: '同时推送 tags?' }); + const tags = await vscode.window.showQuickPick(['No', 'Yes'], { placeHolder: 'Push tags as well?' }); if (!tags) { return; } try { await repo.push(remote, undefined, false, mode.force); - if (tags === '是') { + if (tags === 'Yes') { await service.execGit(['push', remote, '--tags']); } branchesTree.refresh(); logTree.refresh(); - void vscode.window.showInformationMessage(`已推送到 ${remote} ${previewCount}`.trim()); + void vscode.window.showInformationMessage(`Pushed to ${remote} ${previewCount}`.trim()); } catch (e) { if (!(await handleGitConflict(service, 'Push'))) { - void vscode.window.showErrorMessage(`Push 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Push failed: ${errMsg(e)}`); } } }), @@ -93,10 +93,10 @@ export function registerRemoteCommands( } const pick = await vscode.window.showQuickPick( [ - { label: 'Merge 策略(默认)', description: 'pull --no-rebase', args: ['pull', '--no-rebase'] }, - { label: 'Rebase 策略', description: 'pull --rebase(线性历史)', args: ['pull', '--rebase'] }, + { label: 'Merge strategy (default)', description: 'pull --no-rebase', args: ['pull', '--no-rebase'] }, + { label: 'Rebase strategy', description: 'pull --rebase (linear history)', args: ['pull', '--rebase'] }, ], - { placeHolder: 'Update Project:拉取并整合远程变更' }, + { placeHolder: 'Update Project: pull and integrate remote changes' }, ); if (!pick) { return; @@ -105,10 +105,10 @@ export function registerRemoteCommands( await service.execGit(pick.args); branchesTree.refresh(); logTree.refresh(); - void vscode.window.showInformationMessage('Update Project 完成'); + void vscode.window.showInformationMessage('Update Project complete'); } catch (e) { if (!(await handleGitConflict(service, 'Update'))) { - void vscode.window.showErrorMessage(`Update 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Update failed: ${errMsg(e)}`); } } }), @@ -121,24 +121,24 @@ export function registerRemoteCommands( return; } const refs = repo.state.refs.filter((r) => r.name && (r.type === 0 || r.type === 1)); - const target = await vscode.window.showQuickPick(refs.map((r) => r.name!), { placeHolder: '合并哪个分支到当前分支' }); + const target = await vscode.window.showQuickPick(refs.map((r) => r.name!), { placeHolder: 'Merge which branch into the current branch' }); if (!target) { return; } const mode = await vscode.window.showQuickPick( [ - { label: 'Fast-forward only', description: '仅当可快进时合并', args: ['--ff-only'] }, - { label: 'No fast-forward', description: '始终创建合并提交', args: ['--no-ff'] }, - { label: 'Squash', description: '压缩为单个提交(不产生合并提交)', args: ['--squash'] }, + { label: 'Fast-forward only', description: 'Merge only when fast-forward is possible', args: ['--ff-only'] }, + { label: 'No fast-forward', description: 'Always create a merge commit', args: ['--no-ff'] }, + { label: 'Squash', description: 'Squash into a single commit (no merge commit)', args: ['--squash'] }, ], - { placeHolder: `合并模式(${target} → 当前分支)` }, + { placeHolder: `Merge mode (${target} → current branch)` }, ); if (!mode) { return; } const msgArgs: string[] = []; if (mode.args[0] !== '--ff-only') { - const msg = await vscode.window.showInputBox({ prompt: '合并提交信息', value: `Merge ${target}` }); + const msg = await vscode.window.showInputBox({ prompt: 'Merge commit message', value: `Merge ${target}` }); if (msg && msg.trim()) { msgArgs.push('-m', msg.trim()); } @@ -147,10 +147,10 @@ export function registerRemoteCommands( await service.execGit(['merge', ...mode.args, ...msgArgs, target]); branchesTree.refresh(); logTree.refresh(); - void vscode.window.showInformationMessage(`已合并 ${target}`); + void vscode.window.showInformationMessage(`Merged ${target}`); } catch (e) { - if (!(await handleGitConflict(service, '合并'))) { - void vscode.window.showErrorMessage(`合并失败:${errMsg(e)}`); + if (!(await handleGitConflict(service, 'Merge'))) { + void vscode.window.showErrorMessage(`Merge failed: ${errMsg(e)}`); } } }), @@ -169,7 +169,7 @@ export function registerRemoteCommands( } const remotes = repo.state.remotes.map((r) => r.name); if (remotes.length === 0) { - void vscode.window.showWarningMessage('未配置远程仓库(remote)'); + void vscode.window.showWarningMessage('No remote repository configured'); return; } // 用已知 remotes 做最长前缀匹配解析 {remote, branch};丢弃无法归属的脏数据。 @@ -177,13 +177,13 @@ export function registerRemoteCommands( .map((r) => resolveRemoteBranch(r.shortName, remotes)) .filter((t): t is RemoteBranchTarget => t !== null); if (targets.length === 0) { - void vscode.window.showWarningMessage('无法解析所选远程分支的归属 remote'); + void vscode.window.showWarningMessage('Could not resolve the owning remote of the selected remote branches'); return; } // 受保护主干(main/master)硬阻断——与本地删除硬阻断当前 HEAD 对称。 const { deletable, protectedTargets } = partitionRemoteByProtected(targets); if (deletable.length === 0) { - void vscode.window.showWarningMessage(`已跳过受保护分支:${truncateNames(protectedTargets.map((t) => t.shortName))}`); + void vscode.window.showWarningMessage(`Skipped protected branches: ${truncateNames(protectedTargets.map((t) => t.shortName))}`); return; } // 软警示:待删集合是否含当前分支上游(删之不致命,但令当前分支失远程追踪)。 @@ -193,7 +193,7 @@ export function registerRemoteCommands( // 受保护跳过项透明并入文案。 const fullDetail = protectedTargets.length > 0 - ? `${detail}\n\n已自动跳过受保护分支:${truncateNames(protectedTargets.map((t) => t.shortName))}` + ? `${detail}\n\nAutomatically skipped protected branches: ${truncateNames(protectedTargets.map((t) => t.shortName))}` : detail; const choice = await vscode.window.showWarningMessage(fullDetail, { modal: true }, confirmLabel); if (choice !== confirmLabel) { @@ -221,11 +221,11 @@ export function registerRemoteCommands( branchesTree.refresh(); if (failures.length === 0) { void vscode.window.showInformationMessage( - succeeded.length === 1 ? `已删除远程分支 ${succeeded[0].shortName}` : `已删除 ${succeeded.length} 个远程分支`, + succeeded.length === 1 ? `Deleted remote branch ${succeeded[0].shortName}` : `Deleted ${succeeded.length} remote branches`, ); } else { void vscode.window.showWarningMessage( - `已删除 ${succeeded.length} 个远程分支,${failures.length} 个失败:${truncateNames(failures)}`, + `Deleted ${succeeded.length} remote branches, ${failures.length} failed: ${truncateNames(failures)}`, ); } }), diff --git a/src/adapter/shelf.ts b/src/adapter/shelf.ts index d824763..77302b5 100644 --- a/src/adapter/shelf.ts +++ b/src/adapter/shelf.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import type { GitRepositoryService } from './git-repository-service'; import { handleGitConflict } from './conflict-ui'; +import { mdTooltip, relativeDate } from './tree/tree-tooltip'; const errMsg = (e: unknown): string => (e instanceof Error ? e.message : String(e)); @@ -26,6 +27,14 @@ export interface ShelfNode { readonly timestamp: string; } +/** Shelf 内单文件叶子(展开 shelf 节点暴露其包含的文件路径)。 */ +export interface ShelfFileNode { + readonly kind: 'file'; + readonly path: string; +} + +export type ShelfTreeNode = ShelfNode | ShelfFileNode; + /** * ShelfService:基于 patch 的 Shelf 实现(独立于 git stash)。 * @@ -45,11 +54,11 @@ export class ShelfService { async shelve(name: string, paths: readonly string[], timestamp: string): Promise { const repo = this.service.repo; if (!repo) { - throw new Error('未找到 Git 仓库'); + throw new Error('No Git repository found'); } const patch = await this.service.execGit(['diff', '--', ...paths]); if (!patch.trim()) { - throw new Error('所选文件无改动(或为未跟踪文件)'); + throw new Error('Selected files have no changes (or are untracked)'); } fs.mkdirSync(this.shelvesDir, { recursive: true }); const entry: ShelfEntry = { name, paths, timestamp, patch }; @@ -61,7 +70,7 @@ export class ShelfService { async unshelve(name: string, threeWay: boolean): Promise { const entry = this.readEntry(name); if (!entry) { - throw new Error(`Shelf「${name}」不存在`); + throw new Error(`Shelf "${name}" does not exist`); } const tmp = path.join(os.tmpdir(), `hg-unshelve-${Date.now()}.patch`); fs.writeFileSync(tmp, entry.patch); @@ -124,9 +133,9 @@ export class ShelfService { } } -/** Shelf TreeView:显示已存储的 shelf 条目。 */ -export class ShelfTreeProvider implements vscode.TreeDataProvider, vscode.Disposable { - private readonly _onDidChange = new vscode.EventEmitter(); +/** Shelf TreeView:显示已存储的 shelf 条目,展开可见其包含的文件。 */ +export class ShelfTreeProvider implements vscode.TreeDataProvider, vscode.Disposable { + private readonly _onDidChange = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChange.event; constructor(private readonly shelfService: ShelfService) {} @@ -135,17 +144,38 @@ export class ShelfTreeProvider implements vscode.TreeDataProvider, vs this._onDidChange.fire(undefined); } - getChildren(): ShelfNode[] { - return this.shelfService.listShelves(); + getChildren(element?: ShelfTreeNode): ShelfTreeNode[] { + if (!element) { + return this.shelfService.listShelves(); + } + if (element.kind === 'shelf') { + return element.paths.map((p): ShelfFileNode => ({ kind: 'file', path: p })); + } + return []; } - getTreeItem(node: ShelfNode): vscode.TreeItem { - const item = new vscode.TreeItem(node.name, vscode.TreeItemCollapsibleState.None); + getTreeItem(node: ShelfTreeNode): vscode.TreeItem { + if (node.kind === 'file') { + const item = new vscode.TreeItem(node.path, vscode.TreeItemCollapsibleState.None); + item.id = `shelf-file:${node.path}`; + item.contextValue = 'hyperGit.shelfFile'; + item.tooltip = mdTooltip([['Path', node.path]]); + return item; + } + const count = node.paths.length; + const item = new vscode.TreeItem(node.name, vscode.TreeItemCollapsibleState.Collapsed); item.id = `shelf:${node.name}`; - item.description = `${node.paths.length} files · ${node.timestamp}`; - item.tooltip = `${node.name}\n${node.paths.length} files\nShelved: ${node.timestamp}`; + item.description = `${count} ${count === 1 ? 'file' : 'files'} · ${relativeDate(node.timestamp)}`; item.contextValue = 'hyperGit.shelf'; - item.iconPath = new vscode.ThemeIcon('inbox'); + item.iconPath = new vscode.ThemeIcon('library'); + item.tooltip = mdTooltip( + [ + ['Files', String(count)], + ['Shelved', relativeDate(node.timestamp)], + ['Timestamp', node.timestamp], + ], + { title: node.name }, + ); return item; } @@ -166,16 +196,16 @@ export function registerShelfCommands(service: GitRepositoryService, shelfServic } const changes = service.getChanges().filter((c) => !c.staged); if (changes.length === 0) { - void vscode.window.showInformationMessage('无未暂存改动可 shelve'); + void vscode.window.showInformationMessage('No unstaged changes to shelve'); return; } - const name = await vscode.window.showInputBox({ prompt: 'Shelf 名称', placeHolder: '例如 feature-x-wip' }); + const name = await vscode.window.showInputBox({ prompt: 'Shelf name', placeHolder: 'e.g. feature-x-wip' }); if (!name || !name.trim()) { return; } const picks = await vscode.window.showQuickPick( changes.map((c) => ({ label: c.relativePath, picked: true })), - { canPickMany: true, title: '选择要 shelve 的文件' }, + { canPickMany: true, title: 'Select files to shelve' }, ); if (!picks || picks.length === 0) { return; @@ -183,9 +213,9 @@ export function registerShelfCommands(service: GitRepositoryService, shelfServic try { await shelfService.shelve(name.trim(), picks.map((p) => p.label), new Date().toISOString()); shelfTree.refresh(); - void vscode.window.showInformationMessage(`已 shelve「${name.trim()}」(${picks.length} files)`); + void vscode.window.showInformationMessage(`Shelved "${name.trim()}" (${picks.length} files)`); } catch (e) { - void vscode.window.showErrorMessage(`Shelve 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to shelve: ${errMsg(e)}`); } }), ); @@ -198,9 +228,9 @@ export function registerShelfCommands(service: GitRepositoryService, shelfServic try { await shelfService.unshelveAndDrop(node.name, false); shelfTree.refresh(); - void vscode.window.showInformationMessage(`已 unshelve「${node.name}」`); + void vscode.window.showInformationMessage(`Unshelved "${node.name}"`); } catch (e) { - void vscode.window.showErrorMessage(`Unshelve 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to unshelve: ${errMsg(e)}`); } }), ); @@ -213,10 +243,10 @@ export function registerShelfCommands(service: GitRepositoryService, shelfServic try { await shelfService.unshelveAndDrop(node.name, true); shelfTree.refresh(); - void vscode.window.showInformationMessage(`已 unshelve(3-way)「${node.name}」`); + void vscode.window.showInformationMessage(`Unshelved "${node.name}" (3-way)`); } catch (e) { if (!(await handleGitConflict(service, 'Unshelve'))) { - void vscode.window.showErrorMessage(`Unshelve 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to unshelve: ${errMsg(e)}`); } } }), @@ -227,8 +257,8 @@ export function registerShelfCommands(service: GitRepositoryService, shelfServic if (!node) { return; } - const ok = await vscode.window.showWarningMessage(`删除 Shelf「${node.name}」?`, { modal: true }, '删除'); - if (ok === '删除') { + const ok = await vscode.window.showWarningMessage(`Delete shelf "${node.name}"?`, { modal: true }, 'Delete'); + if (ok === 'Delete') { shelfService.drop(node.name); shelfTree.refresh(); } diff --git a/src/adapter/stash-commands.ts b/src/adapter/stash-commands.ts index 8addbf3..5ba9930 100644 --- a/src/adapter/stash-commands.ts +++ b/src/adapter/stash-commands.ts @@ -13,18 +13,40 @@ export function registerStashCommands(service: GitRepositoryService, stashTree: const subs: vscode.Disposable[] = []; const errMsg = (e: unknown): string => (e instanceof Error ? e.message : String(e)); + subs.push( + vscode.commands.registerCommand('hyperGit.stashView', async (node?: StashNode) => { + // 只读查看 stash diff(单击叶子触发;比 apply 更安全,apply/pop/drop 仍在右键菜单)。 + const repo = service.repo; + if (!repo) { + return; + } + const index = node?.kind === 'stash' ? node.index : 0; + try { + const patch = await service.execGit(['stash', 'show', '-p', '--no-color', `stash@{${index}}`]); + if (!patch.trim()) { + void vscode.window.showInformationMessage(`stash@{${index}} has no changes to show`); + return; + } + const doc = await vscode.workspace.openTextDocument({ content: patch, language: 'diff' }); + await vscode.window.showTextDocument(doc, { preview: true }); + } catch (e) { + void vscode.window.showErrorMessage(`Failed to show stash: ${errMsg(e)}`); + } + }), + ); + subs.push( vscode.commands.registerCommand('hyperGit.stashCreate', async () => { const repo = service.repo; if (!repo) { return; } - const message = await vscode.window.showInputBox({ prompt: 'Stash 信息(可空)', placeHolder: 'WIP' }); + const message = await vscode.window.showInputBox({ prompt: 'Stash message (optional)', placeHolder: 'WIP' }); try { await repo.createStash({ message: message && message.trim() ? message.trim() : undefined, includeUntracked: true }); stashTree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`Stash 创建失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to create stash: ${errMsg(e)}`); } }), ); @@ -40,7 +62,7 @@ export function registerStashCommands(service: GitRepositoryService, stashTree: await repo.applyStash(index); stashTree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`Stash 应用失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to apply stash: ${errMsg(e)}`); } }), ); @@ -57,7 +79,7 @@ export function registerStashCommands(service: GitRepositoryService, stashTree: stashTree.refresh(); } catch (e) { if (!(await handleGitConflict(service, 'Stash pop'))) { - void vscode.window.showErrorMessage(`Stash pop 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to pop stash: ${errMsg(e)}`); } } }), @@ -70,13 +92,13 @@ export function registerStashCommands(service: GitRepositoryService, stashTree: return; } const index = node?.kind === 'stash' ? node.index : 0; - const choice = await vscode.window.showWarningMessage(`删除 stash@{${index}}?`, { modal: true }, '删除'); - if (choice === '删除') { + const choice = await vscode.window.showWarningMessage(`Drop stash@{${index}}?`, { modal: true }, 'Drop'); + if (choice === 'Drop') { try { await repo.dropStash(index); stashTree.refresh(); } catch (e) { - void vscode.window.showErrorMessage(`Stash 删除失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to drop stash: ${errMsg(e)}`); } } }), @@ -90,7 +112,7 @@ export function registerStashCommands(service: GitRepositoryService, stashTree: if (!repo) { return; } - const message = await vscode.window.showInputBox({ prompt: 'Stash 信息(保留已暂存改动在工作区)', placeHolder: 'WIP' }); + const message = await vscode.window.showInputBox({ prompt: 'Stash message (keep staged changes in the working tree)', placeHolder: 'WIP' }); try { const args = ['stash', 'push', '--keep-index']; if (message && message.trim()) { @@ -98,9 +120,9 @@ export function registerStashCommands(service: GitRepositoryService, stashTree: } await service.execGit(args); stashTree.refresh(); - void vscode.window.showInformationMessage('已 Stash(保留已暂存)'); + void vscode.window.showInformationMessage('Stashed (index kept)'); } catch (e) { - void vscode.window.showErrorMessage(`Stash 失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to stash: ${errMsg(e)}`); } }), ); @@ -111,16 +133,16 @@ export function registerStashCommands(service: GitRepositoryService, stashTree: if (!repo) { return; } - const ok = await vscode.window.showWarningMessage('清空所有 stash(不可撤销)?', { modal: true }, '清空'); - if (ok !== '清空') { + const ok = await vscode.window.showWarningMessage('Clear ALL stashes (irreversible)?', { modal: true }, 'Clear All'); + if (ok !== 'Clear All') { return; } try { await service.execGit(['stash', 'clear']); stashTree.refresh(); - void vscode.window.showInformationMessage('已清空所有 stash'); + void vscode.window.showInformationMessage('Cleared all stashes'); } catch (e) { - void vscode.window.showErrorMessage(`清空失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to clear stashes: ${errMsg(e)}`); } }), ); @@ -145,29 +167,29 @@ export function registerStashCommands(service: GitRepositoryService, stashTree: }) .filter((x): x is { label: string; description: string; index: number } => x !== null); if (items.length === 0) { - void vscode.window.showInformationMessage('无 stash'); + void vscode.window.showInformationMessage('No stashes'); return; } - const pick = await vscode.window.showQuickPick(items, { placeHolder: '选择要转为分支的 stash' }); + const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Select a stash to turn into a branch' }); if (!pick) { return; } index = pick.index; } catch (e) { - void vscode.window.showErrorMessage(`读取 stash 列表失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to read stash list: ${errMsg(e)}`); return; } } - const name = await vscode.window.showInputBox({ prompt: `从 stash@{${index}} 创建并检出新分支`, placeHolder: '新分支名' }); + const name = await vscode.window.showInputBox({ prompt: `Create and checkout a new branch from stash@{${index}}`, placeHolder: 'New branch name' }); if (!name || !name.trim()) { return; } try { await service.execGit(['stash', 'branch', name.trim(), `stash@{${index}}`]); stashTree.refresh(); - void vscode.window.showInformationMessage(`已从 stash@{${index}} 创建分支 ${name.trim()}`); + void vscode.window.showInformationMessage(`Created branch ${name.trim()} from stash@{${index}}`); } catch (e) { - void vscode.window.showErrorMessage(`创建分支失败:${errMsg(e)}`); + void vscode.window.showErrorMessage(`Failed to create branch: ${errMsg(e)}`); } }), ); diff --git a/src/adapter/tree/branches-tree.ts b/src/adapter/tree/branches-tree.ts index 341ab9a..4168b38 100644 --- a/src/adapter/tree/branches-tree.ts +++ b/src/adapter/tree/branches-tree.ts @@ -3,6 +3,7 @@ import type { Ref } from '../../types/git'; import type { GitRepositoryService } from '../git-repository-service'; import type { BranchFavorites } from '../branch-favorites'; import { FOR_EACH_REF_FORMAT, type RawRef, parseForEachRef } from '../../engine/ref/for-each-ref'; +import { mdTooltip } from './tree-tooltip'; export type BranchGroupId = 'favorites' | 'local' | 'remote' | 'tags'; @@ -10,6 +11,8 @@ export interface BranchGroupNode { readonly kind: 'group'; readonly id: BranchGroupId; readonly label: string; + /** 分组内条目数(标题栏描述显示计数徽标)。 */ + readonly count: number; } export interface BranchRefNode { @@ -55,16 +58,21 @@ export class BranchesTreeProvider implements vscode.TreeDataProvider } if (!element) { const refs = await this.loadRefs(); + const byName = new Map(refs.map((r) => [r.shortName, r] as const)); + const favCount = this.favorites.list().filter((n) => byName.has(n)).length; + const localCount = refs.filter((r) => !r.isRemote && !r.isTag).length; + const remoteCount = refs.filter((r) => r.isRemote).length; + const tagCount = refs.filter((r) => r.isTag).length; const groups: BranchGroupNode[] = []; - if (this.favorites.list().length > 0) { - groups.push({ kind: 'group', id: 'favorites', label: 'Favorites' }); + if (favCount > 0) { + groups.push({ kind: 'group', id: 'favorites', label: 'Favorites', count: favCount }); } groups.push( - { kind: 'group', id: 'local', label: 'Local Branches' }, - { kind: 'group', id: 'remote', label: 'Remote Branches' }, + { kind: 'group', id: 'local', label: 'Local Branches', count: localCount }, + { kind: 'group', id: 'remote', label: 'Remote Branches', count: remoteCount }, ); - if (refs.some((r) => r.isTag)) { - groups.push({ kind: 'group', id: 'tags', label: 'Tags' }); + if (tagCount > 0) { + groups.push({ kind: 'group', id: 'tags', label: 'Tags', count: tagCount }); } return groups; } @@ -165,6 +173,7 @@ export class BranchesTreeProvider implements vscode.TreeDataProvider if (element.kind === 'group') { const item = new vscode.TreeItem(element.label, vscode.TreeItemCollapsibleState.Expanded); item.contextValue = 'hyperGit.branchGroup'; + item.description = String(element.count); const icon = element.id === 'remote' ? 'repo' : element.id === 'tags' ? 'tag' : element.id === 'favorites' ? 'star-full' : 'git-branch'; item.iconPath = new vscode.ThemeIcon(icon); @@ -175,18 +184,21 @@ export class BranchesTreeProvider implements vscode.TreeDataProvider const fav = this.favorites.isFavorite(ref.shortName); const isTag = ref.isTag; const item = new vscode.TreeItem(ref.shortName, vscode.TreeItemCollapsibleState.None); - item.description = this.describe(ref, active, fav); + item.description = this.describe(ref, active); item.contextValue = isTag ? 'hyperGit.tag' : element.remote ? 'hyperGit.remoteBranch' : 'hyperGit.branch'; item.tooltip = this.tooltip(ref, active, fav); const icon = isTag ? 'tag' : element.remote ? 'cloud' : 'git-branch'; - item.iconPath = new vscode.ThemeIcon( - icon, - active ? new vscode.ThemeColor('gitDecoration.modifiedResourceForeground') : undefined, - ); + // 活动分支 = charts.blue(与 Log 本地分支 chip 同语义,全局一致);收藏(非活动)= charts.yellow。 + const color = active + ? new vscode.ThemeColor('charts.blue') + : fav + ? new vscode.ThemeColor('charts.yellow') + : undefined; + item.iconPath = new vscode.ThemeIcon(icon, color); return item; } - private describe(ref: RawRef, active: boolean, fav: boolean): string { + private describe(ref: RawRef, active: boolean): string { const parts: string[] = []; if (active) { parts.push('active'); @@ -197,17 +209,20 @@ export class BranchesTreeProvider implements vscode.TreeDataProvider if (ref.behind) { parts.push(`↓${ref.behind}`); } - } else if (ref.objectname) { - parts.push(ref.objectname); - } - if (fav) { - parts.push('★'); } return parts.join(' '); } - private tooltip(ref: RawRef, active: boolean, fav: boolean): string { - const lines = [ref.shortName + (active ? ' (active)' : '') + (fav ? ' ★' : '')]; + private tooltip(ref: RawRef, active: boolean, fav: boolean): vscode.MarkdownString { + const rows: Array<[string, string]> = []; + if (active) { + rows.push(['State', 'Current branch (HEAD)']); + } else if (fav) { + rows.push(['State', 'Favorite']); + } + if (ref.objectname) { + rows.push([ref.isTag ? 'Tag' : 'Commit', ref.objectname]); + } if (ref.upstream) { const track: string[] = []; if (ref.ahead) { @@ -216,12 +231,9 @@ export class BranchesTreeProvider implements vscode.TreeDataProvider if (ref.behind) { track.push(`behind ${ref.behind}`); } - lines.push(`← ${ref.upstream}${track.length ? ` (${track.join(', ')})` : ''}`); - } - if (ref.isTag) { - lines.push(`tag @ ${ref.objectname}`); + rows.push(['Upstream', `${ref.upstream}${track.length ? ` (${track.join(', ')})` : ''}`]); } - return lines.join('\n'); + return mdTooltip(rows, { title: ref.shortName }); } /** 判定分支是否当前 HEAD:CLI 解析的 head 标记优先,API 路径回退与 state.HEAD.name 比较。 */ diff --git a/src/adapter/tree/changes-tree.ts b/src/adapter/tree/changes-tree.ts index ed2ac41..11ba4c4 100644 --- a/src/adapter/tree/changes-tree.ts +++ b/src/adapter/tree/changes-tree.ts @@ -4,6 +4,7 @@ import { fileStatusLabel } from '../../engine/model'; import { getDecoration } from '../../engine/scm-mapping/status-decoration'; import type { ChangelistRegistry } from '../changelist-registry'; import type { ChangeItem, GitRepositoryService } from '../git-repository-service'; +import { mdTooltip } from './tree-tooltip'; export interface ChangesChangelistNode { readonly kind: 'changelist'; @@ -73,9 +74,16 @@ export class ChangesTreeProvider implements vscode.TreeDataProvider const item = new vscode.TreeItem(node.name, state); item.contextValue = 'hyperGit.changelist'; item.id = `cl:${node.id}`; - item.iconPath = new vscode.ThemeIcon('folder'); + // 活动 changelist 用 folder-opened 区分(视觉锚定提交目标)。 + item.iconPath = new vscode.ThemeIcon(node.active ? 'folder-opened' : 'folder'); item.description = `${count} ${count === 1 ? 'file' : 'files'}${node.active ? ' · active' : ''}`; - item.tooltip = `${node.name}${node.active ? ' (active)' : ''}\n${count} changes`; + item.tooltip = mdTooltip( + [ + ['Status', node.active ? 'Active changelist' : 'Changelist'], + ['Changes', String(count)], + ], + { title: node.name }, + ); return item; } @@ -88,9 +96,15 @@ export class ChangesTreeProvider implements vscode.TreeDataProvider treeItem.id = `file:${change.relativePath}`; treeItem.resourceUri = change.uri; treeItem.description = `${decoration.letter}${dir && dir !== '.' ? ' · ' + dir : ''}`; - treeItem.tooltip = `${change.relativePath}\n状态:${fileStatusLabel(change.status)}${change.staged ? '(已暂存)' : ''}`; + // Deleted → tooltip 路径以删除线呈现(消费 strikeThrough 语义位;TreeItem 稳定 API 无行删除线字段)。 + const pathMd = decoration.strikeThrough ? `~~${change.relativePath}~~` : `\`${change.relativePath}\``; + const md = new vscode.MarkdownString('', true); + md.isTrusted = false; + md.appendMarkdown(`${pathMd} \n`); + md.appendMarkdown(`**Status:** ${fileStatusLabel(change.status)}${change.staged ? ' (staged)' : ''} \n`); + treeItem.tooltip = md; treeItem.iconPath = new vscode.ThemeIcon('circle-filled', new vscode.ThemeColor(decoration.themeColor)); - treeItem.command = { command: 'hyperGit.openDiff', title: '打开 Diff', arguments: [change] }; + treeItem.command = { command: 'hyperGit.openDiff', title: 'Open Diff', arguments: [change] }; return treeItem; } } diff --git a/src/adapter/tree/stash-tree.ts b/src/adapter/tree/stash-tree.ts index 2543bb7..f2045dc 100644 --- a/src/adapter/tree/stash-tree.ts +++ b/src/adapter/tree/stash-tree.ts @@ -1,10 +1,13 @@ import * as vscode from 'vscode'; import type { GitRepositoryService } from '../git-repository-service'; +import { mdTooltip } from './tree-tooltip'; export interface StashEntryNode { readonly kind: 'stash'; readonly index: number; readonly message: string; + /** 相对时间(来自 git stash list --date=relative),可能缺失。 */ + readonly date?: string; } export type StashNode = StashEntryNode; @@ -32,13 +35,22 @@ export class StashTreeProvider implements vscode.TreeDataProvider, vs return []; } try { - const out = await this.service.execGit(['stash', 'list']); + const out = await this.service.execGit(['stash', 'list', '--date=relative']); return out .split('\n') .filter((line) => line.trim().length > 0) .map((line): StashEntryNode => { const m = line.match(/^stash@\{(\d+)\}:\s*(.*)$/); - return { kind: 'stash', index: m ? Number(m[1]) : 0, message: m ? m[2] : line }; + const index = m ? Number(m[1]) : 0; + let rest = m ? m[2] : line; + let date: string | undefined; + // --date=relative 追加形如 "(2 days ago)" 的尾缀:拆出日期,其余留作 message。 + const dm = rest.match(/\s*\(([^)]+)\)\s*$/); + if (dm && dm.index !== undefined) { + date = dm[1]; + rest = rest.slice(0, dm.index); + } + return { kind: 'stash', index, message: rest, date }; }); } catch { return []; @@ -47,12 +59,20 @@ export class StashTreeProvider implements vscode.TreeDataProvider, vs getTreeItem(node: StashNode): vscode.TreeItem { const subject = node.message.split(':').slice(1).join(':').trim() || node.message; - const item = new vscode.TreeItem(subject.slice(0, 60), vscode.TreeItemCollapsibleState.None); + const trimmed = subject.length > 60 ? `${subject.slice(0, 60)}…` : subject; + const item = new vscode.TreeItem(trimmed, vscode.TreeItemCollapsibleState.None); item.id = `stash:${node.index}`; - item.description = `stash@{${node.index}}`; - item.tooltip = `stash@{${node.index}}\n${node.message}`; + item.description = node.date || `stash@{${node.index}}`; item.contextValue = 'hyperGit.stash'; item.iconPath = new vscode.ThemeIcon('archive'); + const rows: Array<[string, string]> = [['Ref', `stash@{${node.index}}`]]; + if (node.date) { + rows.push(['Date', node.date]); + } + rows.push(['Message', subject]); + item.tooltip = mdTooltip(rows); + // 单击查看 stash diff(只读,比 apply 更安全;apply/pop/drop 仍在右键菜单)。 + item.command = { command: 'hyperGit.stashView', title: 'View Stash Diff', arguments: [node] }; return item; } diff --git a/src/adapter/tree/tree-tooltip.ts b/src/adapter/tree/tree-tooltip.ts new file mode 100644 index 0000000..5de255b --- /dev/null +++ b/src/adapter/tree/tree-tooltip.ts @@ -0,0 +1,59 @@ +import * as vscode from 'vscode'; + +/** + * 树 Tooltip 共享构造器(MarkdownString,单一下沉入口)。 + * + * 设计:可选标题行 + 一组 `**label:** value` 行;value 按是否含路径/分隔符自动 code-fence(提升 ref/路径可读性)。 + * 取代各树 `\n` 拼接的纯文本 tooltip,统一粗体标签 + code 样式,深/浅主题自适应。 + */ +export function mdTooltip(rows: ReadonlyArray, opts?: { title?: string }): vscode.MarkdownString { + const md = new vscode.MarkdownString('', true); + md.isTrusted = false; + md.supportThemeIcons = true; + if (opts?.title) { + md.appendMarkdown(`**${escapeMd(opts.title)}**\n\n`); + } + for (const [label, value] of rows) { + if (!value) { + continue; + } + const isPathLike = /[/.]/.test(value); // 路径或 ref → code 包裹 + md.appendMarkdown(`**${escapeMd(label)}:** ${isPathLike ? '`' + escapeMd(value) + '`' : escapeMd(value)} \n`); + } + return md; +} + +/** MarkdownString 转义:避免反引号/管道/反斜杠破坏 code-fence 或表格语义。 */ +function escapeMd(s: string): string { + return s.replace(/([\\`|])/g, '\\$1'); +} + +/** + * ISO 时间 → 相对人可读描述("just now" / "N min ago" / "N hr ago" / "N days ago" / "Mon D")。 + * 用于 stash/shelf 行内描述;技术标识(stash@{n} / 原始 ISO)保留在 Tooltip。 + * 解析失败时原样返回,不阻断渲染。 + */ +export function relativeDate(iso: string): string { + const t = new Date(iso).getTime(); + if (Number.isNaN(t)) { + return iso; + } + const diff = Date.now() - t; + const min = 60_000; + const hr = 3_600_000; + const day = 86_400_000; + if (diff < min) { + return 'just now'; + } + if (diff < hr) { + return `${Math.floor(diff / min)} min ago`; + } + if (diff < day) { + return `${Math.floor(diff / hr)} hr ago`; + } + const days = Math.floor(diff / day); + if (days < 30) { + return `${days} day${days === 1 ? '' : 's'} ago`; + } + return new Date(t).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +} diff --git a/src/adapter/tree/worktree-tree.ts b/src/adapter/tree/worktree-tree.ts index 0803d95..778b773 100644 --- a/src/adapter/tree/worktree-tree.ts +++ b/src/adapter/tree/worktree-tree.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import type { GitRepositoryService } from '../git-repository-service'; import { parseWorktreeList, type ParsedWorktree } from '../../engine/worktree/worktree-list'; +import { mdTooltip } from './tree-tooltip'; /** Worktrees 视图节点:单个工作树(扁平,无分组——工作树数量通常 ≤ 个位数)。 */ export interface WorktreeNode { @@ -92,12 +93,13 @@ export class WorktreeTreeProvider implements vscode.TreeDataProvider 高亮),其次当前打开高亮,对齐 branches-tree 的 ThemeIcon 高亮范式。 + // 锁定优先用 lock 图标(替代原 🔒 emoji);main 用 root-folder-opened。 + const icon = node.locked ? 'lock' : node.isMain ? 'root-folder-opened' : node.detached ? 'git-commit' : 'git-branch'; + // prunable 警示色优先(红 > 蓝),当前打开高亮用 charts.blue(与 branches-tree 活动色全局一致)。 const color = node.prunable ? new vscode.ThemeColor('gitDecoration.deletedResourceForeground') : isCurrent - ? new vscode.ThemeColor('gitDecoration.modifiedResourceForeground') + ? new vscode.ThemeColor('charts.blue') : undefined; item.iconPath = new vscode.ThemeIcon(icon, color); return item; @@ -106,33 +108,36 @@ export class WorktreeTreeProvider implements vscode.TreeDataProvider = [ + ['Path', node.path], + ['HEAD', `${node.ref}${node.detached ? ' (detached)' : ''}`], + ]; + if (node.isMain) { + rows.push(['Type', 'Main worktree']); + } + if (isCurrent) { + rows.push(['State', 'Current']); + } if (node.locked) { - lines.push('已锁定(防止自动清理)'); + rows.push(['Lock', 'Locked (excluded from prune)']); } if (node.prunable) { - lines.push('⚠ 可清理(目录已失效)'); + rows.push(['Status', 'Prunable (directory is stale)']); } - return lines.join('\n'); + return mdTooltip(rows, { title: `${node.name}${isCurrent ? ' (current)' : ''}` }); } /** 该节点是否当前打开的 worktree(供命令层删除/移动守护复用)。 */ diff --git a/src/adapter/webview/commit-webview.ts b/src/adapter/webview/commit-webview.ts index 6d95676..2f81809 100644 --- a/src/adapter/webview/commit-webview.ts +++ b/src/adapter/webview/commit-webview.ts @@ -7,6 +7,7 @@ import type { ChangelistRegistry } from '../changelist-registry'; import type { ChangeItem, GitRepositoryService } from '../git-repository-service'; import type { CommitFileItem, CommitViewState, HostToWebviewMessage, WebviewToHostMessage } from '../../shared/protocol'; import type { CommitService } from '../commit/commit-service'; +import { getBaseStyles } from './shared-styles'; /** * Commit 提交窗口(WebviewView,自绘提交面板)。 @@ -112,52 +113,63 @@ export class CommitWebviewProvider implements vscode.WebviewViewProvider { ].join('; '); return ` - + -
活动 Changelist:
+
Active Changelist:
+
- -
+ +
- - - +
+ Advanced Options + + + +
- - + +
-
+
破出。 + // 不可用 escapeHtml——其产出 " 在 diff --git a/src/adapter/webview/shared-styles.ts b/src/adapter/webview/shared-styles.ts new file mode 100644 index 0000000..aec77dc --- /dev/null +++ b/src/adapter/webview/shared-styles.ts @@ -0,0 +1,85 @@ +/** + * Webview 共享设计 Token 与基础组件类(单一事实源)。 + * + * 4 个自绘 Webview(Commit / Log / Merge / Rebase)共享同一套: + * - 设计 Token(spacing/radius,对齐 VS Code 语义); + * - 基础组件类(`.hg-btn` / `.hg-btn--secondary` / `.hg-btn--sm` / `.hg-input` / `.hg-row`) + * 统一交互态(hover / active / focus-visible / disabled),消除各 Webview 各自硬编码导致的 + * 「按钮无 hover」「`:last-child` 脆弱选择器」「无 focus ring」等熵增。 + * + * 设计原则:纯字符串、零 vscode 依赖(可单测);每个 Webview 在 `