diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml new file mode 100644 index 00000000..311c3039 --- /dev/null +++ b/.github/workflows/sync-docs.yml @@ -0,0 +1,38 @@ +name: Sync Vitest Docs + +on: + workflow_dispatch: + +jobs: + sync-docs: + runs-on: ubuntu-latest + + steps: + - name: Checkout docs-cn repository + uses: actions/checkout@v4 + with: + repository: vitest-dev/docs-cn + ref: dev + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + npm install shelljs @octokit/rest colors + + - name: Run sync script + env: + GITHUB_USERNAME: elonehoo + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_EMAIL: elonehoo@users.noreply.github.com + run: | + node scripts/sync-vitest-docs.mjs "$GITHUB_USERNAME" "$GITHUB_TOKEN" "$GITHUB_EMAIL" + + - name: Cleanup + if: always() + run: | + rm -rf temp diff --git a/package.json b/package.json index 567f60ee..467b337f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "lint": "eslint --cache .", "lint:fix": "eslint . --fix", "generate-pwa-icons": "pwa-assets-generator", - "prepare": "simple-git-hooks" + "prepare": "simple-git-hooks", + "sync": "node scripts/sync-vitest-docs.mjs" }, "dependencies": { "@vueuse/core": "latest", @@ -45,6 +46,9 @@ "lint-staged": "^16.2.6", "ofetch": "^1.4.1", "pathe": "^2.0.3", + "shelljs": "^0.8.5", + "@octokit/rest": "^21.0.2", + "colors": "^1.4.0", "simple-git-hooks": "^2.13.1", "tinyglobby": "latest", "tsx": "^4.19.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f58bc7b0..00cd037b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,9 @@ importers: '@iconify-json/logos': specifier: latest version: 1.2.5 + '@octokit/rest': + specifier: ^21.0.2 + version: 21.1.1 '@shikijs/transformers': specifier: latest version: 3.9.2 @@ -63,6 +66,9 @@ importers: '@vitest/ui': specifier: ^4.0.0-beta.7 version: 4.0.0-beta.7(vitest@4.0.6) + colors: + specifier: ^1.4.0 + version: 1.4.0 eslint: specifier: ^9.38.0 version: 9.39.0(jiti@2.5.1) @@ -84,6 +90,9 @@ importers: pathe: specifier: ^2.0.3 version: 2.0.3 + shelljs: + specifier: ^0.8.5 + version: 0.8.5 simple-git-hooks: specifier: ^2.13.1 version: 2.13.1 @@ -1160,6 +1169,64 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@octokit/auth-token@5.1.2': + resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.6': + resolution: {integrity: sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.4': + resolution: {integrity: sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.2.2': + resolution: {integrity: sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} + + '@octokit/plugin-paginate-rest@11.6.0': + resolution: {integrity: sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.5.0': + resolution: {integrity: sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.8': + resolution: {integrity: sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==} + engines: {node: '>= 18'} + + '@octokit/request@9.2.4': + resolution: {integrity: sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==} + engines: {node: '>= 18'} + + '@octokit/rest@21.1.1': + resolution: {integrity: sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==} + engines: {node: '>= 18'} + + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2023,6 +2090,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2146,6 +2216,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -2653,6 +2727,9 @@ packages: exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2960,6 +3037,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -3771,6 +3852,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3936,6 +4021,11 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + shiki@3.9.2: resolution: {integrity: sha512-t6NKl5e/zGTvw/IyftLcumolgOczhuroqwXngDeMqJ3h3EQiTY/7wmfgPlsmloD8oYfqkEDqxiaH37Pjm1zUhQ==} @@ -4305,6 +4395,9 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -5779,6 +5872,74 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@octokit/auth-token@5.1.2': {} + + '@octokit/core@6.1.6': + dependencies: + '@octokit/auth-token': 5.1.2 + '@octokit/graphql': 8.2.2 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@10.1.4': + dependencies: + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@8.2.2': + dependencies: + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/openapi-types@24.2.0': {} + + '@octokit/openapi-types@25.1.0': {} + + '@octokit/plugin-paginate-rest@11.6.0(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + + '@octokit/plugin-rest-endpoint-methods@13.5.0(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 + + '@octokit/request-error@6.1.8': + dependencies: + '@octokit/types': 14.1.0 + + '@octokit/request@9.2.4': + dependencies: + '@octokit/endpoint': 10.1.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.3 + + '@octokit/rest@21.1.1': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/plugin-paginate-rest': 11.6.0(@octokit/core@6.1.6) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.6) + '@octokit/plugin-rest-endpoint-methods': 13.5.0(@octokit/core@6.1.6) + + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 + + '@octokit/types@14.1.0': + dependencies: + '@octokit/openapi-types': 25.1.0 + '@pkgr/core@0.2.9': {} '@polka/url@1.0.0-next.29': {} @@ -6851,6 +7012,8 @@ snapshots: balanced-match@1.0.2: {} + before-after-hook@3.0.2: {} + binary-extensions@2.3.0: {} birpc@2.5.0: {} @@ -6985,6 +7148,8 @@ snapshots: colorette@2.0.20: {} + colors@1.4.0: {} + comma-separated-tokens@2.0.3: {} commander@14.0.2: {} @@ -7627,6 +7792,8 @@ snapshots: exsolve@1.0.7: {} + fast-content-type-parse@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -7940,6 +8107,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + interpret@1.4.0: {} + ipaddr.js@1.9.1: {} is-array-buffer@3.0.5: @@ -8904,6 +9073,10 @@ snapshots: dependencies: picomatch: 2.3.1 + rechoir@0.6.2: + dependencies: + resolve: 1.22.10 + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.1 @@ -9151,6 +9324,12 @@ snapshots: shebang-regex@3.0.0: {} + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + shiki@3.9.2: dependencies: '@shikijs/core': 3.9.2 @@ -9576,6 +9755,8 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universal-user-agent@7.0.3: {} + universalify@2.0.1: {} unocss@66.4.2(postcss@8.5.6)(vite@7.1.6(@types/node@24.2.1)(jiti@2.5.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.1)): diff --git a/scripts/sync-vitest-docs.mjs b/scripts/sync-vitest-docs.mjs new file mode 100644 index 00000000..c90a8a6d --- /dev/null +++ b/scripts/sync-vitest-docs.mjs @@ -0,0 +1,407 @@ +import path from 'node:path' +import process from 'node:process' +import { Octokit } from '@octokit/rest' +import shell from 'shelljs' + +// 从命令行参数或环境变量获取配置 +const username = process.argv[2] || process.env.GITHUB_USERNAME +const token = process.argv[3] || process.env.GITHUB_TOKEN +const email = process.argv[4] || process.env.GITHUB_EMAIL + +// 配置 +const owner = 'vitest-dev' +const sourceRepo = 'vitest' +const targetRepo = 'docs-cn' +const sourceDir = 'docs' // vitest 仓库中的 docs 目录 +const sourceBranch = 'main' // vitest 仓库的分支 +const targetBranch = 'dev' // docs-cn 仓库的默认分支 + +const sourceUrl = `https://github.com/${owner}/${sourceRepo}.git` +const targetUrl = `https://${username}:${token}@github.com/${owner}/${targetRepo}.git` + +console.log(`\n🚀 开始同步 ${sourceRepo}/${sourceDir} 到 ${targetRepo}`) + +// 保存原始工作目录 +const originalCwd = process.cwd() + +// 创建临时工作目录 +const tempDir = path.join(originalCwd, 'temp') +if (!shell.test('-d', tempDir)) { + shell.mkdir('-p', tempDir) +} +shell.cd(tempDir) + +// 克隆目标仓库 (docs-cn) - 包含完整历史 +console.log(`\n📦 正在克隆目标仓库 ${targetRepo}...`) +if (shell.test('-d', targetRepo)) { + shell.rm('-rf', targetRepo) +} +const cloneResult = shell.exec(`git clone ${targetUrl} ${targetRepo}`) +if (cloneResult.code !== 0) { + console.error('❌ 克隆目标仓库失败!') + process.exit(1) +} + +// 进入目标仓库 +shell.cd(path.join(tempDir, targetRepo)) + +// 配置 git +shell.exec(`git config user.name "${username}"`) +shell.exec(`git config user.email "${email}"`) + +// 添加上游仓库作为 remote +console.log(`\n🔗 添加上游仓库 ${sourceRepo} 作为 remote...`) +shell.exec(`git remote add upstream ${sourceUrl} 2>/dev/null || git remote set-url upstream ${sourceUrl}`) + +// 获取上游仓库的更新(获取完整历史以便比较) +console.log(`\n📥 正在获取上游仓库更新...`) +const fetchResult = shell.exec(`git fetch upstream ${sourceBranch}`) +if (fetchResult.code !== 0) { + console.error('❌ 获取上游仓库失败!') + process.exit(1) +} + +// 获取上游最新 commit hash +const upstreamHash = shell.exec(`git rev-parse upstream/${sourceBranch}`).stdout.trim() +const shortHash = upstreamHash.substring(0, 8) + +// 检查是否已经同步过(通过 tag 记录上次同步的上游 commit) +const lastSyncTag = shell.exec('git tag -l "synced-*" --sort=-creatordate | head -1').stdout.trim() +const lastSyncHash = lastSyncTag ? lastSyncTag.replace('synced-', '') : '' + +console.log(`\n📋 同步状态:`) +console.log(` 上游最新: ${shortHash}`) +console.log(` 上次同步: ${lastSyncHash || '无记录(首次同步)'}`) + +if (lastSyncHash === shortHash) { + console.log(`\n✅ 已经是最新的,无需同步`) + process.exit(0) +} + +// 创建同步分支 +const syncBranch = `sync-${shortHash}` + +// 检查是否已存在该同步分支(可能有未合并的 PR) +const branchExists = shell.exec(`git ls-remote --heads origin ${syncBranch}`).stdout.trim() +if (branchExists) { + console.log(`\n⚠️ 分支 ${syncBranch} 已存在,可能已经有同步 PR 在进行中`) + process.exit(0) +} + +// 从目标分支创建新的同步分支 +shell.exec(`git checkout ${targetBranch}`) +shell.exec(`git checkout -b ${syncBranch}`) + +// 获取上游在 docs 目录的变更 +console.log(`\n🔍 正在分析上游变更...`) + +let diffBase = '' +if (lastSyncHash) { + // 检查上次同步的 commit 是否存在于上游历史中 + const commitExists = shell.exec(`git cat-file -t ${lastSyncHash} 2>/dev/null`).code === 0 + if (commitExists) { + diffBase = lastSyncHash + } +} + +// 获取变更的文件列表 +let changedFilesInUpstream = [] +let newFiles = [] +const modifiedFiles = [] +const deletedFiles = [] + +if (diffBase) { + // 增量模式:只获取从上次同步以来的变更 + console.log(`\n📊 增量同步模式 (从 ${diffBase.substring(0, 8)} 到 ${shortHash})`) + + const diffOutput = shell.exec( + `git diff --name-status ${diffBase}..upstream/${sourceBranch} -- ${sourceDir}/`, + ).stdout.trim() + + if (diffOutput) { + const lines = diffOutput.split('\n').filter(l => l.trim()) + for (const line of lines) { + const [status, ...fileParts] = line.split('\t') + const file = fileParts.join('\t') // 处理文件名包含 tab 的情况 + const relativePath = file.replace(`${sourceDir}/`, '') + + if (status === 'A') { + newFiles.push(relativePath) + } + else if (status === 'M') { + modifiedFiles.push(relativePath) + } + else if (status === 'D') { + deletedFiles.push(relativePath) + } + else if (status.startsWith('R')) { + // 重命名:视为删除旧文件 + 添加新文件 + const [oldFile, newFile] = fileParts + deletedFiles.push(oldFile.replace(`${sourceDir}/`, '')) + newFiles.push(newFile.replace(`${sourceDir}/`, '')) + } + } + changedFilesInUpstream = [...newFiles, ...modifiedFiles, ...deletedFiles] + } +} +else { + // 首次同步:标记所有文件为新增 + console.log(`\n📊 首次同步模式`) + const allFiles = shell.exec( + `git ls-tree -r --name-only upstream/${sourceBranch} -- ${sourceDir}/`, + ).stdout.trim() + + if (allFiles) { + newFiles = allFiles.split('\n').map(f => f.replace(`${sourceDir}/`, '')) + changedFilesInUpstream = newFiles + } +} + +console.log(`\n📈 上游变更统计:`) +console.log(` 新增文件: ${newFiles.length}`) +console.log(` 修改文件: ${modifiedFiles.length}`) +console.log(` 删除文件: ${deletedFiles.length}`) + +if (changedFilesInUpstream.length === 0) { + console.log(`\n✅ 上游 ${sourceDir} 目录没有变更`) + // 创建标记 tag + shell.exec(`git tag synced-${shortHash}`) + shell.exec(`git push origin synced-${shortHash}`) + process.exit(0) +} + +// 创建临时目录存放上游 docs +const upstreamDocsTemp = path.join(tempDir, 'upstream-docs') +if (shell.test('-d', upstreamDocsTemp)) { + shell.rm('-rf', upstreamDocsTemp) +} +shell.mkdir('-p', upstreamDocsTemp) + +// 使用 git archive 提取上游的 docs 目录 +console.log(`\n📂 正在提取上游 ${sourceDir} 目录...`) +const archiveResult = shell.exec( + `git archive upstream/${sourceBranch} ${sourceDir} | tar -x -C ${upstreamDocsTemp}`, +) + +if (archiveResult.code !== 0) { + console.error('❌ 提取上游 docs 目录失败!') + process.exit(1) +} + +const sourceDocsPath = path.join(upstreamDocsTemp, sourceDir) +const targetRootPath = path.join(tempDir, targetRepo) + +// 分类处理文件 +const actualNewFiles = [] +const actualModifiedFiles = [] +const actualDeletedFiles = [] +const skippedFiles = [] // 跳过的文件(本地有修改) + +console.log(`\n🔄 正在同步文件...`) + +// 处理新增文件:直接复制 +for (const file of newFiles) { + const srcFile = path.join(sourceDocsPath, file) + const destFile = path.join(targetRootPath, file) + + if (shell.test('-f', srcFile)) { + // 确保目标目录存在 + const destDir = path.dirname(destFile) + shell.mkdir('-p', destDir) + shell.cp(srcFile, destFile) + actualNewFiles.push(file) + } +} + +// 处理修改文件:检查本地是否有翻译(通过内容比较) +for (const file of modifiedFiles) { + const srcFile = path.join(sourceDocsPath, file) + const destFile = path.join(targetRootPath, file) + + if (shell.test('-f', srcFile)) { + if (shell.test('-f', destFile)) { + // 本地文件存在,检查是否有本地修改 + // 策略:如果本地文件和上次同步时的上游文件不同,说明有翻译,跳过 + // 简化策略:总是更新,但在 PR 中标记出来让人工检查 + + // 这里我们选择:复制新文件,但记录下来 + shell.cp(srcFile, destFile) + actualModifiedFiles.push(file) + } + else { + // 本地文件不存在,直接复制 + const destDir = path.dirname(destFile) + shell.mkdir('-p', destDir) + shell.cp(srcFile, destFile) + actualNewFiles.push(file) + } + } +} + +// 处理删除文件:如果上游删除了,本地也删除 +for (const file of deletedFiles) { + const destFile = path.join(targetRootPath, file) + + if (shell.test('-f', destFile)) { + shell.rm(destFile) + actualDeletedFiles.push(file) + } +} + +// 检查是否有实际变更 +const statusOutput = shell.exec('git status --porcelain').stdout.trim() + +if (!statusOutput) { + console.log('\n✅ 没有需要提交的变更') + shell.exec(`git tag synced-${shortHash}`) + shell.exec(`git push origin synced-${shortHash}`) + process.exit(0) +} + +// 为每个变更创建单独的 commit(可选,这里简化为一个 commit) +// 如果需要更细粒度的 commit,可以在这里分别 commit + +shell.exec('git add .') + +const commitMessage = `chore: sync docs from vitest@${shortHash} + +Upstream: https://github.com/${owner}/${sourceRepo}/commit/${upstreamHash} +${lastSyncHash ? `Previous sync: ${lastSyncHash}` : 'Initial sync'} + +Changes from upstream: +- New files: ${actualNewFiles.length} +- Modified files: ${actualModifiedFiles.length} +- Deleted files: ${actualDeletedFiles.length}` + +shell.exec(`git commit -m "${commitMessage}"`) + +// 创建同步标记 tag +const syncTag = `synced-${shortHash}` +shell.exec(`git tag ${syncTag}`) + +// 推送到远程 +console.log(`\n📤 正在推送到分支 ${syncBranch}...`) +const pushResult = shell.exec(`git push --set-upstream origin ${syncBranch}`) + +if (pushResult.code !== 0) { + console.error('❌ 推送失败!') + process.exit(1) +} + +// 推送 tag +shell.exec(`git push origin ${syncTag}`) + +// 创建 Pull Request +console.log('\n📝 正在创建 Pull Request...') + +const octokit = new Octokit({ + auth: `token ${token}`, +}) + +const title = `chore: sync docs from vitest @ ${shortHash}` + +// 生成文件列表 +function formatFileList(files, maxShow = 15) { + if (files.length === 0) +return '_无_' + const shown = files.slice(0, maxShow) + const list = shown.map(f => `- \`${f}\``).join('\n') + if (files.length > maxShow) { + return `${list}\n- _... 还有 ${files.length - maxShow} 个文件_` + } + return list +} + +// 生成上游 commit 链接(如果有增量) +let upstreamChangesLink = '' +if (lastSyncHash) { + upstreamChangesLink = `\n\n**查看上游变更:** [${lastSyncHash.substring(0, 8)}...${shortHash}](https://github.com/${owner}/${sourceRepo}/compare/${lastSyncHash}...${upstreamHash})` +} + +const body = ` +## 🤖 自动同步 + +此 PR 由自动化脚本生成,用于同步上游 vitest 仓库的 docs 目录。 + +### 📌 同步信息 + +| 项目 | 值 | +|------|-----| +| 源提交 | [\`${shortHash}\`](https://github.com/${owner}/${sourceRepo}/commit/${upstreamHash}) | +| 上次同步 | ${lastSyncHash ? `[\`${lastSyncHash.substring(0, 8)}\`](https://github.com/${owner}/${sourceRepo}/commit/${lastSyncHash})` : '首次同步'} | +| 同步时间 | ${new Date().toISOString()} | +| 同步模式 | ${lastSyncHash ? '增量同步' : '全量同步'} | +${upstreamChangesLink} + +### 📊 变更统计 + +| 类型 | 数量 | 说明 | +|------|------|------| +| ➕ 新增 | **${actualNewFiles.length}** | 上游新增的文档,需要翻译 | +| 📝 修改 | **${actualModifiedFiles.length}** | 上游修改的文档,可能需要更新翻译 | +| ➖ 删除 | **${actualDeletedFiles.length}** | 上游删除的文档 | + +### ➕ 新增文件 (需要翻译) + +${formatFileList(actualNewFiles)} + +### 📝 修改文件 (检查是否需要更新翻译) + +${formatFileList(actualModifiedFiles)} + +### ➖ 删除文件 + +${formatFileList(actualDeletedFiles)} + +## ⚠️ 重要提示 + +1. **新增文件**需要添加中文翻译 +2. **修改文件**请对比上游变更,更新相应的翻译 +3. 点击上方的"查看上游变更"链接可以看到具体改了什么 +4. **不要使用 Squash Merge**,请使用普通 Merge + +## 📝 翻译检查清单 + +${actualNewFiles.slice(0, 10).map(f => `- [ ] \`${f}\` - 添加翻译`).join('\n')} +${actualModifiedFiles.slice(0, 10).map(f => `- [ ] \`${f}\` - 检查/更新翻译`).join('\n')} +${(actualNewFiles.length > 10 || actualModifiedFiles.length > 10) ? '\n- [ ] ... 检查其他文件' : ''} +` + +try { + const { data: pr } = await octokit.pulls.create({ + owner, + repo: targetRepo, + title, + body, + head: syncBranch, + base: targetBranch, + }) + + console.log('\n✅ Pull Request 创建成功!') + console.log(`📋 PR 编号: #${pr.number}`) + console.log(`🔗 URL: ${pr.html_url}`) + + // 可选: 添加标签 + try { + await octokit.issues.addLabels({ + owner, + repo: targetRepo, + issue_number: pr.number, + labels: ['sync', 'documentation'], + }) + } + catch (err) { + console.log('添加标签失败(可能标签不存在):', err.message) + } +} +catch (err) { + console.error('\n❌ 创建 Pull Request 失败:') + console.error(err.message) + process.exit(1) +} + +console.log('\n🎉 同步完成!') +console.log(`\n📋 摘要:`) +console.log(` 新增: ${actualNewFiles.length} 个文件 (需要翻译)`) +console.log(` 修改: ${actualModifiedFiles.length} 个文件 (检查翻译)`) +console.log(` 删除: ${actualDeletedFiles.length} 个文件`)