🐛 fix(vscode): 提交代码改调 generate-git-commit-message 写说明

This commit is contained in:
cruldra committed 2026-10-15 15:34:18 +11:00
1 parent 5e9bf44710
commit d10c29a260
7 files changed
+130 -3

No files matched your search

+5 -1
View File
@@ -14,6 +14,10 @@ import (
type commitRunMsg struct{ err error } type commitRunMsg struct{ err error }
const commitSkill = "/generate-git-commit-message"
const commitPrompt = commitSkill + "\n先加载这条技能,按它的格式根据当前未提交改动生成提交说明,再 git add 并 git commit。\n用 git status 和 git diff(含暂存区)看清改了什么。\n不要把本指令、按钮名或任何操作口令原样当作 commit message。\n没有改动就停下来,不要空提交。不要 push,不要改无关文件。"
type mergePreviewResultMsg struct { type mergePreviewResultMsg struct {
issueNumber int issueNumber int
result git.MergePreviewResult result git.MergePreviewResult
@@ -30,7 +34,7 @@ func commitRunCmd() tea.Cmd {
return commitRunMsg{err: err} return commitRunMsg{err: err}
} }
_, err = cc.SpawnClaude(ctx, cc.Opts{ _, err = cc.SpawnClaude(ctx, cc.Opts{
Prompt: "提交下代码", Prompt: commitPrompt,
Cwd: root, Cwd: root,
TimeoutMs: 30 * 60 * 1000, TimeoutMs: 30 * 60 * 1000,
}) })
+15
View File
@@ -184,3 +184,18 @@ func TestCommitRun_InFlightGuard(t *testing.T) {
t.Fatal("commitRunMsg must clear commitRunning") t.Fatal("commitRunMsg must clear commitRunning")
} }
} }
func TestCommitPrompt_NotLiteralCommand(t *testing.T) {
if !strings.HasPrefix(commitPrompt, commitSkill+"\n") {
t.Fatalf("commitPrompt must start with %q, got %q", commitSkill, commitPrompt)
}
if commitPrompt == "提交下代码" {
t.Fatal("commitPrompt must not be the old literal command")
}
if !strings.Contains(commitPrompt, "不要把本指令") {
t.Errorf("commitPrompt should forbid using the instruction as the commit message, got %q", commitPrompt)
}
if !strings.Contains(commitPrompt, "git commit") {
t.Errorf("commitPrompt should ask for git commit, got %q", commitPrompt)
}
}
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'
import { COMMIT_PROMPT, COMMIT_SKILL } from './commitPrompt'
describe('commit prompt', () => {
it('开头就是生成提交说明的技能命令', () => {
expect(COMMIT_SKILL).toBe('/generate-git-commit-message')
expect(COMMIT_PROMPT.startsWith(`${COMMIT_SKILL}\n`)).toBe(true)
})
it('不是「提交下代码」这种会被原样拿去当提交说明的短口令', () => {
expect(COMMIT_PROMPT).not.toBe('提交下代码')
expect(COMMIT_PROMPT).not.toContain('提交下代码')
})
it('要求按改动写提交说明,并禁止把指令本身当成 commit message', () => {
expect(COMMIT_PROMPT).toContain('git commit')
expect(COMMIT_PROMPT).toContain('不要把本指令')
expect(COMMIT_PROMPT).toContain('不要空提交')
})
})
+13
View File
@@ -0,0 +1,13 @@
/**
* 工具栏「提交代码」发给 Claude 的一次性指令:先加载生成提交说明的技能,再按改动 commit。
* `--bare` 下技能仍靠 `/skill-name` 解析,所以第一行必须是技能命令。
*/
export const COMMIT_SKILL = '/generate-git-commit-message'
export const COMMIT_PROMPT = [
COMMIT_SKILL,
'先加载这条技能,按它的格式根据当前未提交改动生成提交说明,再 git add 并 git commit。',
'用 git status 和 git diff(含暂存区)看清改了什么。',
'不要把本指令、按钮名或任何操作口令原样当作 commit message。',
'没有改动就停下来,不要空提交。不要 push,不要改无关文件。',
].join('\n')
+74
View File
@@ -0,0 +1,74 @@
import type { KanbanWebviewPanel } from '../KanbanPanel'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { COMMIT_PROMPT } from '../../cc/commitPrompt'
import { handleCommitRun } from './toolbar'
const mocks = vi.hoisted(() => ({
spawnClaude: vi.fn(),
getEffectiveCommitProfilePath: vi.fn(),
resolveCommitProfileForRun: vi.fn(),
readGitIdentity: vi.fn(),
toast: vi.fn(),
dismiss: vi.fn(),
}))
vi.mock('vscode', () => ({
workspace: { workspaceFolders: [{ uri: { fsPath: '/repo' } }] },
}))
vi.mock('../../cc/commitProfile', () => ({
resolveCommitProfileForRun: (...args: unknown[]) => mocks.resolveCommitProfileForRun(...args),
}))
vi.mock('../../cc/profiles', () => ({ resolveProfilePath: (p: string) => p }))
vi.mock('../../cc/spawnClaude', () => ({ spawnClaude: (...args: unknown[]) => mocks.spawnClaude(...args) }))
vi.mock('../../git/identity', () => ({
missingGitIdentityKeys: (identity: { name: string, email: string }) => {
const missing: string[] = []
if (!identity.name.trim())
missing.push('user.name')
if (!identity.email.trim())
missing.push('user.email')
return missing
},
readGitIdentity: (...args: unknown[]) => mocks.readGitIdentity(...args),
}))
vi.mock('../../settings/store', () => ({
getEffectiveCommitProfilePath: (...args: unknown[]) => mocks.getEffectiveCommitProfilePath(...args),
}))
vi.mock('../../webview/html', () => ({ makeNonce: () => 'n1' }))
vi.mock('./handoffFlow', () => ({
toast: (...args: unknown[]) => mocks.toast(...args),
dismiss: (...args: unknown[]) => mocks.dismiss(...args),
}))
vi.mock('../../logging/logger', () => ({ logger: { add: vi.fn() } }))
vi.mock('../../files/envLock', () => ({ findEnvFiles: vi.fn(), lockEnvFiles: vi.fn(), unlockEnvFiles: vi.fn() }))
vi.mock('../../git/gitExtension', () => ({ loadGitApi: vi.fn() }))
function panel(): KanbanWebviewPanel {
return {
commitRunning: false,
context: {},
postMessage: vi.fn(),
} as unknown as KanbanWebviewPanel
}
beforeEach(() => {
vi.clearAllMocks()
mocks.getEffectiveCommitProfilePath.mockResolvedValue('/p/deepseek-v4-pro.json')
mocks.resolveCommitProfileForRun.mockReturnValue({ ok: true, profilePath: '/p/deepseek-v4-pro.json' })
mocks.readGitIdentity.mockResolvedValue({ name: 'me', email: 'me@example.com' })
mocks.spawnClaude.mockResolvedValue({ resultText: 'committed', sessionId: 's', rawJson: '{}' })
mocks.toast.mockReturnValue('toast-id')
})
describe('handleCommitRun', () => {
it('把写提交说明的指令交给 claude,不用「提交下代码」', async () => {
const p = panel()
await handleCommitRun(p)
expect(mocks.spawnClaude).toHaveBeenCalledOnce()
const args = mocks.spawnClaude.mock.calls[0][0] as { prompt: string, cwd: string, profilePath: string }
expect(args.prompt).toBe(COMMIT_PROMPT)
expect(args.prompt).not.toBe('提交下代码')
expect(args.cwd).toBe('/repo')
expect(args.profilePath).toBe('/p/deepseek-v4-pro.json')
})
})
+2 -1
View File
@@ -4,6 +4,7 @@ import { accessSync, constants } from 'node:fs'
import * as path from 'node:path' import * as path from 'node:path'
import { workspace } from 'vscode' import { workspace } from 'vscode'
import { resolveCommitProfileForRun } from '../../cc/commitProfile' import { resolveCommitProfileForRun } from '../../cc/commitProfile'
import { COMMIT_PROMPT } from '../../cc/commitPrompt'
import { resolveProfilePath } from '../../cc/profiles' import { resolveProfilePath } from '../../cc/profiles'
import { spawnClaude } from '../../cc/spawnClaude' import { spawnClaude } from '../../cc/spawnClaude'
import { findEnvFiles, lockEnvFiles, unlockEnvFiles } from '../../files/envLock' import { findEnvFiles, lockEnvFiles, unlockEnvFiles } from '../../files/envLock'
@@ -84,7 +85,7 @@ export async function handleCommitRun(panel: KanbanWebviewPanel): Promise<void>
// claude -p 提交流程可能跑得比较久(要 git add / 编 commit message / git // claude -p 提交流程可能跑得比较久(要 git add / 编 commit message / git
// commit),所以给一个比较宽松的超时(30 min)。 // commit),所以给一个比较宽松的超时(30 min)。
const { resultText } = await spawnClaude({ const { resultText } = await spawnClaude({
prompt: '提交下代码', prompt: COMMIT_PROMPT,
cwd: workspaceRoot, cwd: workspaceRoot,
profilePath: resolution.profilePath, profilePath: resolution.profilePath,
timeoutMs: 30 * 60 * 1000, timeoutMs: 30 * 60 * 1000,
+1 -1
View File
@@ -259,7 +259,7 @@ export interface UseIssuesResult {
*/ */
commitRunning: boolean commitRunning: boolean
/** /**
* Trigger a background `claude -p "提交下代码"` run in the workspace * Trigger a background Claude commit run in the workspace
* root, gated by `commitRunning`. The extension responds with * root, gated by `commitRunning`. The extension responds with
* `commit/state` messages and a `toast/show` on completion. * `commit/state` messages and a `toast/show` on completion.
*/ */