diff --git a/README.ZH-CN.md b/README.ZH-CN.md index 94ecffe..5d91237 100644 --- a/README.ZH-CN.md +++ b/README.ZH-CN.md @@ -138,6 +138,20 @@ pr-checker -v pr-checker -h ```` +#### -m | --mode +使用 rebase 模式 或者 merge 模式 , 默认值是 rebase 模式 + +> 在 `rebase` 模式中, 你可以选择仓库或这直接对所有你所提交的 `pr` 进行 `rebase` 操作 +它将调用 `/repos/${repoName}/pulls/${prNumber}/update-branch`. + +>在 `merge` 模式中,你可以对你所拥有的仓库(`fork` 的仓库除外)进行 `merge` 操作 +它将调用 `/repos/${repoName}/pulls/${prNumber}/merge`. +一个典型的引用场景就是批量处理`dependabot`的 `pr` (加入到 `merge queue` 的功能还未完成) + +```` shell +pr-checker run -m merge | rebase +```` + ## 快照 Detect and update your Pull Requests in batches Detect and update your Pull Requests in batches diff --git a/README.md b/README.md index 439fd4a..7159cd9 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,20 @@ pr-checker -v #### -h | --help Display help message +#### -m | --mode +Use `rebase` mode or `merge` mode, the default value is `rebase` mode + +> In `rebase` mode, you can choose a repository or directly `rebase` all your submitted `pr` +It will call `/repos/${repoName}/pulls/${prNumber}/update-branch`. + +>In `merge` mode, you can `merge` on repositories you own (except `fork` repositories) +It will call `/repos/${repoName}/pulls/${prNumber}/merge`. +A typical usage scenario is batch processing `pr` of `dependabot` (the function of adding to `merge queue` has not yet been completed) + +```` shell +pr-checker run -m merge | rebase +```` + ```` shell pr-checker -h ```` diff --git a/packages/cli/core/index.ts b/packages/cli/core/index.ts index b0cd78e..840a688 100644 --- a/packages/cli/core/index.ts +++ b/packages/cli/core/index.ts @@ -26,21 +26,27 @@ async function initCli() { // get git config cli.option('-g, --get', 'get git config') - cli.command('run', 'check your pr').action(async() => { - if (!storage.token) { - log('error', 'use `pr-checker -t ` to set your token') - process.exit(1) - } + cli.command('run', 'check your pr') + // use rebase or merge + .option('-m , --mode ', 'use rebase or merge') + .action(async(options) => { + if (!storage.token) { + log('error', 'use `pr-checker -t ` to set your token') + process.exit(1) + } - if (!storage.username) { - log('info', 'You have not set a username, ' + if (!storage.username) { + log('info', 'You have not set a username, ' + 'it has been automatically set for you according to the token') - const { login } = await getUserName(storage.token) - storage.username = login - await saveStorage() - } - await handleSelect(storage as Storage) - }) + const { login } = await getUserName(storage.token) + storage.username = login + await saveStorage() + } + + const { m, mode } = options + const lastMode = m || mode || 'rebase' + await handleSelect(storage as Storage, lastMode) + }) cli.help() cli.version(version) return cli diff --git a/packages/cli/core/select/handle-select.ts b/packages/cli/core/select/handle-select.ts index e06ef28..349b89c 100644 --- a/packages/cli/core/select/handle-select.ts +++ b/packages/cli/core/select/handle-select.ts @@ -8,14 +8,14 @@ import type { IPRCheckRes, IPRInfo, IPRListItem } from '@pr-checker/utils/git-ap import type { Storage } from '../store/storage' declare type IPRSelect = Record -export async function handleSelect(store: Storage) { +export async function handleSelect(store: Storage, mode: 'merge' | 'rebase') { // select type ( all Repo ?) const isAllRepo = await promptsRun(typeOption) let updateRes = [] const spinner = ora({ text: 'Loading Repo......', color: 'blue' }).start() const githubApi = new GitApi(store.token, store.username!) - const prList = await githubApi.getPRList() + const prList = mode === 'rebase' ? (await githubApi.getSubmitPRList()) : (await githubApi.getAllRepoPRList()) spinner.stop() const repoList = Object.keys(prList) @@ -26,12 +26,12 @@ export async function handleSelect(store: Storage) { const prl = prList[selectRepo.RepoSelect as keyof typeof prList] as IPRListItem[] // check pr log('info', `Checking PR by ${selectRepo.RepoSelect}......`) - const prListByRepo = await checkPR(prl, githubApi) + const prListByRepo = await checkPR(prl, githubApi, mode) // select pr const prSelectRes = await promptsRun(createPrOption(prListByRepo as IPRCheckRes[])) // update pr log('info', `Update PR by ${selectRepo.RepoSelect}......`) - updateRes = await updatePR(prl, prSelectRes as IPRSelect, githubApi) + updateRes = await updatePR(prl, prSelectRes as IPRSelect, githubApi, mode) } else { const prl = [] as IPRListItem[] repoList.forEach((val: string) => { @@ -41,12 +41,12 @@ export async function handleSelect(store: Storage) { }) // check pr log('info', 'Checking PR......') - const prListByRepo = await checkPR(prl, githubApi) + const prListByRepo = await checkPR(prl, githubApi, mode) // select pr const prSelectRes = await promptsRun(createPrOption(prListByRepo as IPRCheckRes[])) // update pr log('info', 'Update PR......') - updateRes = await updatePR(prl, prSelectRes as IPRSelect, githubApi) + updateRes = await updatePR(prl, prSelectRes as IPRSelect, githubApi, mode) } spinner.succeed() @@ -54,7 +54,7 @@ export async function handleSelect(store: Storage) { await printUpdateRes(updateRes as IPRCheckRes[]) } -async function checkPR(prl: IPRListItem[], githubApi: GitApi) { +async function checkPR(prl: IPRListItem[], githubApi: GitApi, mode: 'merge' | 'rebase') { if (prl.length === 0) { log('error', 'Please select a pr to check') process.exit() @@ -63,7 +63,7 @@ async function checkPR(prl: IPRListItem[], githubApi: GitApi) { // get pr detail data const prInfo = await githubApi.getPRByRepo(prl[i].number, prl[i].repo, prl[i].title) // need update pr ? - const res = await githubApi.needUpdate(prl[i].repo, prInfo as IPRInfo) + const res = await githubApi.needUpdate(prl[i].repo, prInfo as IPRInfo, mode) log('success', `✔ Check PR #${prl[i].number} completed`) return { ...prInfo, @@ -73,12 +73,19 @@ async function checkPR(prl: IPRListItem[], githubApi: GitApi) { return prListByRepo } -async function updatePR(prl: IPRListItem[], +async function updatePR( + prl: IPRListItem[], prSelectRes: IPRSelect, - githubApi: GitApi) { + githubApi: GitApi, + mode: 'merge' | 'rebase') { const updateRes = await Promise.all(createRunList(prl.length, async(i: number) => { if (prSelectRes.prSelect[i] && prSelectRes.prSelect[i].isNeedUpdate) { - await githubApi.updatePR(prSelectRes.prSelect[i].number, prSelectRes.prSelect[i].repo) + if (mode === 'rebase') + await githubApi.rebasePR(prSelectRes.prSelect[i].number, prSelectRes.prSelect[i].repo) + + if (mode === 'merge') + await githubApi.mergePR(prSelectRes.prSelect[i].number, prSelectRes.prSelect[i].repo) + log('success', `✔ Update PR #${prl[i].number} completed`) } return { diff --git a/utils/git-api.ts b/utils/git-api.ts index 23c5c90..70d92f6 100644 --- a/utils/git-api.ts +++ b/utils/git-api.ts @@ -1,5 +1,5 @@ import { Octokit } from '@octokit/core' -import { log } from '@pr-checker/utils' +import { createRunList, isEmptyObj, log } from '@pr-checker/utils' export declare interface IPRListItem { title: string number: number @@ -36,15 +36,18 @@ export class GitApi { } /** - * 获取账户下所有 pr + * 获取账户下所有提交 pr * @param username */ - async getPRList(username: string = this.owner) { + async getSubmitPRList(username: string = this.owner) { try { const { data } = await this.octokit.request('GET /search/issues', { q: `is:pr is:open author:${username}`, per_page: 1000, }) + if (!data.items || (data.items && data.items.length === 0)) + log('error', 'You don\'t have any pull requests that are open') + const res = {} as Record data.items.forEach((val: any) => { const repo = val.repository_url.split('repos/')[1] @@ -56,8 +59,47 @@ export class GitApi { id: val.id, }) }) - if (!data.items || (data.items && data.items.length === 0)) - log('error', 'You don\'t have any pull requests that are open') + return res + } catch (error: any) { + if (error.status === 401) + log('error', 'Your token is invalid or does not match your username') + + log('error', error) + return {} + } + } + + async getAllRepoPRList(username: string = this.owner) { + try { + const { data } = await this.octokit.request('GET /user/repos', { + type: 'owner', + per_page: 1000, + }) + const notForkRepoList = data.filter(v => !v.fork) + const res = {} as Record + await Promise.all(createRunList(notForkRepoList.length, async(i: number) => { + const { data: prData } = await this.octokit.request( + 'GET /repos/{owner}/{repo}/pulls', + { + owner: username, + repo: notForkRepoList[i].name, + per_page: 1000, + }) + + prData.forEach((val: any) => { + const repo = notForkRepoList[i].full_name + if (!res[repo]) res[repo] = [] + res[repo].push({ + title: val.title, + number: val.number, + repo, + id: val.id, + }) + }) + })) + + if (isEmptyObj(res)) + log('error', 'You don\'t have any pull requests') return res } catch (error: any) { @@ -65,7 +107,7 @@ export class GitApi { log('error', 'Your token is invalid or does not match your username') log('error', error) - return [] + return {} } } @@ -108,8 +150,9 @@ export class GitApi { * Determine whether pr wants to synchronize the upstream Repo * @param repo_name upstream Repo name * @param pr_info The getPRByRepo function returns the result + * @param mode */ - async needUpdate(repo_name: string, pr_info: IPRInfo) { + async needUpdate(repo_name: string, pr_info: IPRInfo, mode: 'rebase' | 'merge') { try { if (pr_info.mergeable_state === 'dirty') return { isNeedUpdate: false, reason: 'code conflict' } @@ -122,7 +165,8 @@ export class GitApi { // Comparison of the default branch hash of // the fork warehouse and the upstream warehouse hash const cur_sha = data[0].sha - if (pr_info.sha !== cur_sha + const matchedSha = (mode === 'rebase' && pr_info.sha !== cur_sha) || mode === 'merge' + if (matchedSha && pr_info.mergeable && !pr_info.merged) return { isNeedUpdate: true, reason: '--' } @@ -142,7 +186,7 @@ export class GitApi { * @param pull_number * @param repo_name */ - async updatePR(pull_number: number, repo_name: string) { + async rebasePR(pull_number: number, repo_name: string) { try { const res = await this.octokit.request( `PUT /repos/${repo_name}/pulls/{pull_number}/update-branch`, @@ -156,6 +200,21 @@ export class GitApi { return {} } } + + async mergePR(pull_number: number, repo_name: string) { + try { + const res = await this.octokit.request( + `PUT /repos/${repo_name}/pulls/{pull_number}/merge`, + { + pull_number, + }, + ) + return res.data.message + } catch (error: any) { + log('error', error) + return {} + } + } } export async function getUserName(token: string) { diff --git a/utils/index.ts b/utils/index.ts index 40e3400..60ab8fb 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -3,7 +3,7 @@ export * from './git-api' export function createRunList( taskNum: number, - taskFunc: (index: number) => Promise>) { + taskFunc: (index: number) => Promise | void>) { const taskList = [] as Array> for (let i = 0; i < taskNum; i++) { taskList.push(new Promise((resolve) => {