From ef37fbc77226f936459e2edf65ef7f7818d2e800 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Fri, 8 Jan 2021 16:23:39 +1100 Subject: [PATCH 01/17] Adding folder for Webstorm project files to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3c3629e..eb79dd5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.idea From 61802e9d1081b222549f65cb15de8dc49bdf713e Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Fri, 8 Jan 2021 16:25:49 +1100 Subject: [PATCH 02/17] Added pr-train config file to .gitignore as well --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index eb79dd5..0a43334 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules +# Intellij IDE project settings .idea +# Because dogfooding +.pr-train.yml From 1716cfc6969cd17f6de624c6f33799b5b17a16a1 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Fri, 8 Jan 2021 15:14:32 +1100 Subject: [PATCH 03/17] Added ability to customise the base branch --- README.md | 25 +++++++++++ cfg_template.yml | 4 ++ consts.js | 1 + github.js | 36 +++++++++++++--- index.js | 107 ++++++++++++++++++++++++++++++----------------- 5 files changed, 129 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 222a558..dd82d27 100644 --- a/README.md +++ b/README.md @@ -108,3 +108,28 @@ Unlike the sub-branches, the combined branch doesn't need to exist when you run Run `git pr-train` in your working dir when you're on any branch that belongs to a PR train. You don't have to be on the first branch, any branch will do. Use `-r/--rebase` option if you'd like to rebase branches on top of each other rather than merge (note: you will have to push with `git pr-train -pf` in that case). `git pr-train -p` will merge/rebase and push your updated changes to remote `origin` (configurable via `--remote` option). + +## No master? No problem! + +_All your base are belong to us._ - CATS + +Are you working in a repository that doesn't use `master` as the main (default) branch? +For example, newer repos use `main` instead. +Or do you have a different branch that you want all PR trains to use as a base? + +Add a section to the top of the config like so: + +```yml +prs: + main-branch-name: main + +trains: + # existing train config +``` + +### Override the base branch when creating PRs + +You can override the base branch to use when creating PRs by passing the `--base `. This takes precedence +over the main branch specified in the config file. + +e.g. `git pr-train -p -c -b feat/my-feature-base` diff --git a/cfg_template.yml b/cfg_template.yml index 08646b0..b3afb3c 100644 --- a/cfg_template.yml +++ b/cfg_template.yml @@ -1,3 +1,7 @@ +prs: + # Change this if you use a different name for your default branch (e.g. main) + main-branch-name: master + trains: # This is an example. This PR train would have 4 branches plus 1 "combined" branch to run tests etc on. # diff --git a/consts.js b/consts.js index 0d24f16..a2b290b 100644 --- a/consts.js +++ b/consts.js @@ -1,5 +1,6 @@ module.exports = { DEFAULT_REMOTE: 'origin', + DEFAULT_MAIN_BRANCH: 'master', MERGE_STEP_DELAY_MS: 1000, MERGE_STEP_DELAY_WAIT_FOR_LOCK: 2500, } diff --git a/github.js b/github.js index b3e669e..b39f494 100644 --- a/github.js +++ b/github.js @@ -2,9 +2,11 @@ const octo = require('octonode'); const promptly = require('promptly'); const { - DEFAULT_REMOTE + DEFAULT_REMOTE, + DEFAULT_MAIN_BRANCH } = require('./consts'); const fs = require('fs'); +const get = require('lodash/get'); const colors = require('colors'); const emoji = require('node-emoji'); const simpleGit = require('simple-git/promise'); @@ -74,14 +76,35 @@ function upsertNavigationInBody(newNavigation, body) { } } +function checkAndReportInvalidBaseError(e, base) { + const { field, code } = get(e, 'body.errors[0]', {}); + if (field === 'base' && code === 'invalid') { + console.log([ + emoji.get('no_entry'), + `\n${emoji.get('confounded')} This is embarrassing. `, + `The base branch of ${base.bold} doesn't seem to exist on the remote.`, + `\nDid you forget to ${emoji.get('arrow_up')} push it?`, + ].join('')); + return true; + } + return false; +} + /** * * @param {simpleGit.SimpleGit} sg * @param {Array.} allBranches * @param {string} combinedBranch * @param {string} remote + * @param {string} baseBranch */ -async function ensurePrsExist(sg, allBranches, combinedBranch, remote = DEFAULT_REMOTE) { +async function ensurePrsExist({ + sg, + allBranches, + combinedBranch, + remote = DEFAULT_REMOTE, + baseBranch = DEFAULT_MAIN_BRANCH +}) { //const allBranches = combinedBranch ? sortedBranches.concat(combinedBranch) : sortedBranches; const octoClient = octo.client(readGHKey()); // TODO: take remote name from `-r` value. @@ -145,7 +168,7 @@ async function ensurePrsExist(sg, allBranches, combinedBranch, remote = DEFAULT_ title, body } = branch === combinedBranch ? getCombinedBranchPrMsg() : await constructPrMsg(sg, branch); - const base = index === 0 || branch === combinedBranch ? 'master' : allBranches[index - 1]; + const base = index === 0 || branch === combinedBranch ? baseBranch : allBranches[index - 1]; process.stdout.write(`Checking if PR for branch ${branch} already exists... `); const prs = await ghRepo.prsAsync({ head: `${nick}:${branch}`, @@ -163,11 +186,14 @@ async function ensurePrsExist(sg, allBranches, combinedBranch, remote = DEFAULT_ title, body, }; - process.stdout.write(`Creating PR for branch "${branch}"...`); + const baseMessage = base === baseBranch ? colors.dim(` (against ${base})`) : ''; + process.stdout.write(`Creating PRs for branch "${branch}"${baseMessage}...`); try { prResponse = (await ghRepo.prAsync(payload))[0]; } catch (e) { - console.error(JSON.stringify(e, null, 2)); + if (!checkAndReportInvalidBaseError(e, base)) { + console.error(JSON.stringify(e, null, 2)); + } throw e; } console.log(emoji.get('white_check_mark')); diff --git a/index.js b/index.js index d475573..e7c4f24 100755 --- a/index.js +++ b/index.js @@ -9,12 +9,13 @@ const fs = require('fs'); const yaml = require('js-yaml'); const { ensurePrsExist, readGHKey, checkGHKeyExists } = require('./github'); const colors = require('colors'); -const { DEFAULT_REMOTE, MERGE_STEP_DELAY_MS, MERGE_STEP_DELAY_WAIT_FOR_LOCK } = require('./consts'); +const { DEFAULT_REMOTE, DEFAULT_MAIN_BRANCH, MERGE_STEP_DELAY_MS, MERGE_STEP_DELAY_WAIT_FOR_LOCK } = require('./consts'); const path = require('path'); // @ts-ignore const package = require('./package.json'); const inquirer = require('inquirer'); const shelljs = require('shelljs'); +const camelCase = require('lodash/camelCase'); const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); @@ -63,8 +64,8 @@ async function pushBranches(sg, branches, forcePush, remote = DEFAULT_REMOTE) { console.log('All changes pushed ' + emoji.get('white_check_mark')); } -async function getUnmergedBranches(sg, branches) { - const mergedBranchesOutput = await sg.raw(['branch', '--merged', 'master']); +async function getUnmergedBranches(sg, branches, baseBranch = DEFAULT_MAIN_BRANCH) { + const mergedBranchesOutput = await sg.raw(['branch', '--merged', baseBranch]); const mergedBranches = mergedBranchesOutput .split('\n') .map(b => b.trim()) @@ -79,12 +80,14 @@ async function getConfigPath(sg) { /** * @typedef {string | Object.} BranchCfg + * @typedef {{ mainBranch?: string, 'main-branch'?: string }} PrsCfg * @typedef {Object.>} TrainCfg + * @typedef {{ prs?: PrsCfg, trains: Array.}} YamlCfg */ /** * @param {simpleGit.SimpleGit} sg - * @return {Promise.<{trains: Array.}>} + * @return {Promise.} */ async function loadConfig(sg) { const path = await getConfigPath(sg); @@ -161,7 +164,56 @@ async function handleSwitchToBranchCommand(sg, sortedBranches, combinedBranch) { process.exit(0); } +/** + * @param {YamlCfg} ymlConfig + * @param {string} path + */ +function getConfigOption(ymlConfig, path) { + const parts = path.split(/\./g); + let ptr = ymlConfig; + while (ptr && parts.length) { + const part = parts.shift(); + // cater for both kebab case and camel cased variants of key, just for developer convenience. + ptr = part in ptr ? ptr[part] : ptr[camelCase(part)]; + } + return ptr; +} + async function main() { + const sg = simpleGit(); + if (!(await sg.checkIsRepo())) { + console.log('Not a git repo'.red); + process.exit(1); + } + + // try to create or init the config first so we can read values from it + if (process.argv.includes('--init')) { + if (fs.existsSync(await getConfigPath(sg))) { + console.log('.pr-train.yml already exists'); + process.exit(1); + } + const root = path.dirname(require.main.filename); + const cfgTpl = fs.readFileSync(`${root}/cfg_template.yml`); + fs.writeFileSync(await getConfigPath(sg), cfgTpl); + console.log(`Created a ".pr-train.yml" file. Please make sure it's gitignored.`); + process.exit(0); + } + + let ymlConfig; + try { + ymlConfig = await loadConfig(sg); + } catch (e) { + if (e instanceof yaml.YAMLException) { + console.log('There seems to be an error in `.pr-train.yml`.'); + console.log(e.message); + process.exit(1); + } + console.log('`.pr-train.yml` file not found. Please run `git pr-train --init` to create one.'.red); + process.exit(1); + } + + const defaultBase = getConfigOption(ymlConfig, 'prs.main-branch-name') || DEFAULT_MAIN_BRANCH; + program .version(package.version) .option('--init', 'Creates a .pr-train.yml file with an example configuration') @@ -169,8 +221,9 @@ async function main() { .option('-l, --list', 'List branches in current train') .option('-r, --rebase', 'Rebase branches rather than merging them') .option('-f, --force', 'Force push to remote') - .option('--push-merged', 'Push all branches (inclusing those that have already been merged into master)') + .option('--push-merged', 'Push all branches (including those that have already been merged into the base branch)') .option('--remote ', 'Set remote to push to. Defaults to "origin"') + .option('-b, --base ', `Specify the base branch to use for the first and combined PRs.`, defaultBase) .option('-c, --create-prs', 'Create GitHub PRs from your train branches'); program.on('--help', () => { @@ -199,37 +252,7 @@ async function main() { program.createPrs && checkGHKeyExists(); - const sg = simpleGit(); - if (!(await sg.checkIsRepo())) { - console.log('Not a git repo'.red); - process.exit(1); - } - - if (program.init) { - if (fs.existsSync(await getConfigPath(sg))) { - console.log('.pr-train.yml already exists'); - process.exit(1); - } - const root = path.dirname(require.main.filename); - const cfgTpl = fs.readFileSync(`${root}/cfg_template.yml`); - fs.writeFileSync(await getConfigPath(sg), cfgTpl); - console.log(`Created a ".pr-train.yml" file. Please make sure it's gitignored.`); - process.exit(0); - } - - let ymlConfig; - try { - ymlConfig = await loadConfig(sg); - } catch (e) { - if (e instanceof yaml.YAMLException) { - console.log('There seems to be an error in `.pr-train.yml`.'); - console.log(e.message); - process.exit(1); - } - console.log('`.pr-train.yml` file not found. Please run `git pr-train --init` to create one.'.red); - process.exit(1); - } - + const baseBranch = program.base; // will have default value if one is not supplied const { current: currentBranch, all: allBranches } = await sg.branchLocal(); const trainCfg = await getBranchesConfigInCurrentTrain(sg, ymlConfig); if (!trainCfg) { @@ -273,7 +296,7 @@ async function main() { async function findAndPushBranches() { let branchesToPush = sortedTrainBranches; if (!program.pushMerged) { - branchesToPush = await getUnmergedBranches(sg, sortedTrainBranches); + branchesToPush = await getUnmergedBranches(sg, sortedTrainBranches, baseBranch); const branchDiff = difference(sortedTrainBranches, branchesToPush); if (branchDiff.length > 0) { console.log(`Not pushing already merged branches: ${branchDiff.join(', ')}`); @@ -286,7 +309,13 @@ async function main() { // the PR titles and descriptions). Just push and create the PRs. if (program.createPrs) { await findAndPushBranches(); - await ensurePrsExist(sg, sortedTrainBranches, combinedTrainBranch, program.remote); + await ensurePrsExist({ + sg, + allBranches: sortedTrainBranches, + combinedBranch: combinedTrainBranch, + remote: program.remote, + baseBranch, + }); return; } @@ -309,6 +338,6 @@ async function main() { } main().catch(e => { - console.log(`${emoji.get('x')} An error occured. Was there a conflict perhaps?`.red); + console.log(`${emoji.get('x')} An error occurred. Was there a conflict perhaps?`.red); console.error('error', e); }); From 724a04bbd381319c1391347f8255ce071bebc692 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Sun, 10 Jan 2021 01:36:35 +1100 Subject: [PATCH 04/17] Remove dedicated type for config object, it doesn't provide any benefit. --- index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.js b/index.js index e7c4f24..e57a361 100755 --- a/index.js +++ b/index.js @@ -80,9 +80,8 @@ async function getConfigPath(sg) { /** * @typedef {string | Object.} BranchCfg - * @typedef {{ mainBranch?: string, 'main-branch'?: string }} PrsCfg * @typedef {Object.>} TrainCfg - * @typedef {{ prs?: PrsCfg, trains: Array.}} YamlCfg + * @typedef {{ prs?: Object, trains: Array.}} YamlCfg */ /** From 1333e46638c8bf3d338584f634ccf78503ae6e8a Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Mon, 18 Jan 2021 13:39:34 +1100 Subject: [PATCH 05/17] Address CR feedback --- cfg_template.yml | 4 ++-- consts.js | 2 +- github.js | 4 ++-- index.js | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cfg_template.yml b/cfg_template.yml index b3afb3c..f26543b 100644 --- a/cfg_template.yml +++ b/cfg_template.yml @@ -1,6 +1,6 @@ prs: - # Change this if you use a different name for your default branch (e.g. main) - main-branch-name: master + # Uncomment this if you use a different name for your default branch (e.g. "main" instead of "master") + # main-branch-name: main trains: # This is an example. This PR train would have 4 branches plus 1 "combined" branch to run tests etc on. diff --git a/consts.js b/consts.js index a2b290b..4a8a05f 100644 --- a/consts.js +++ b/consts.js @@ -1,6 +1,6 @@ module.exports = { DEFAULT_REMOTE: 'origin', - DEFAULT_MAIN_BRANCH: 'master', + DEFAULT_BASE_BRANCH: 'master', MERGE_STEP_DELAY_MS: 1000, MERGE_STEP_DELAY_WAIT_FOR_LOCK: 2500, } diff --git a/github.js b/github.js index b39f494..e012702 100644 --- a/github.js +++ b/github.js @@ -3,7 +3,7 @@ const octo = require('octonode'); const promptly = require('promptly'); const { DEFAULT_REMOTE, - DEFAULT_MAIN_BRANCH + DEFAULT_BASE_BRANCH } = require('./consts'); const fs = require('fs'); const get = require('lodash/get'); @@ -103,7 +103,7 @@ async function ensurePrsExist({ allBranches, combinedBranch, remote = DEFAULT_REMOTE, - baseBranch = DEFAULT_MAIN_BRANCH + baseBranch = DEFAULT_BASE_BRANCH }) { //const allBranches = combinedBranch ? sortedBranches.concat(combinedBranch) : sortedBranches; const octoClient = octo.client(readGHKey()); diff --git a/index.js b/index.js index e57a361..f1e0dfa 100755 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ const fs = require('fs'); const yaml = require('js-yaml'); const { ensurePrsExist, readGHKey, checkGHKeyExists } = require('./github'); const colors = require('colors'); -const { DEFAULT_REMOTE, DEFAULT_MAIN_BRANCH, MERGE_STEP_DELAY_MS, MERGE_STEP_DELAY_WAIT_FOR_LOCK } = require('./consts'); +const { DEFAULT_REMOTE, DEFAULT_BASE_BRANCH, MERGE_STEP_DELAY_MS, MERGE_STEP_DELAY_WAIT_FOR_LOCK } = require('./consts'); const path = require('path'); // @ts-ignore const package = require('./package.json'); @@ -64,7 +64,7 @@ async function pushBranches(sg, branches, forcePush, remote = DEFAULT_REMOTE) { console.log('All changes pushed ' + emoji.get('white_check_mark')); } -async function getUnmergedBranches(sg, branches, baseBranch = DEFAULT_MAIN_BRANCH) { +async function getUnmergedBranches(sg, branches, baseBranch = DEFAULT_BASE_BRANCH) { const mergedBranchesOutput = await sg.raw(['branch', '--merged', baseBranch]); const mergedBranches = mergedBranchesOutput .split('\n') @@ -211,7 +211,7 @@ async function main() { process.exit(1); } - const defaultBase = getConfigOption(ymlConfig, 'prs.main-branch-name') || DEFAULT_MAIN_BRANCH; + const defaultBase = getConfigOption(ymlConfig, 'prs.main-branch-name') || DEFAULT_BASE_BRANCH; program .version(package.version) From 7b0945f655ef10dd1e2a3b689586d2250e9c144a Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Mon, 18 Jan 2021 13:43:38 +1100 Subject: [PATCH 06/17] Implement support for creating draft PRs # Conflicts: # cfg_template.yml # index.js --- README.md | 17 +++++++++++++++++ cfg_template.yml | 5 +++++ github.js | 9 +++++++-- index.js | 24 ++++++++++++++++++++---- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dd82d27..d296241 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,23 @@ If you run with `--create-prs` again, `pr-train` will only override the Table of **Pro-tip**: If you want to udpate the ToCs in your GitHub PRs, just update the PR titles and re-run pr train with `--create-prs` - it will do the right thing. +### Draft PRs + +To create PRs in draft mode ([if your repo allows](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests)), +pass the `-d` or `--draft` argument on the command line (instead of, or in addition to, `-c`/`--create-prs`). + +You can also configure PRs to be created in draft mode by default if you add the following section to your `.pr-train.yml` file: + +```yaml +prs: + draft-by-default: true + +trains: + # etc +``` + +Specifying this option will allow you to omit the `-d`/`--draft` parameter (though you still need to specify `-c`/`--create-prs`) when you want to create/update PRs. + ## Example with explanation You finished coding a feature and now you have a patch that is over 1000 SLOCs long. That's a big patch. As a good citizen, you want to split the diff into multiple PRs, e.g.: diff --git a/cfg_template.yml b/cfg_template.yml index f26543b..559bef2 100644 --- a/cfg_template.yml +++ b/cfg_template.yml @@ -1,6 +1,11 @@ prs: # Uncomment this if you use a different name for your default branch (e.g. "main" instead of "master") # main-branch-name: main + # + # Create new PRs in draft mode by default. This will prevent them from notifying anybody + # until you are ready. This option can be overridden on the command line using the + # -d/--draft or --no-draft options, which set draft mode to true or false, respectively. + draft-by-default: true trains: # This is an example. This PR train would have 4 branches plus 1 "combined" branch to run tests etc on. diff --git a/github.js b/github.js index e012702..e88da28 100644 --- a/github.js +++ b/github.js @@ -95,6 +95,7 @@ function checkAndReportInvalidBaseError(e, base) { * @param {simpleGit.SimpleGit} sg * @param {Array.} allBranches * @param {string} combinedBranch + * @param {boolean} draft * @param {string} remote * @param {string} baseBranch */ @@ -102,6 +103,7 @@ async function ensurePrsExist({ sg, allBranches, combinedBranch, + draft, remote = DEFAULT_REMOTE, baseBranch = DEFAULT_BASE_BRANCH }) { @@ -131,8 +133,10 @@ async function ensurePrsExist({ body: '', }); + const prText = draft ? 'draft PR' : 'PR'; + console.log(); - console.log('This will create (or update) PRs for the following branches:'); + console.log(`This will create (or update) ${prText}s for the following branches:`); await allBranches.reduce(async (memo, branch) => { await memo; const { @@ -185,9 +189,10 @@ async function ensurePrsExist({ base, title, body, + draft, }; const baseMessage = base === baseBranch ? colors.dim(` (against ${base})`) : ''; - process.stdout.write(`Creating PRs for branch "${branch}"${baseMessage}...`); + process.stdout.write(`Creating ${prText} for branch "${branch}"${baseMessage}...`); try { prResponse = (await ghRepo.prAsync(payload))[0]; } catch (e) { diff --git a/index.js b/index.js index f1e0dfa..8023495 100755 --- a/index.js +++ b/index.js @@ -9,7 +9,12 @@ const fs = require('fs'); const yaml = require('js-yaml'); const { ensurePrsExist, readGHKey, checkGHKeyExists } = require('./github'); const colors = require('colors'); -const { DEFAULT_REMOTE, DEFAULT_BASE_BRANCH, MERGE_STEP_DELAY_MS, MERGE_STEP_DELAY_WAIT_FOR_LOCK } = require('./consts'); +const { + DEFAULT_REMOTE, + DEFAULT_BASE_BRANCH, + MERGE_STEP_DELAY_MS, + MERGE_STEP_DELAY_WAIT_FOR_LOCK, +} = require('./consts'); const path = require('path'); // @ts-ignore const package = require('./package.json'); @@ -130,7 +135,7 @@ function getBranchesInCurrentTrain(branchConfig) { */ function getCombinedBranch(branchConfig) { const combinedBranch = /** @type {Object} */ branchConfig.find(cfg => { - if (typeof cfg === 'string') { + if (!cfg || typeof cfg === 'string') { return false; } const branchName = Object.keys(cfg)[0]; @@ -223,6 +228,8 @@ async function main() { .option('--push-merged', 'Push all branches (including those that have already been merged into the base branch)') .option('--remote ', 'Set remote to push to. Defaults to "origin"') .option('-b, --base ', `Specify the base branch to use for the first and combined PRs.`, defaultBase) + .option('-d, --draft', 'Create PRs in draft mode. Implies --create-prs.') + .option('--no-draft', 'Do not create PRs in draft mode. Implies --create-prs.') .option('-c, --create-prs', 'Create GitHub PRs from your train branches'); program.on('--help', () => { @@ -249,9 +256,17 @@ async function main() { program.parse(process.argv); - program.createPrs && checkGHKeyExists(); + const createPrs = program.draft != null || program.createPrs; + + createPrs && checkGHKeyExists(); const baseBranch = program.base; // will have default value if one is not supplied + + // if there is no `-d`/`--draft`/`--no-draft` option specified, try to extract it from the config file + const draft = program.draft != null + ? program.draft + : !!getConfigOption(ymlConfig, 'prs.draft-by-default'); + const { current: currentBranch, all: allBranches } = await sg.branchLocal(); const trainCfg = await getBranchesConfigInCurrentTrain(sg, ymlConfig); if (!trainCfg) { @@ -306,13 +321,14 @@ async function main() { // If we're creating PRs, don't combine branches (that might change branch HEADs and consequently // the PR titles and descriptions). Just push and create the PRs. - if (program.createPrs) { + if (createPrs) { await findAndPushBranches(); await ensurePrsExist({ sg, allBranches: sortedTrainBranches, combinedBranch: combinedTrainBranch, remote: program.remote, + draft, baseBranch, }); return; From a103fc6a6ecd88f738ac774907bca2b540d566b3 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Mon, 18 Jan 2021 13:46:13 +1100 Subject: [PATCH 07/17] Fixed bug where branches were always being pushed # Conflicts: # index.js --- index.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 8023495..2dc64d8 100755 --- a/index.js +++ b/index.js @@ -217,6 +217,7 @@ async function main() { } const defaultBase = getConfigOption(ymlConfig, 'prs.main-branch-name') || DEFAULT_BASE_BRANCH; + const draftByDefault = getConfigOption(ymlConfig, 'prs.draft-by-default'); program .version(package.version) @@ -227,10 +228,13 @@ async function main() { .option('-f, --force', 'Force push to remote') .option('--push-merged', 'Push all branches (including those that have already been merged into the base branch)') .option('--remote ', 'Set remote to push to. Defaults to "origin"') - .option('-b, --base ', `Specify the base branch to use for the first and combined PRs.`, defaultBase) - .option('-d, --draft', 'Create PRs in draft mode. Implies --create-prs.') - .option('--no-draft', 'Do not create PRs in draft mode. Implies --create-prs.') - .option('-c, --create-prs', 'Create GitHub PRs from your train branches'); + .option('-b, --base ', `Specify the base branch to use for the first and combined PRs.`, defaultBase); + if (draftByDefault) { + program.option('--no-draft', 'Do not create PRs in draft mode (override default)'); + } else { + program.option('-d, --draft', 'Create PRs in draft mode') + } + program.option('-c, --create-prs', 'Create GitHub PRs from your train branches'); program.on('--help', () => { console.log(''); @@ -256,16 +260,11 @@ async function main() { program.parse(process.argv); - const createPrs = program.draft != null || program.createPrs; - - createPrs && checkGHKeyExists(); + program.createPrs && checkGHKeyExists(); const baseBranch = program.base; // will have default value if one is not supplied - // if there is no `-d`/`--draft`/`--no-draft` option specified, try to extract it from the config file - const draft = program.draft != null - ? program.draft - : !!getConfigOption(ymlConfig, 'prs.draft-by-default'); + const draft = program.draft != null ? program.draft : draftByDefault; const { current: currentBranch, all: allBranches } = await sg.branchLocal(); const trainCfg = await getBranchesConfigInCurrentTrain(sg, ymlConfig); @@ -321,7 +320,7 @@ async function main() { // If we're creating PRs, don't combine branches (that might change branch HEADs and consequently // the PR titles and descriptions). Just push and create the PRs. - if (createPrs) { + if (program.createPrs) { await findAndPushBranches(); await ensurePrsExist({ sg, From 75c74f3af8cfefa4b6b81e17e4798c8408a87164 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Sun, 10 Jan 2021 02:19:43 +1100 Subject: [PATCH 08/17] Clarify the README text --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d296241..9cb2926 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ If you run with `--create-prs` again, `pr-train` will only override the Table of ### Draft PRs To create PRs in draft mode ([if your repo allows](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests)), -pass the `-d` or `--draft` argument on the command line (instead of, or in addition to, `-c`/`--create-prs`). +pass the `-d` or `--draft` argument on the command line (in addition to `-c`/`--create-prs`). You can also configure PRs to be created in draft mode by default if you add the following section to your `.pr-train.yml` file: From 03746e4c1e2d7b8487b687f1d179453dc29d87aa Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Mon, 18 Jan 2021 14:09:43 +1100 Subject: [PATCH 09/17] Address CR comments --- index.js | 13 +++++-------- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 2dc64d8..8106963 100755 --- a/index.js +++ b/index.js @@ -217,7 +217,7 @@ async function main() { } const defaultBase = getConfigOption(ymlConfig, 'prs.main-branch-name') || DEFAULT_BASE_BRANCH; - const draftByDefault = getConfigOption(ymlConfig, 'prs.draft-by-default'); + const draftByDefault = !!getConfigOption(ymlConfig, 'prs.draft-by-default'); program .version(package.version) @@ -228,13 +228,10 @@ async function main() { .option('-f, --force', 'Force push to remote') .option('--push-merged', 'Push all branches (including those that have already been merged into the base branch)') .option('--remote ', 'Set remote to push to. Defaults to "origin"') - .option('-b, --base ', `Specify the base branch to use for the first and combined PRs.`, defaultBase); - if (draftByDefault) { - program.option('--no-draft', 'Do not create PRs in draft mode (override default)'); - } else { - program.option('-d, --draft', 'Create PRs in draft mode') - } - program.option('-c, --create-prs', 'Create GitHub PRs from your train branches'); + .option('-b, --base ', `Specify the base branch to use for the first and combined PRs.`, defaultBase) + .option('-d, --draft', 'Create PRs in draft mode', draftByDefault) + .option('--no-draft', 'Do not create PRs in draft mode', !draftByDefault) + .option('-c, --create-prs', 'Create GitHub PRs from your train branches'); program.on('--help', () => { console.log(''); diff --git a/package-lock.json b/package-lock.json index 824361a..2bb20b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -170,9 +170,9 @@ } }, "commander": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.0.tgz", - "integrity": "sha512-7B1ilBwtYSbetCgTY1NJFg+gVpestg0fdA1MhC1Vs4ssyfSXnCAjFr+QcQM9/RedXC0EaUx1sG8Smgw2VfgKEg==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" }, "concat-map": { "version": "0.0.1", diff --git a/package.json b/package.json index cb656ac..40a39c2 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "colors": "^1.2.1", - "commander": "^2.15.0", + "commander": "^3.0.2", "figlet": "^1.2.0", "inquirer": "^6.2.1", "js-yaml": "^3.13.1", From e83af0e68e726c0d50b33b762540bf9fbca39a0c Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Mon, 18 Jan 2021 14:31:20 +1100 Subject: [PATCH 10/17] Comment out draft-by-default option in cfg_template.yml --- cfg_template.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cfg_template.yml b/cfg_template.yml index 559bef2..e9ee25a 100644 --- a/cfg_template.yml +++ b/cfg_template.yml @@ -2,10 +2,10 @@ prs: # Uncomment this if you use a different name for your default branch (e.g. "main" instead of "master") # main-branch-name: main # - # Create new PRs in draft mode by default. This will prevent them from notifying anybody - # until you are ready. This option can be overridden on the command line using the + # Uncomment to create new PRs in draft mode by default. This will prevent them from notifying anybody + # (including CODEOWNERS) until you are ready. This option can be overridden on the command line using the # -d/--draft or --no-draft options, which set draft mode to true or false, respectively. - draft-by-default: true + # draft-by-default: true trains: # This is an example. This PR train would have 4 branches plus 1 "combined" branch to run tests etc on. From b55bc3398264530aa197f4aa8a7b7d75d2d2acf7 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Fri, 8 Jan 2021 15:33:08 +1100 Subject: [PATCH 11/17] Cater for body being nullish or empty Fixes #19 --- github.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/github.js b/github.js index e88da28..2a792a4 100644 --- a/github.js +++ b/github.js @@ -69,10 +69,11 @@ function readGHKey() { * @param {string} body */ function upsertNavigationInBody(newNavigation, body) { + body = body || ''; if (body.match(//)) { return body.replace(/[^]*<\/pr-train-toc>/, newNavigation); } else { - return body + '\n' + newNavigation; + return (body ? body + '\n' : '') + newNavigation; } } From 672fdd9604eb2e935fdfe272ff504b609882dba3 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Mon, 18 Jan 2021 14:41:32 +1100 Subject: [PATCH 12/17] Added support for table format for TOC # Conflicts: # cfg_template.yml # consts.js # github.js # index.js --- README.md | 29 +++++++++++++++++ cfg_template.yml | 3 ++ consts.js | 1 + github.js | 41 ++++++++++++++++-------- index.js | 2 ++ package-lock.json | 80 ++++++++++++++++++++++++++++++++++++++--------- package.json | 4 ++- 7 files changed, 131 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9cb2926..9d54df2 100644 --- a/README.md +++ b/README.md @@ -150,3 +150,32 @@ You can override the base branch to use when creating PRs by passing the `--base over the main branch specified in the config file. e.g. `git pr-train -p -c -b feat/my-feature-base` + +## Formatting the Table of Contents as an actual table + +If you would prefer that the Table of Contents use an actual markdown table format instead, +simply set `prs.toc-format` in the `.pr-train.yml` file to be `table`. + +e.g. + +```yml +prs: + # other config + toc-format: table + +trains: + # existing train config +``` + +This will yield something like the following: + +| | PR | Description | +| --- | --- | ----------- | +| | #1 | Foo | +| 👉 | #17 | Bar | +| | #3 | Baz | +| | #4 | Qux | +| | #5 | Quux | + + + diff --git a/cfg_template.yml b/cfg_template.yml index e9ee25a..e7a4a22 100644 --- a/cfg_template.yml +++ b/cfg_template.yml @@ -6,6 +6,9 @@ prs: # (including CODEOWNERS) until you are ready. This option can be overridden on the command line using the # -d/--draft or --no-draft options, which set draft mode to true or false, respectively. # draft-by-default: true + # + # Specify the format for the Table of Contents in each PR. Either `text` (default) or `table` + # toc-format: table trains: # This is an example. This PR train would have 4 branches plus 1 "combined" branch to run tests etc on. diff --git a/consts.js b/consts.js index 4a8a05f..a43217b 100644 --- a/consts.js +++ b/consts.js @@ -1,6 +1,7 @@ module.exports = { DEFAULT_REMOTE: 'origin', DEFAULT_BASE_BRANCH: 'master', + DEFAULT_FORMAT: 'text', MERGE_STEP_DELAY_MS: 1000, MERGE_STEP_DELAY_WAIT_FOR_LOCK: 2500, } diff --git a/github.js b/github.js index 2a792a4..06392f4 100644 --- a/github.js +++ b/github.js @@ -9,7 +9,8 @@ const fs = require('fs'); const get = require('lodash/get'); const colors = require('colors'); const emoji = require('node-emoji'); -const simpleGit = require('simple-git/promise'); +const table = require('markdown-table'); +const width = require('string-width'); /** * @@ -31,19 +32,31 @@ async function constructPrMsg(sg, branch) { * @param {Object.} branchToPrDict * @param {string} currentBranch * @param {string} combinedBranch + * @param {'text'|'table'} format */ -function constructTrainNavigation(branchToPrDict, currentBranch, combinedBranch) { - let contents = '\n\n#### PR chain:\n'; - contents = Object.keys(branchToPrDict).reduce((output, branch) => { - const maybeHandRight = branch === currentBranch ? '👉 ' : ''; +function constructTrainNavigation(branchToPrDict, currentBranch, combinedBranch, format) { + let contents = '\n\n'; + let tableData = [['', 'PR', 'Description']]; + Object.keys(branchToPrDict).forEach((branch) => { + const maybeHandRight = branch === currentBranch ? '👉 ' : ' '; const maybeHandLeft = branch === currentBranch ? ' 👈 **YOU ARE HERE**' : ''; const combinedInfo = branch === combinedBranch ? ' **[combined branch]** ' : ' '; - output += `${maybeHandRight}#${branchToPrDict[branch].pr}${combinedInfo}(${branchToPrDict[ - branch - ].title.trim()})${maybeHandLeft}`; - return output + '\n'; - }, contents); - contents += '\n'; + const prTitle = branchToPrDict[branch].title.trim(); + const prNumber = `#${branchToPrDict[branch].pr}`; + const prInfo = format === 'text' + ? `${combinedInfo}(${prTitle})` + : `${combinedInfo}${prTitle}`.trim(); + const parts = [maybeHandRight, prNumber, prInfo, format === 'text' && maybeHandLeft].filter(Boolean); + if (format === 'text') { + contents += parts.join('') + '\n'; + } else { + tableData.push(parts); + } + }); + if (format === 'table') { + contents += table(tableData, { stringLength: width }) + '\n'; + } + contents += '\n' return contents; } @@ -99,6 +112,7 @@ function checkAndReportInvalidBaseError(e, base) { * @param {boolean} draft * @param {string} remote * @param {string} baseBranch + * @param {'text'|'table'} format */ async function ensurePrsExist({ sg, @@ -106,7 +120,8 @@ async function ensurePrsExist({ combinedBranch, draft, remote = DEFAULT_REMOTE, - baseBranch = DEFAULT_BASE_BRANCH + baseBranch = DEFAULT_BASE_BRANCH, + format= 'text' }) { //const allBranches = combinedBranch ? sortedBranches.concat(combinedBranch) : sortedBranches; const octoClient = octo.client(readGHKey()); @@ -228,7 +243,7 @@ async function ensurePrsExist({ branch === combinedBranch ? getCombinedBranchPrMsg() : await constructPrMsg(sg, branch); - const navigation = constructTrainNavigation(prDict, branch, combinedBranch); + const navigation = constructTrainNavigation(prDict, branch, combinedBranch, format); const newBody = upsertNavigationInBody(navigation, body); process.stdout.write(`Updating PR for branch ${branch}...`); await ghPr.updateAsync({ diff --git a/index.js b/index.js index 8106963..789a189 100755 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ const colors = require('colors'); const { DEFAULT_REMOTE, DEFAULT_BASE_BRANCH, + DEFAULT_FORMAT, MERGE_STEP_DELAY_MS, MERGE_STEP_DELAY_WAIT_FOR_LOCK, } = require('./consts'); @@ -326,6 +327,7 @@ async function main() { remote: program.remote, draft, baseBranch, + format: getConfigOption(ymlConfig, 'prs.toc-format') || DEFAULT_FORMAT, }); return; } diff --git a/package-lock.json b/package-lock.json index 2bb20b0..fb93b20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,9 +27,9 @@ "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" }, "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { "version": "3.2.1", @@ -220,6 +220,11 @@ "safer-buffer": "^2.1.0" } }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -383,6 +388,37 @@ "string-width": "^2.1.0", "strip-ansi": "^5.0.0", "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + } } }, "interpret": { @@ -391,9 +427,9 @@ "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-promise": { "version": "2.1.0", @@ -471,6 +507,14 @@ "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" }, + "markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "requires": { + "repeat-string": "^1.0.0" + } + }, "mime-db": { "version": "1.35.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", @@ -620,6 +664,11 @@ "resolve": "^1.1.6" } }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, "request": { "version": "2.87.0", "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", @@ -735,20 +784,21 @@ } }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" }, "dependencies": { "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.0" } } } diff --git a/package.json b/package.json index 40a39c2..08a24dd 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,14 @@ "lodash": "^4.17.15", "lodash.difference": "^4.5.0", "lodash.sortby": "^4.7.0", + "markdown-table": "^2.0.0", "node-emoji": "^1.8.1", "octonode": "^0.9.3", "progress": "^2.0.0", "promptly": "^3.0.3", "shelljs": "^0.8.3", - "simple-git": "^1.102.0" + "simple-git": "^1.102.0", + "string-width": "^4.2.0" }, "devDependencies": { "@types/js-yaml": "^3.11.2" From 67f8efb57d62a62f8a841d6d47440d61b1f80614 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Mon, 18 Jan 2021 14:50:19 +1100 Subject: [PATCH 13/17] Remove configuration option and just change to always use table format. --- README.md | 29 ----------------------------- cfg_template.yml | 3 --- consts.js | 1 - github.js | 22 +++++----------------- index.js | 2 -- 5 files changed, 5 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 9d54df2..9cb2926 100644 --- a/README.md +++ b/README.md @@ -150,32 +150,3 @@ You can override the base branch to use when creating PRs by passing the `--base over the main branch specified in the config file. e.g. `git pr-train -p -c -b feat/my-feature-base` - -## Formatting the Table of Contents as an actual table - -If you would prefer that the Table of Contents use an actual markdown table format instead, -simply set `prs.toc-format` in the `.pr-train.yml` file to be `table`. - -e.g. - -```yml -prs: - # other config - toc-format: table - -trains: - # existing train config -``` - -This will yield something like the following: - -| | PR | Description | -| --- | --- | ----------- | -| | #1 | Foo | -| 👉 | #17 | Bar | -| | #3 | Baz | -| | #4 | Qux | -| | #5 | Quux | - - - diff --git a/cfg_template.yml b/cfg_template.yml index e7a4a22..e9ee25a 100644 --- a/cfg_template.yml +++ b/cfg_template.yml @@ -6,9 +6,6 @@ prs: # (including CODEOWNERS) until you are ready. This option can be overridden on the command line using the # -d/--draft or --no-draft options, which set draft mode to true or false, respectively. # draft-by-default: true - # - # Specify the format for the Table of Contents in each PR. Either `text` (default) or `table` - # toc-format: table trains: # This is an example. This PR train would have 4 branches plus 1 "combined" branch to run tests etc on. diff --git a/consts.js b/consts.js index a43217b..4a8a05f 100644 --- a/consts.js +++ b/consts.js @@ -1,7 +1,6 @@ module.exports = { DEFAULT_REMOTE: 'origin', DEFAULT_BASE_BRANCH: 'master', - DEFAULT_FORMAT: 'text', MERGE_STEP_DELAY_MS: 1000, MERGE_STEP_DELAY_WAIT_FOR_LOCK: 2500, } diff --git a/github.js b/github.js index 06392f4..5950533 100644 --- a/github.js +++ b/github.js @@ -32,30 +32,19 @@ async function constructPrMsg(sg, branch) { * @param {Object.} branchToPrDict * @param {string} currentBranch * @param {string} combinedBranch - * @param {'text'|'table'} format */ -function constructTrainNavigation(branchToPrDict, currentBranch, combinedBranch, format) { +function constructTrainNavigation(branchToPrDict, currentBranch, combinedBranch) { let contents = '\n\n'; let tableData = [['', 'PR', 'Description']]; Object.keys(branchToPrDict).forEach((branch) => { const maybeHandRight = branch === currentBranch ? '👉 ' : ' '; - const maybeHandLeft = branch === currentBranch ? ' 👈 **YOU ARE HERE**' : ''; const combinedInfo = branch === combinedBranch ? ' **[combined branch]** ' : ' '; const prTitle = branchToPrDict[branch].title.trim(); const prNumber = `#${branchToPrDict[branch].pr}`; - const prInfo = format === 'text' - ? `${combinedInfo}(${prTitle})` - : `${combinedInfo}${prTitle}`.trim(); - const parts = [maybeHandRight, prNumber, prInfo, format === 'text' && maybeHandLeft].filter(Boolean); - if (format === 'text') { - contents += parts.join('') + '\n'; - } else { - tableData.push(parts); - } + const prInfo = `${combinedInfo}${prTitle}`.trim(); + tableData.push([maybeHandRight, prNumber, prInfo]); }); - if (format === 'table') { - contents += table(tableData, { stringLength: width }) + '\n'; - } + contents += table(tableData, { stringLength: width }) + '\n'; contents += '\n' return contents; } @@ -121,7 +110,6 @@ async function ensurePrsExist({ draft, remote = DEFAULT_REMOTE, baseBranch = DEFAULT_BASE_BRANCH, - format= 'text' }) { //const allBranches = combinedBranch ? sortedBranches.concat(combinedBranch) : sortedBranches; const octoClient = octo.client(readGHKey()); @@ -243,7 +231,7 @@ async function ensurePrsExist({ branch === combinedBranch ? getCombinedBranchPrMsg() : await constructPrMsg(sg, branch); - const navigation = constructTrainNavigation(prDict, branch, combinedBranch, format); + const navigation = constructTrainNavigation(prDict, branch, combinedBranch); const newBody = upsertNavigationInBody(navigation, body); process.stdout.write(`Updating PR for branch ${branch}...`); await ghPr.updateAsync({ diff --git a/index.js b/index.js index 789a189..8106963 100755 --- a/index.js +++ b/index.js @@ -12,7 +12,6 @@ const colors = require('colors'); const { DEFAULT_REMOTE, DEFAULT_BASE_BRANCH, - DEFAULT_FORMAT, MERGE_STEP_DELAY_MS, MERGE_STEP_DELAY_WAIT_FOR_LOCK, } = require('./consts'); @@ -327,7 +326,6 @@ async function main() { remote: program.remote, draft, baseBranch, - format: getConfigOption(ymlConfig, 'prs.toc-format') || DEFAULT_FORMAT, }); return; } From ef7f1e14aa25a653ccd14f9f282a29a46cb16fdb Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Sat, 9 Jan 2021 23:41:55 +1100 Subject: [PATCH 14/17] Added support to print links to PRs in terminal --- README.md | 5 +++++ cfg_template.yml | 3 +++ github.js | 8 +++++--- index.js | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9cb2926..c3d5354 100644 --- a/README.md +++ b/README.md @@ -150,3 +150,8 @@ You can override the base branch to use when creating PRs by passing the `--base over the main branch specified in the config file. e.g. `git pr-train -p -c -b feat/my-feature-base` + +## Print the PR links to the terminal + +To have the command output include a link to the PR that was created or updated, +simply add `print-urls: true` to the `prs` section of the config file. diff --git a/cfg_template.yml b/cfg_template.yml index e9ee25a..3e43420 100644 --- a/cfg_template.yml +++ b/cfg_template.yml @@ -6,6 +6,9 @@ prs: # (including CODEOWNERS) until you are ready. This option can be overridden on the command line using the # -d/--draft or --no-draft options, which set draft mode to true or false, respectively. # draft-by-default: true + # + # Print out the links to the terminal for the created/updated PRs (default false) + # print-urls: true trains: # This is an example. This PR train would have 4 branches plus 1 "combined" branch to run tests etc on. diff --git a/github.js b/github.js index 5950533..14da639 100644 --- a/github.js +++ b/github.js @@ -101,7 +101,7 @@ function checkAndReportInvalidBaseError(e, base) { * @param {boolean} draft * @param {string} remote * @param {string} baseBranch - * @param {'text'|'table'} format + * @param {boolean} printLinks */ async function ensurePrsExist({ sg, @@ -110,6 +110,7 @@ async function ensurePrsExist({ draft, remote = DEFAULT_REMOTE, baseBranch = DEFAULT_BASE_BRANCH, + printLinks = false }) { //const allBranches = combinedBranch ? sortedBranches.concat(combinedBranch) : sortedBranches; const octoClient = octo.client(readGHKey()); @@ -234,11 +235,12 @@ async function ensurePrsExist({ const navigation = constructTrainNavigation(prDict, branch, combinedBranch); const newBody = upsertNavigationInBody(navigation, body); process.stdout.write(`Updating PR for branch ${branch}...`); - await ghPr.updateAsync({ + const updateResponse = await ghPr.updateAsync({ title, body: `${newBody}`, }); - console.log(emoji.get('white_check_mark')); + const prLink = get(updateResponse, '0._links.html.href', colors.yellow('Could not get URL')); + console.log(emoji.get('white_check_mark') + printLinks ? ` (${prLink})` : ''); }, Promise.resolve()); } diff --git a/index.js b/index.js index 8106963..866bd34 100755 --- a/index.js +++ b/index.js @@ -326,6 +326,7 @@ async function main() { remote: program.remote, draft, baseBranch, + printLinks: getConfigOption(ymlConfig, 'prs.print-urls'), }); return; } From fd941be83a6b5d1cf9dcd5a4f9dc23a75ebfe1e1 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Mon, 18 Jan 2021 20:16:41 +1100 Subject: [PATCH 15/17] =?UTF-8?q?Make=20sure=20=E2=9C=85=20appears?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- github.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github.js b/github.js index 14da639..410cf8a 100644 --- a/github.js +++ b/github.js @@ -240,7 +240,7 @@ async function ensurePrsExist({ body: `${newBody}`, }); const prLink = get(updateResponse, '0._links.html.href', colors.yellow('Could not get URL')); - console.log(emoji.get('white_check_mark') + printLinks ? ` (${prLink})` : ''); + console.log(emoji.get('white_check_mark') + (printLinks ? ` (${prLink})` : '')); }, Promise.resolve()); } From 0f03fb97e1a80db1c6cbcd086bd74196874ccc40 Mon Sep 17 00:00:00 2001 From: Greg Lockwood Date: Sun, 10 Jan 2021 00:50:53 +1100 Subject: [PATCH 16/17] Work when remote URL doesn't have .git suffix --- github.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github.js b/github.js index 410cf8a..4028081 100644 --- a/github.js +++ b/github.js @@ -156,7 +156,7 @@ async function ensurePrsExist({ process.exit(0); } - const nickAndRepo = remoteUrl.match(/github\.com[/:](.*)\.git/)[1]; + const nickAndRepo = remoteUrl.match(/github\.com[/:](.*)/)[1].replace(/\.git$/, ''); if (!nickAndRepo) { console.log(`I could not parse your remote ${remote} repo URL`.red); process.exit(4); From 45624d451277a53db72348268da1ca760b95b6ff Mon Sep 17 00:00:00 2001 From: Tomas Brambora Date: Sat, 30 Jan 2021 11:16:58 +1100 Subject: [PATCH 17/17] Update github.js --- github.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github.js b/github.js index 4028081..d1c207f 100644 --- a/github.js +++ b/github.js @@ -86,7 +86,7 @@ function checkAndReportInvalidBaseError(e, base) { emoji.get('no_entry'), `\n${emoji.get('confounded')} This is embarrassing. `, `The base branch of ${base.bold} doesn't seem to exist on the remote.`, - `\nDid you forget to ${emoji.get('arrow_up')} push it?`, + `\nDid you forget to ${emoji.get('arrow_up')} push it?`, ].join('')); return true; }