From 9279050ca98fa0c8bcc182c23f156315c164c9bd Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 12 May 2026 01:01:19 +0000 Subject: [PATCH] feat: refactor commitAndPush to use GIT_ASKPASS for authentication and add tests --- app/git.js | 25 ++++++++++++++++++++----- app/git.test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 app/git.test.js diff --git a/app/git.js b/app/git.js index c9bc837..4ff96a8 100644 --- a/app/git.js +++ b/app/git.js @@ -1,7 +1,7 @@ import { spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; -import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; +import { GITEA_SERVER_URL, GITEA_REPOSITORY, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; function git(args, cwd) { const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); @@ -11,21 +11,36 @@ function git(args, cwd) { } export async function commitAndPush(workspace) { - const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '') - .replace('https://', `https://${GITEA_TOKEN}@`) - .replace('http://', `http://${GITEA_TOKEN}@`) + `/${GITEA_REPOSITORY}.git`; + const remoteUrl = GITEA_SERVER_URL.replace(/\/$/, '') + `/${GITEA_REPOSITORY}.git`; const repoDir = path.join(workspace, 'repo'); try { if (!fs.existsSync(repoDir)) { - git(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace); + // Use GIT_ASKPASS to provide token for authentication + gitWithToken(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace); } git(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); git(['config', 'user.name', 'AI Review Bot'], repoDir); git(['fetch', 'origin', PR_HEAD_BRANCH], repoDir); git(['checkout', PR_HEAD_BRANCH], repoDir); +// Helper to run git with GITEA_TOKEN via GIT_ASKPASS +import { GITEA_TOKEN } from './config.js'; +function gitWithToken(args, cwd) { + const askPassScript = `#!/bin/sh\necho \"${GITEA_TOKEN}\"`; + const askPassPath = path.join(cwd, 'git-askpass.sh'); + fs.writeFileSync(askPassPath, askPassScript, { mode: 0o700 }); + const result = spawnSync('git', args, { + cwd, + encoding: 'utf8', + env: { ...process.env, GIT_ASKPASS: askPassPath }, + }); + fs.unlinkSync(askPassPath); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim()); + return (result.stdout || '').trim(); +} // 將 findings.json 從 workspace 複製到 clone 的 repo const srcFindings = path.join(workspace, FINDINGS_PATH); diff --git a/app/git.test.js b/app/git.test.js new file mode 100644 index 0000000..5e751d8 --- /dev/null +++ b/app/git.test.js @@ -0,0 +1,30 @@ +import { commitAndPush } from './git.js'; +import fs from 'fs'; +import path from 'path'; + +// Mock dependencies and environment +jest.mock('fs'); +jest.mock('child_process', () => ({ + spawnSync: jest.fn(() => ({ status: 0, stdout: '', stderr: '' })) +})); + +describe('commitAndPush', () => { + const workspace = '/tmp/workspace'; + const repoDir = path.join(workspace, 'repo'); + + beforeEach(() => { + jest.clearAllMocks(); + fs.existsSync.mockReturnValue(false); + fs.writeFileSync.mockImplementation(() => {}); + fs.unlinkSync.mockImplementation(() => {}); + }); + + it('should clone repo and configure git', async () => { + await expect(commitAndPush(workspace)).resolves.not.toThrow(); + }); + + it('should not clone if repo exists', async () => { + fs.existsSync.mockReturnValue(true); + await expect(commitAndPush(workspace)).resolves.not.toThrow(); + }); +});