diff --git a/.gitignore b/.gitignore index 02f0293..1fe442c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ npm-debug.log /tmp +/.vscode diff --git a/README.md b/README.md index 8970448..8840f26 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,22 @@ # Branch Workflow CLI A cli that provides a set of `git wf` subcommands which simplify dealing with -feature branches & GitHub pull requests. Does not require a GH API token, as +feature branches & GitHub pull requests. Does not require a GH API token, as it just opens your browser to complete Pull Request operations. -* creates named feature branches which track their intended "parent" (`start`) -* opens pull requests against the intended parent branch (`pr`) -* cleans up when done (`done`) -* aborts abandoned branches cleanly (`abort`) -* renames branches locally & on server (`rename`) -* additional optional release management commands (`cut-release`, `qa`, - `hotfix`, `merge-back`) +- creates named feature branches which track their intended "parent" (`start`) +- opens pull requests against the intended parent branch (`pr`) +- cleans up when done (`done`) +- aborts abandoned branches cleanly (`abort`) +- renames branches locally & on server (`rename`) +- additional optional release management commands (`cut-release`, `qa`, + `hotfix`, `merge-back`) + +## "master" vs "main" + +Below we use the term `main` to refer to your mainline branch; if you have a +`main` branch in your local checkout, we'll assume that's the one you're using. +If not, we'll assume you're using `master`. ## Installation @@ -31,9 +37,9 @@ $ git wf --help ## Commands The `start`, `pr`, `abort`, `rename`, and `done` commands can be used on **any** -project that has a master branch. +project that has a master or main branch. -All of the other commands will enforce the existence and use of the `master`, +All of the other commands will enforce the existence and use of the `main`, `release`, and `hotfix` branch naming scheme. ### `git wf start [--fork] ` - starts a new feature branch @@ -42,18 +48,18 @@ Given you are currently on branch `` 1. Updates the branch you currently have checked out with `git pull` 1. Creates a new feature branch named `` locally with - `git checkout -b ` + `git checkout -b ` 1. If you specified `--fork` or already have a remote named `fork`: - 1. verifies you have a remote named `fork` - 1. if you don't, verifies that `/` exists on github, - and if not prompts you to create it - 1. if you do have a github fork, creates the `fork` remote for you - 1. Pushes your feature branch to `fork` as a branch named - `feature//` with - `git push -u fork :feature/` + 1. verifies you have a remote named `fork` + 1. if you don't, verifies that `/` exists on github, + and if not prompts you to create it + 1. if you do have a github fork, creates the `fork` remote for you + 1. Pushes your feature branch to `fork` as a branch named + `feature//` with + `git push -u fork :feature/` 1. If you didn't, pushes your feature branch to `origin` as a branch named - `/feature//` with - `git push -u origin :/feature//` + `/feature//` with + `git push -u origin :/feature//` ### `git wf rename ` - renames a feature branch @@ -62,9 +68,9 @@ branch run this command, passing a new name, it will: 1. Fetch the latest commits from the remote 1. Create a new remote branch named correctly, based on the fetched - version of the old remote branch (no new commits from local) + version of the old remote branch (no new commits from local) 1. Create a new local branch with the new name, based on the current - local branch + local branch 1. Make the former the upstream of the latter 1. Delete the old local branch 1. Delete the old remote branch @@ -72,7 +78,7 @@ branch run this command, passing a new name, it will: ### `git wf abort` - aborts a feature If you decide you don't like your new feature, you may PERMANENTLY delete it, -locally and remotely, using `git wf abort`. This will: +locally and remotely, using `git wf abort`. This will: 1. Commit any working tree changes as a commit with message "WIP" 1. Save the SHA of whatever the final commit was @@ -95,11 +101,11 @@ Given you are currently on a feature branch named `` 1. Deletes the feature branch with `git branch -d ` 1. Cleans up the corresponding remote branch with `git remote prune origin` -### `git wf cut-release [branch]` - PRs starting a fresh release from master +### `git wf cut-release [branch]` - PRs starting a fresh release from main 1. Runs `git wf merge-back` (see below) -1. Opens a PR, as per `git wf pr` to merge `branch` (default: `master`) to - `release` +1. Opens a PR, as per `git wf pr` to merge `branch` (default: `main`) to + `release` ### `git wf qa [branch]` - Tags build of _branch_ @@ -108,7 +114,7 @@ Given you are currently on a feature branch named `` 1. Switches to `[branch]` with `git checkout [branch]` 1. Updates with `git pull --no-rebase` 1. Tags `HEAD` of `[branch]` as `build-YYYY.mm.dd_HH.MM.SS` with - `git tag build-...` + `git tag build-...` 1. Pushes tag with `git push origin tag build-...` ### `git wf hotfix ` - Moves the hotfix branch to given tag @@ -118,58 +124,58 @@ Given you are currently on a feature branch named `` 1. Fast-forward merges `hotfix` to given build tag 1. Pushes `hotfix` branch -### `git wf merge-back` - Merges all changes back from master ← release ← hotfix +### `git wf merge-back` - Merges all changes back from main ← release ← hotfix 1. Switches to `hotfix` branch 1. Pulls latest updates 1. Merges `hotfix` branch to `release` branch - if there are conflicts, it - creates a feature branch for you to clean up the results, and submit a PR. - If not, pushes the merged branch. -1. As before, but this time merging `release` onto `master` + creates a feature branch for you to clean up the results, and submit a PR. + If not, pushes the merged branch. +1. As before, but this time merging `release` onto `main` ## Example Flow Here's a narrative sequence of events in the life of a project: -* The project starts with branches `master`, `release`, and `hotfix` all - pointing at the same place -* On branch master, you `git wf start widget-fix` -* Now on branch `widget-fix`, you make some commits, decide it's ready to PR, - and run `git wf pr` -* The PR is tested, accepted, and merged, and at some point, while on branch - `widget-fix`, you run `git wf done`, which cleans it up -* You start a new features, `git wf start bad-ideea`, make a few commits, then - realize you named it wrong, so you `git wf rename bad-idea` - which is fine - until you realize you don't want it at all, so you `git wf abort` and it's - all gone. -* A few more good features go in, and it's time to `git wf cut-release` - - now your `release` branch is pointing up-to-date with `master`, and people - can resume adding features to `master` -* It's time to QA your upcoming release, so you `git wf qa release` which - creates a `build-...` tag -* Your shiny new `build-...` tag is available for deploying - however you do that, so you deploy it, QA it, and eventually release it - to production. -* Everything's progressing along, there's new stuff on `master`, maybe a - new release has even been cut to `release`, when you realize there's - a problem on production, so you run `git wf hotfix build-...` with the - build tag that's currently on production. Your `hotfix` branch is now - ready for fixes. -* From the `hotfix` branch, you `git wf start urgent-thingy` and now you're - on a feature branch off of `hotfix` - you make your commits to fix the - bug and `git wf pr` -* People review and approve your PR, it's merged to the `hotfix` branch, you - `git wf done` to cleanup -* `git wf qa hotfix` creates a new `build-...` tag off of the `hotfix` branch, - which can be QAed, then (quickly!) deployed to production -* Now's a good time to run `git wf merge-back`, which will take those commits - sitting on `hotfix` and merge them back onto the `release` branch you had - in progress. This goes cleanly, so it just does it for you. -* Then it goes to merge `release` back onto `master`, but uh-oh there are some - conflicts by now, because someone fixed the problem a different way on - `master`. No worries, `git wf` will detect that, create a feature branch - to resolve the conflicts, let you clean up the merge on that branch, and - then you `git wf pr` and it will open a PR to review the resolution. +- The project starts with branches `main`, `release`, and `hotfix` all + pointing at the same place +- On branch main, you `git wf start widget-fix` +- Now on branch `widget-fix`, you make some commits, decide it's ready to PR, + and run `git wf pr` +- The PR is tested, accepted, and merged, and at some point, while on branch + `widget-fix`, you run `git wf done`, which cleans it up +- You start a new features, `git wf start bad-ideea`, make a few commits, then + realize you named it wrong, so you `git wf rename bad-idea` - which is fine + until you realize you don't want it at all, so you `git wf abort` and it's + all gone. +- A few more good features go in, and it's time to `git wf cut-release` - + now your `release` branch is pointing up-to-date with `main`, and people + can resume adding features to `main` +- It's time to QA your upcoming release, so you `git wf qa release` which + creates a `build-...` tag +- Your shiny new `build-...` tag is available for deploying + however you do that, so you deploy it, QA it, and eventually release it + to production. +- Everything's progressing along, there's new stuff on `main`, maybe a + new release has even been cut to `release`, when you realize there's + a problem on production, so you run `git wf hotfix build-...` with the + build tag that's currently on production. Your `hotfix` branch is now + ready for fixes. +- From the `hotfix` branch, you `git wf start urgent-thingy` and now you're + on a feature branch off of `hotfix` - you make your commits to fix the + bug and `git wf pr` +- People review and approve your PR, it's merged to the `hotfix` branch, you + `git wf done` to cleanup +- `git wf qa hotfix` creates a new `build-...` tag off of the `hotfix` branch, + which can be QAed, then (quickly!) deployed to production +- Now's a good time to run `git wf merge-back`, which will take those commits + sitting on `hotfix` and merge them back onto the `release` branch you had + in progress. This goes cleanly, so it just does it for you. +- Then it goes to merge `release` back onto `main`, but uh-oh there are some + conflicts by now, because someone fixed the problem a different way on + `main`. No worries, `git wf` will detect that, create a feature branch + to resolve the conflicts, let you clean up the merge on that branch, and + then you `git wf pr` and it will open a PR to review the resolution. At every stage, you don't need to stop your forward progress, forget which your next planned release was, or anything else as you add new features and hotfix diff --git a/git-wf.1 b/git-wf.1 index 6312826..5f93033 100644 --- a/git-wf.1 +++ b/git-wf.1 @@ -9,10 +9,10 @@ Options: Commands: abort Close a feature branch without it being merged [CAREFUL] - cut-release [branch] Move branch (default: master) commits onto release branch + cut-release [branch] Move branch (default: main|master) commits onto release branch done Cleanup current merged, PRed feature branch hotfix Move branch hotfix to given build tag - merge-back Merges all changes back from master ← release ← hotfix + merge-back Merges all changes back from main|master ← release ← hotfix pr [options] Open a PR to merge current feature branch qa [options] [branch] Tag given (or current) branch as a build rename Rename local and remote current feature branch diff --git a/lib/cli.js b/lib/cli.js index 7e41015..6768366 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -61,7 +61,7 @@ function wrapAction(cmd, fn) { } if (opts.parent.no) deps.forceBool = false; verifySetup(cmd, deps) - .then(() => fn({ deps, opts, args })) + .then(main => fn({ deps, opts, args, main })) .catch( /** @param {Error} err */ err => { const justMessage = !err.stack || err instanceof UIError; diff --git a/lib/commands/cut-release.js b/lib/commands/cut-release.js index b573e16..3baf0c1 100644 --- a/lib/commands/cut-release.js +++ b/lib/commands/cut-release.js @@ -39,13 +39,18 @@ const { action: mergeBackAction } = require('./merge-back'); const { ghURL, assertNoFork } = require('../common'); /** @type {import('../typedefs').ActionFn} */ -async function cutReleaseAction({ deps: { git, log }, args: [branch], opts }) { +async function cutReleaseAction({ + deps: { git, log }, + args: [branch], + opts, + main, +}) { await assertNoFork(git, 'cut-release'); - if (!branch) branch = 'master'; + if (!branch) branch = main; log('Ensuring all changes are merged back'); - await mergeBackAction({ deps: { git, log }, opts, args: [] }); + await mergeBackAction({ deps: { git, log }, opts, args: [], main }); log(`Creating PR to fast-forward merge ${branch} onto release`); const prURL = await ghURL(git, `/compare/release...${branch}`, { @@ -62,7 +67,9 @@ module.exports = { command(prog, wrapAction) { prog .command('cut-release [branch]') - .description('Move branch (default: master) commits onto release branch') + .description( + 'Move branch (default: main|master) commits onto release branch' + ) .action(wrapAction(cutReleaseAction)); }, }; diff --git a/lib/commands/merge-back.js b/lib/commands/merge-back.js index db11cfa..7dcd1fb 100644 --- a/lib/commands/merge-back.js +++ b/lib/commands/merge-back.js @@ -35,16 +35,22 @@ const { action: startAction } = require('./start'); const { UIError, cmdLine, assertNoFork } = require('../common'); +/** + * @typedef {import('../typedefs').MainBranch} MainBranch + */ + /** * @param {import('../typedefs').CmdDeps} deps * @param {string} from + * @param {MainBranch} main */ -async function createFeatureMerge({ git, log }, from) { +async function createFeatureMerge({ git, log }, from, main) { await git.reset('hard'); await startAction({ deps: { git, log }, args: [`merge-${from}`], opts: { parent: {} }, + main, }); await git.merge([from]).catch(() => {}); throw new UIError('When conflicts are resolved, commit and `wf pr`'); @@ -64,8 +70,9 @@ async function switchAndPull(git, branch) { * @param {import('../typedefs').CmdDeps} deps * @param {string} from * @param {string} to + * @param {MainBranch} main */ -async function tryMerge(deps, from, to) { +async function tryMerge(deps, from, to, main) { const { git, log } = deps; log(`${to} ← ${from}`); await switchAndPull(git, to); @@ -80,7 +87,7 @@ async function tryMerge(deps, from, to) { if (/\nCONFLICT /.test(output)) throw new Error(output); } catch (err) { log('Automated merge failed; creating feature branch for resolution'); - await createFeatureMerge(deps, from); // will throw + await createFeatureMerge(deps, from, main); // will throw } log(`Merged cleanly; committing & pushing results to ${to} branch`); @@ -92,7 +99,7 @@ async function tryMerge(deps, from, to) { } /** @type {import('../typedefs').ActionFn} */ -async function mergeBackAction({ deps }) { +async function mergeBackAction({ deps, main }) { const { git, log } = deps; await assertNoFork(git, 'merge-back'); @@ -100,14 +107,12 @@ async function mergeBackAction({ deps }) { const origBranch = (await git.branchLocal()).current; await switchAndPull(git, 'hotfix'); - await tryMerge(deps, 'hotfix', 'release'); - await tryMerge(deps, 'release', 'master'); + await tryMerge(deps, 'hotfix', 'release', main); + await tryMerge(deps, 'release', main, main); log('merge-back is clean'); - if (origBranch !== 'master') await git.checkout(origBranch); - - return true; + if (origBranch !== main) await git.checkout(origBranch); } /** @type {import('../typedefs').Action} */ @@ -116,7 +121,9 @@ module.exports = { command(prog, wrapAction) { prog .command('merge-back') - .description('Merges all changes back from master ← release ← hotfix') + .description( + 'Merges all changes back from main|master ← release ← hotfix' + ) .action(wrapAction(mergeBackAction)); }, }; diff --git a/lib/commands/qa.js b/lib/commands/qa.js index 869f470..02db712 100644 --- a/lib/commands/qa.js +++ b/lib/commands/qa.js @@ -46,7 +46,7 @@ function genBuildTag() { } /** @type {import('../typedefs').ActionFn} */ -async function qaAction({ deps: { git, log }, args: [branch], opts }) { +async function qaAction({ deps: { git, log }, args: [branch], opts, main }) { await assertNoFork(git, 'qa'); if (branch) await git.checkout(branch); @@ -54,7 +54,7 @@ async function qaAction({ deps: { git, log }, args: [branch], opts }) { if (branch === 'release' && opts.mergeBack) { log('Requiring clean merge-back for release qa'); - await mergeBackAction({ deps: { git, log }, opts, args: [] }); + await mergeBackAction({ deps: { git, log }, opts, args: [], main }); } else { log(`Pulling latest commits for '${branch}'`); // @ts-ignore diff --git a/lib/setup.js b/lib/setup.js index f30c9a5..374cc02 100644 --- a/lib/setup.js +++ b/lib/setup.js @@ -54,7 +54,7 @@ async function oldestSHA(git) { * @param {string} cmd * @param {import('./typedefs').CmdDeps} deps * @param {boolean} [doFetch] - * @return {Promise} + * @return {Promise} */ async function verifySetup(cmd, deps, doFetch = false) { const { git, log } = deps; @@ -74,12 +74,27 @@ async function verifySetup(cmd, deps, doFetch = false) { } // these commands don't require the custom branches - if (/^(done|start|rename|pr|abort)$/.test(cmd)) return; + if (/^(done|start|rename|pr|abort)$/.test(cmd)) return 'main'; - debug('checking config for master branch'); - if (!branches.branches.master || cfg['branch.master.remote'] !== 'origin') { - if (!doFetch) return void (await verifySetup(cmd, deps, true)); - throw new UIError("Missing required 'master' branch from remote 'origin'"); + /** @type {import('./typedefs').MainBranch} */ + let main; + debug('checking config for main|master branch'); + if (branches.branches.main && cfg['branch.main.remote'] === 'origin') { + debug('detected main branch "main"'); + main = 'main'; + } else if ( + branches.branches.master && + cfg['branch.master.remote'] === 'origin' + ) { + debug('detected main branch "master"'); + main = 'master'; + } else if (!doFetch) { + debug('no main branch detected; fetching'); + return await verifySetup(cmd, deps, true); + } else { + throw new UIError( + "Missing required 'main' or 'master' branch from remote 'origin'" + ); } for (const name of ['release', 'hotfix']) { @@ -87,10 +102,10 @@ async function verifySetup(cmd, deps, doFetch = false) { const remote = branches.branches[`remotes/origin/${name}`]; const local = branches.branches[name]; if (!local) { - if (!doFetch) return void (await verifySetup(cmd, deps, true)); + if (!doFetch) return await verifySetup(cmd, deps, true); log(`Creating missing local branch ${name}`); // start hotfix at the initial commit so it always moves forward - const base = name === 'hotfix' ? await oldestSHA(git) : 'origin/master'; + const base = name === 'hotfix' ? await oldestSHA(git) : `origin/${main}`; await git.checkoutBranch(name, base); await git.checkout(branches.current); } @@ -103,21 +118,23 @@ async function verifySetup(cmd, deps, doFetch = false) { const fullKey = `branch.${name}.${key}`; const cur = cfg[fullKey]; if (cur == null) { - if (!doFetch) return void (await verifySetup(cmd, deps, true)); + if (!doFetch) return await verifySetup(cmd, deps, true); log(`Setting ${fullKey} to ${val}`); await git.addConfig(fullKey, val); } else if (cur !== val) { - if (!doFetch) return void (await verifySetup(cmd, deps, true)); + if (!doFetch) return await verifySetup(cmd, deps, true); throw new UIError( `Invalid ${key} '${cur}' for local branch '${name}'` ); } } } else { - if (!doFetch) return void (await verifySetup(cmd, deps, true)); + if (!doFetch) return await verifySetup(cmd, deps, true); log(`Pushing ${name} to origin`); await git.push('origin', `${name}:${name}`, { '--set-upstream': true }); } } + + return main; } module.exports = verifySetup; diff --git a/lib/typedefs.d.ts b/lib/typedefs.d.ts index 8a253a8..6e8ea9b 100644 --- a/lib/typedefs.d.ts +++ b/lib/typedefs.d.ts @@ -13,10 +13,13 @@ export type CmdOpts = { [opt: string]: any; }; +export type MainBranch = 'main' | 'master'; + export type ActionFn = (allArgs: { deps: CmdDeps; opts: CmdOpts; args: string[]; + main: MainBranch; }) => void | Promise; export type WrapActionFn = (action: ActionFn) => (...args: any[]) => void; diff --git a/test/cli.test.js b/test/cli.test.js index 22b8fe3..e9527d7 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -5,37 +5,41 @@ const assert = require('assertive'); const addHooks = require('./test-common'); describe('cli', () => { - const t = addHooks(); - - it('runs setup', async () => { - await t.cli('setup'); - assert.include('hotfix', (await t.ghGit.branchLocal()).all); - }); - - it('shows errors', async () => { - const err = await assert.rejects(t.cli('qa', 'tofu')); - assert.include("'tofu' did not match", err.message); - }); - - it('rejects --yes --no', async () => { - const err = await assert.rejects(t.cli('setup', '--yes', '--no')); - assert.include('exclusive', err.message); - }); - - it('forces "yes" response with --yes', async () => { - await t.cli('start', 'kittens'); - await t.changeSomething(); - await t.git.commit('changed', ['README']); - await t.cli('done', '--yes'); - assert.notInclude('kittens', (await t.ghGit.branchLocal()).all); - }); - - it('forces "no" response with --no', async () => { - await t.cli('start', 'kittens'); - await t.changeSomething(); - await t.git.commit('changed', ['README']); - const err = await assert.rejects(t.cli('done', '--no')); - assert.include('unmerged', err.message); - assert.include('kittens', (await t.git.branchLocal()).all); - }); + for (const main of ['main', 'master']) { + describe(`with ${main} branch`, () => { + const t = addHooks(main); + + it('runs setup', async () => { + await t.cli('setup'); + assert.include('hotfix', (await t.ghGit.branchLocal()).all); + }); + + it('shows errors', async () => { + const err = await assert.rejects(t.cli('qa', 'tofu')); + assert.include("'tofu' did not match", err.message); + }); + + it('rejects --yes --no', async () => { + const err = await assert.rejects(t.cli('setup', '--yes', '--no')); + assert.include('exclusive', err.message); + }); + + it('forces "yes" response with --yes', async () => { + await t.cli('start', 'kittens'); + await t.changeSomething(); + await t.git.commit('changed', ['README']); + await t.cli('done', '--yes'); + assert.notInclude('kittens', (await t.ghGit.branchLocal()).all); + }); + + it('forces "no" response with --no', async () => { + await t.cli('start', 'kittens'); + await t.changeSomething(); + await t.git.commit('changed', ['README']); + const err = await assert.rejects(t.cli('done', '--no')); + assert.include('unmerged', err.message); + assert.include('kittens', (await t.git.branchLocal()).all); + }); + }); + } }); diff --git a/test/cut-release.test.js b/test/cut-release.test.js index aa9f70e..736c23a 100644 --- a/test/cut-release.test.js +++ b/test/cut-release.test.js @@ -8,23 +8,33 @@ const verifySetup = require('../lib/setup'); const { action: cutReleaseAction } = require('../lib/commands/cut-release'); describe('cut-release', () => { - const t = addHooks(); + for (const main of ['main', 'master']) { + describe(`with ${main} branch`, () => { + const t = addHooks(main); - it('opens a master → release PR url', async () => { - await verifySetup('cut-release', t); // need branches to merge back - await cutReleaseAction({ - deps: t, - args: [], - opts: { parent: { open: false } }, - }); - assert.include('/compare/release...master?', t.logged); - }); + it(`opens a ${main} → release PR url`, async () => { + await verifySetup('cut-release', t); // need branches to merge back + await cutReleaseAction({ + deps: t, + args: [], + opts: { parent: { open: false } }, + main, + }); + assert.include(`/compare/release...${main}?`, t.logged); + }); - it('refuses to run on a checkout with a fork', async () => { - t.git = t.gitFork; - const err = await assert.rejects( - cutReleaseAction({ deps: t, args: [], opts: { parent: { open: false } } }) - ); - assert.include('with a fork remote', err.message); - }); + it('refuses to run on a checkout with a fork', async () => { + t.git = t.gitFork; + const err = await assert.rejects( + cutReleaseAction({ + deps: t, + args: [], + opts: { parent: { open: false } }, + main, + }) + ); + assert.include('with a fork remote', err.message); + }); + }); + } }); diff --git a/test/merge-back.test.js b/test/merge-back.test.js index 8510531..bdff560 100644 --- a/test/merge-back.test.js +++ b/test/merge-back.test.js @@ -6,16 +6,16 @@ const addHooks = require('./test-common'); const verifySetup = require('../lib/setup'); const { action: mergeBackAction } = require('../lib/commands/merge-back'); -async function assertMergedBack(git) { +async function assertMergedBack(git, main) { assert.deepEqual( 'no unmerged release ← hotfix commits', [], (await git.log(['release..hotfix'])).all ); assert.deepEqual( - 'no unmerged master ← release commits', + `no unmerged ${main} ← release commits`, [], - (await git.log(['master..release'])).all + (await git.log([`${main}..release`])).all ); } @@ -25,48 +25,52 @@ async function getReleaseCommit(git) { } describe('merge-back', () => { - const t = addHooks(); + for (const main of ['main', 'master']) { + describe(`with ${main} branch`, () => { + const t = addHooks(main); - it('merges changes back automatically', async () => { - await verifySetup('merge-back', t); // make our release & hotfix branches + it('merges changes back automatically', async () => { + await verifySetup('merge-back', t); // make our release & hotfix branches - // put an unmerged commit on release and a non-conflicting one on master - await t.git.checkout('release'); - await t.changeSomething(); - await t.git.commit('unmerged release change', ['README']); - await t.git.push(); - const releaseCommit = await getReleaseCommit(t.git); + // put an unmerged commit on release and a non-conflicting one on main + await t.git.checkout('release'); + await t.changeSomething(); + await t.git.commit('unmerged release change', ['README']); + await t.git.push(); + const releaseCommit = await getReleaseCommit(t.git); - await t.git.checkout('master'); - await t.changeSomething('another-file'); - await t.git.add(['.']); - await t.git.commit('umerged master change'); - await t.git.push(); + await t.git.checkout(main); + await t.changeSomething('another-file'); + await t.git.add(['.']); + await t.git.commit(`unmerged ${main} change`); + await t.git.push(); - await mergeBackAction({ deps: t }); + await mergeBackAction({ deps: t, main }); - // make sure everything got merged back - await assertMergedBack(t.ghGit); + // make sure everything got merged back + await assertMergedBack(t.ghGit, main); - // and that we didn't need to change release - assert.equal(releaseCommit, await getReleaseCommit(t.git)); - }); + // and that we didn't need to change release + assert.equal(releaseCommit, await getReleaseCommit(t.git)); + }); - it('tries to merges back and starts a feature branch on conflict', async () => { - await verifySetup('merge-back', t); + it('tries to merges back and starts a feature branch on conflict', async () => { + await verifySetup('merge-back', t); - for (const branch of ['hotfix', 'release']) { - await t.git.checkout(branch); - await t.changeSomething(); - await t.git.commit(`conflicting ${branch} change`, ['README']); - await t.git.push(); - } + for (const branch of ['hotfix', 'release']) { + await t.git.checkout(branch); + await t.changeSomething(); + await t.git.commit(`conflicting ${branch} change`, ['README']); + await t.git.push(); + } - const err = await assert.rejects(mergeBackAction({ deps: t })); - assert.include('When conflicts are resolved', err.message); + const err = await assert.rejects(mergeBackAction({ deps: t, main })); + assert.include('When conflicts are resolved', err.message); - const branches = await t.ghGit.branchLocal(); - assert.include('jdoe/feature/release/merge-hotfix', branches.all); - assert.notInclude('jdoe/feature/master/merge-release', branches.all); - }); + const branches = await t.ghGit.branchLocal(); + assert.include('jdoe/feature/release/merge-hotfix', branches.all); + assert.notInclude(`jdoe/feature/${main}/merge-release`, branches.all); + }); + }); + } }); diff --git a/test/qa.test.js b/test/qa.test.js index 71e6486..6bc03dc 100644 --- a/test/qa.test.js +++ b/test/qa.test.js @@ -7,47 +7,56 @@ const verifySetup = require('../lib/setup'); const { action: qaAction } = require('../lib/commands/qa'); describe('qa', () => { - const t = addHooks(); - - it('tags an implicit hotfix build', async () => { - await verifySetup('qa', t); // make our release & hotfix branches - - // put an extra commit onto hotfix branch - await t.git.checkout('hotfix'); - await t.changeSomething(); - await t.git.commit('hotfix change', ['README']); - - await qaAction({ deps: t, opts: { mergeBack: true }, args: [] }); - - assert.expect( - (await t.ghGit.tags()).all.some(tag => /^build-\d/.test(tag)) - ); - }); - - it('tags an explicit release build, merging back', async () => { - await verifySetup('qa', t); - - await t.git.checkout('release'); - await t.changeSomething(); - await t.git.commit('release change', ['README']); - await t.git.checkout('master'); - - assert.equal(1, (await t.git.log(['master..release'])).total); - - await qaAction({ deps: t, args: ['release'], opts: { mergeBack: true } }); - - assert.equal( - 'no unmerged commits locally', - 0, - (await t.git.log(['master..release'])).total - ); - assert.equal( - 'no unmerged commits on remote', - 0, - (await t.ghGit.log(['master..release'])).total - ); - assert.expect( - (await t.ghGit.tags()).all.some(tag => /^build-\d/.test(tag)) - ); - }); + for (const main of ['main', 'master']) { + describe(`with ${main} branch`, () => { + const t = addHooks(main); + + it('tags an implicit hotfix build', async () => { + await verifySetup('qa', t); // make our release & hotfix branches + + // put an extra commit onto hotfix branch + await t.git.checkout('hotfix'); + await t.changeSomething(); + await t.git.commit('hotfix change', ['README']); + + await qaAction({ deps: t, opts: { mergeBack: true }, args: [] }); + + assert.expect( + (await t.ghGit.tags()).all.some(tag => /^build-\d/.test(tag)) + ); + }); + + it('tags an explicit release build, merging back', async () => { + await verifySetup('qa', t); + + await t.git.checkout('release'); + await t.changeSomething(); + await t.git.commit('release change', ['README']); + await t.git.checkout(main); + + assert.equal(1, (await t.git.log([`${main}..release`])).total); + + await qaAction({ + deps: t, + args: ['release'], + opts: { mergeBack: true }, + main, + }); + + assert.equal( + 'no unmerged commits locally', + 0, + (await t.git.log([`${main}..release`])).total + ); + assert.equal( + 'no unmerged commits on remote', + 0, + (await t.ghGit.log([`${main}..release`])).total + ); + assert.expect( + (await t.ghGit.tags()).all.some(tag => /^build-\d/.test(tag)) + ); + }); + }); + } }); diff --git a/test/setup.test.js b/test/setup.test.js index 1323c5d..1cc60db 100644 --- a/test/setup.test.js +++ b/test/setup.test.js @@ -6,17 +6,21 @@ const addHooks = require('./test-common'); const verifySetup = require('../lib/setup'); describe('verifySetup', () => { - const t = addHooks(); + for (const main of ['main', 'master']) { + describe(`with ${main} branch`, () => { + const t = addHooks(main); - it('creates and pushes missing release & hotfix branches', async () => { - await verifySetup('setup', t); - const repoBranches = await Promise.all( - ['git', 'ghGit'].map(git => t[git].branchLocal()) - ); - for (const name of ['release', 'hotfix']) { - for (const repo of repoBranches) { - assert.include(name, repo.all); - } - } - }); + it('creates and pushes missing release & hotfix branches', async () => { + await verifySetup('setup', t); + const repoBranches = await Promise.all( + ['git', 'ghGit'].map(git => t[git].branchLocal()) + ); + for (const name of ['release', 'hotfix']) { + for (const repo of repoBranches) { + assert.include(name, repo.all); + } + } + }); + }); + } }); diff --git a/test/test-common.js b/test/test-common.js index cb0f0b9..dc6bd33 100644 --- a/test/test-common.js +++ b/test/test-common.js @@ -11,6 +11,10 @@ const mktemp = require('mktemp'); const writeFileAsync = promisify(require('fs').writeFile); const rimrafAsync = promisify(require('rimraf')); +/** + * @typedef {import('../lib/typedefs').MainBranch} MainBranch + */ + const tmpDir = process.env.TMPDIR || '/tmp'; const FAKE_USER = 'jdoe'; @@ -32,7 +36,7 @@ async function setupGitHubDir() { return [dir, git]; } -async function setupLocalDir(ghDir) { +async function setupLocalDir(ghDir, ghGit, main) { const dir = await mktemp.createDir( path.join(tmpDir, 'feature-test-local-XXXXXXX') ); @@ -42,6 +46,13 @@ async function setupLocalDir(ghDir) { await git.add(['.']); await git.commit('init'); await git.push(); + + if (main !== 'master') { + await ghGit.branch(['-m', 'master', main]); + await git.branch(['-m', 'master', main]); + await git.push(['-u', 'origin', `${main}:${main}`]); + } + return [dir, git]; } @@ -85,7 +96,11 @@ function wfCLI(dir, args) { }); } -function addHooks() { +/** + * TODO: have this default to main, once there's time to rewrite all the tests + * @param {MainBranch} [main] + */ +function addHooks(main = 'master') { const t = { changeSomething(file) { return changeSomething(t.localDir, file); @@ -112,7 +127,7 @@ function addHooks() { savedUser = process.env.USER; process.env.USER = FAKE_USER; [t.ghDir, t.ghGit] = await setupGitHubDir(); - [t.localDir, t.git] = await setupLocalDir(t.ghDir); + [t.localDir, t.git] = await setupLocalDir(t.ghDir, t.ghGit, main); [t.localDir2, t.git2] = await setupLocalDir2(t.ghDir); [t.forkDir, t.gitFork] = await setupForkDir(t.ghDir); t.logged = '';