Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

My combined fixes for a few issues #31

Merged
merged 17 commits into from
Jan 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
node_modules
# Intellij IDE project settings
.idea
# Because dogfooding
.pr-train.yml
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (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.:
Expand Down Expand Up @@ -108,3 +125,33 @@ 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 <branch-name>`. This takes precedence
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.
12 changes: 12 additions & 0 deletions cfg_template.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
prs:
# Uncomment this if you use a different name for your default branch (e.g. "main" instead of "master")
# main-branch-name: main
#
# 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
#
# 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.
#
Expand Down
1 change: 1 addition & 0 deletions consts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
DEFAULT_REMOTE: 'origin',
DEFAULT_BASE_BRANCH: 'master',
MERGE_STEP_DELAY_MS: 1000,
MERGE_STEP_DELAY_WAIT_FOR_LOCK: 2500,
}
79 changes: 58 additions & 21 deletions github.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
const octo = require('octonode');
const promptly = require('promptly');
const {
DEFAULT_REMOTE
DEFAULT_REMOTE,
DEFAULT_BASE_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');
const table = require('markdown-table');
const width = require('string-width');

/**
*
Expand All @@ -31,17 +34,18 @@ async function constructPrMsg(sg, branch) {
* @param {string} combinedBranch
*/
function constructTrainNavigation(branchToPrDict, currentBranch, combinedBranch) {
let contents = '<pr-train-toc>\n\n#### PR chain:\n';
contents = Object.keys(branchToPrDict).reduce((output, branch) => {
const maybeHandRight = branch === currentBranch ? '👉 ' : '';
const maybeHandLeft = branch === currentBranch ? ' 👈 **YOU ARE HERE**' : '';
let contents = '<pr-train-toc>\n\n';
let tableData = [['', 'PR', 'Description']];
Object.keys(branchToPrDict).forEach((branch) => {
const maybeHandRight = branch === currentBranch ? '👉 ' : ' ';
const combinedInfo = branch === combinedBranch ? ' **[combined branch]** ' : ' ';
output += `${maybeHandRight}#${branchToPrDict[branch].pr}${combinedInfo}(${branchToPrDict[
branch
].title.trim()})${maybeHandLeft}`;
return output + '\n';
}, contents);
contents += '\n</pr-train-toc>';
const prTitle = branchToPrDict[branch].title.trim();
const prNumber = `#${branchToPrDict[branch].pr}`;
const prInfo = `${combinedInfo}${prTitle}`.trim();
tableData.push([maybeHandRight, prNumber, prInfo]);
});
contents += table(tableData, { stringLength: width }) + '\n';
contents += '\n</pr-train-toc>'
return contents;
}

Expand All @@ -67,21 +71,47 @@ function readGHKey() {
* @param {string} body
*/
function upsertNavigationInBody(newNavigation, body) {
body = body || '';
if (body.match(/<pr-train-toc>/)) {
return body.replace(/<pr-train-toc>[^]*<\/pr-train-toc>/, newNavigation);
} else {
return body + '\n' + newNavigation;
return (body ? body + '\n' : '') + newNavigation;
}
}

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.<string>} allBranches
* @param {string} combinedBranch
* @param {boolean} draft
* @param {string} remote
* @param {string} baseBranch
* @param {boolean} printLinks
*/
async function ensurePrsExist(sg, allBranches, combinedBranch, remote = DEFAULT_REMOTE) {
async function ensurePrsExist({
sg,
allBranches,
combinedBranch,
draft,
remote = DEFAULT_REMOTE,
baseBranch = DEFAULT_BASE_BRANCH,
printLinks = false
}) {
//const allBranches = combinedBranch ? sortedBranches.concat(combinedBranch) : sortedBranches;
const octoClient = octo.client(readGHKey());
// TODO: take remote name from `-r` value.
Expand All @@ -108,8 +138,10 @@ async function ensurePrsExist(sg, allBranches, combinedBranch, remote = DEFAULT_
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 {
Expand All @@ -124,7 +156,7 @@ async function ensurePrsExist(sg, allBranches, combinedBranch, remote = DEFAULT_
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);
Expand All @@ -145,7 +177,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}`,
Expand All @@ -162,12 +194,16 @@ async function ensurePrsExist(sg, allBranches, combinedBranch, remote = DEFAULT_
base,
title,
body,
draft,
};
process.stdout.write(`Creating PR for branch "${branch}"...`);
const baseMessage = base === baseBranch ? colors.dim(` (against ${base})`) : '';
process.stdout.write(`Creating ${prText} 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'));
Expand Down Expand Up @@ -199,11 +235,12 @@ async function ensurePrsExist(sg, allBranches, combinedBranch, remote = DEFAULT_
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());
}

Expand Down
Loading