diff --git a/README.md b/README.md index 1afb69b..fb85b6f 100644 --- a/README.md +++ b/README.md @@ -290,7 +290,7 @@ open tests/test_simyo_esim.html - `📝 docs(readme): 更新安装说明` ### 当前 Git 钩子 -- `pre-commit`:提交前格式化 staged 文件,并执行 JS/JSON 语法检查与智能引号拦截 +- `pre-commit`:提交前先同步远程更新(有 upstream 时自动 `git pull --rebase --autostash`),再格式化 staged 文件并执行 JS/JSON 语法检查与智能引号拦截 - `pre-push`:默认快速模式(`pre-commit` + 相关测试),无差异文件时回退全量测试 - `prepare-commit-msg`:提交信息为空时自动填充模板 - `commit-msg`:校验提交信息格式(emoji + type + 中文描述 + 映射关系) diff --git a/README_EN.md b/README_EN.md index 52899cd..46784c2 100644 --- a/README_EN.md +++ b/README_EN.md @@ -278,7 +278,7 @@ Examples: - `📝 docs(readme): 更新安装说明` ### Current Git Hooks -- `pre-commit`: formats staged files and runs JS/JSON syntax + smart quotes checks +- `pre-commit`: syncs remote updates first (auto `git pull --rebase --autostash` when upstream exists), then formats staged files and runs JS/JSON syntax + smart quotes checks - `pre-push`: defaults to fast mode (`pre-commit` + related tests), falls back to full tests when no diff files are detected - `prepare-commit-msg`: auto-fills a template when commit message is empty - `commit-msg`: validates message format (emoji + type + Chinese summary + mapping) diff --git a/scripts/pre-commit-check.js b/scripts/pre-commit-check.js index 78d6aa3..e15ef7c 100644 --- a/scripts/pre-commit-check.js +++ b/scripts/pre-commit-check.js @@ -71,6 +71,39 @@ function getTouchedProtectedFiles() { return output.split('\0').filter(Boolean); } +function getUpstreamRef() { + try { + return git([ + 'rev-parse', + '--abbrev-ref', + '--symbolic-full-name', + '@{u}' + ]).trim(); + } catch (_) { + return ''; + } +} + +function syncWithRemote() { + const upstreamRef = getUpstreamRef(); + if (!upstreamRef) { + console.log('ℹ️ 当前分支未配置 upstream,跳过远程同步'); + return; + } + + console.log(`🔄 提交前同步远程更新(${upstreamRef})...`); + try { + git(['pull', '--rebase', '--autostash', '--quiet'], { + stdio: 'inherit' + }); + console.log('✅ 远程更新同步完成'); + } catch (_) { + console.error('\n❌ pre-commit 远程同步失败:'); + console.error(' - 请先手动执行 `git pull --rebase` 解决冲突后再提交。'); + process.exit(1); + } +} + function shouldFormat(relPath) { const ext = path.extname(relPath).toLowerCase(); if (TEXT_EXTENSIONS.has(ext)) { @@ -191,6 +224,8 @@ function main() { process.exit(1); } + syncWithRemote(); + const stagedFiles = getStagedFiles(); if (stagedFiles.length === 0) { process.exit(0); diff --git a/tests/giffgaff/oauth.test.js b/tests/giffgaff/oauth.test.js index 4a26c92..0369573 100644 --- a/tests/giffgaff/oauth.test.js +++ b/tests/giffgaff/oauth.test.js @@ -8,7 +8,7 @@ describe('Giffgaff OAuthManager', () => { describe('generateCodeVerifier()', () => { it('应该生成有效的 code verifier', () => { const verifier = OAuthManager.generateCodeVerifier(); - + expect(verifier).toBeDefined(); expect(typeof verifier).toBe('string'); expect(verifier.length).toBeGreaterThanOrEqual(43); @@ -20,7 +20,7 @@ describe('Giffgaff OAuthManager', () => { it('每次生成的 verifier 应该不同', () => { const verifier1 = OAuthManager.generateCodeVerifier(); const verifier2 = OAuthManager.generateCodeVerifier(); - + expect(verifier1).not.toBe(verifier2); }); }); @@ -29,7 +29,7 @@ describe('Giffgaff OAuthManager', () => { it('应该从 verifier 生成有效的 challenge', async () => { const verifier = 'test-verifier-string'; const challenge = await OAuthManager.generateCodeChallenge(verifier); - + expect(challenge).toBeDefined(); expect(typeof challenge).toBe('string'); // 检查是否为 base64url 编码 @@ -40,7 +40,7 @@ describe('Giffgaff OAuthManager', () => { describe('generateState()', () => { it('应该生成有效的 state 参数', () => { const state = OAuthManager.generateState(); - + expect(state).toBeDefined(); expect(typeof state).toBe('string'); expect(state.length).toBeGreaterThan(0); @@ -51,7 +51,7 @@ describe('Giffgaff OAuthManager', () => { it('每次生成的 state 应该不同', () => { const state1 = OAuthManager.generateState(); const state2 = OAuthManager.generateState(); - + expect(state1).not.toBe(state2); }); }); @@ -60,7 +60,7 @@ describe('Giffgaff OAuthManager', () => { it('应该构建正确的授权 URL', async () => { const verifier = 'test-verifier'; const url = await OAuthManager.buildAuthorizationUrl(verifier); - + expect(url).toContain('https://id.giffgaff.com/oauth/authorize'); expect(url).toContain('response_type=code'); expect(url).toContain('client_id='); @@ -76,36 +76,42 @@ describe('Giffgaff OAuthManager', () => { it('应该从标准 URL 提取授权码', () => { const callbackUrl = 'https://example.com/callback?code=test-code&state=test-state'; const code = OAuthManager.extractCodeFromCallback(callbackUrl); - + expect(code).toBe('test-code'); }); it('应该从 giffgaff:// 协议 URL 提取授权码', () => { const callbackUrl = 'giffgaff://auth/callback/?code=test-code&state=test-state'; const code = OAuthManager.extractCodeFromCallback(callbackUrl); - + expect(code).toBe('test-code'); }); it('应该处理编码的授权码', () => { const callbackUrl = 'https://example.com/callback?code=test%2Bcode%3D&state=test'; const code = OAuthManager.extractCodeFromCallback(callbackUrl); - + expect(code).toBe('test+code='); }); it('应该在没有授权码时返回 null', () => { const callbackUrl = 'https://example.com/callback?state=test-state'; const code = OAuthManager.extractCodeFromCallback(callbackUrl); - + expect(code).toBeNull(); }); it('应该处理无效的 URL', () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); const callbackUrl = 'not-a-valid-url'; - const code = OAuthManager.extractCodeFromCallback(callbackUrl); - - expect(code).toBeNull(); + try { + const code = OAuthManager.extractCodeFromCallback(callbackUrl); + expect(code).toBeNull(); + } finally { + consoleErrorSpy.mockRestore(); + } }); }); @@ -120,14 +126,14 @@ describe('Giffgaff OAuthManager', () => { token_type: 'Bearer', expires_in: 3600 }; - + global.fetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse }); - + const result = await OAuthManager.exchangeToken('test-code', 'test-verifier'); - + expect(result).toEqual(mockResponse); expect(global.fetch).toHaveBeenCalledWith( '/.netlify/functions/giffgaff-token-exchange', @@ -145,10 +151,10 @@ describe('Giffgaff OAuthManager', () => { status: 400, text: async () => 'Invalid grant' }); - + await expect( OAuthManager.exchangeToken('invalid-code', 'test-verifier') ).rejects.toThrow('Token exchange failed: 400 - Invalid grant'); }); }); -}); \ No newline at end of file +});