Skip to content

Commit

Permalink
feat: support "main" as default branch
Browse files Browse the repository at this point in the history
non-breaking as it still falls back on "master"
  • Loading branch information
David Bushong committed Oct 15, 2020
1 parent c7c5ca6 commit 2b8d9f2
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 245 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
node_modules/
npm-debug.log
/tmp
/.vscode
146 changes: 76 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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] <name>` - starts a new feature branch
Expand All @@ -42,18 +48,18 @@ Given you are currently on branch `<parent>`

1. Updates the branch you currently have checked out with `git pull`
1. Creates a new feature branch named `<name>` locally with
`git checkout -b <name>`
`git checkout -b <name>`
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 `<yourusername>/<reponame>` 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/<parent>/<name>` with
`git push -u fork <name>:feature/<parent><name>`
1. verifies you have a remote named `fork`
1. if you don't, verifies that `<yourusername>/<reponame>` 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/<parent>/<name>` with
`git push -u fork <name>:feature/<parent><name>`
1. If you didn't, pushes your feature branch to `origin` as a branch named
`<yourusername>/feature/<parent>/<name>` with
`git push -u origin <name>:<yourusername>/feature/<parent>/<name>`
`<yourusername>/feature/<parent>/<name>` with
`git push -u origin <name>:<yourusername>/feature/<parent>/<name>`

### `git wf rename <newname>` - renames a feature branch

Expand All @@ -62,17 +68,17 @@ 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

### `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
Expand All @@ -95,11 +101,11 @@ Given you are currently on a feature branch named `<name>`
1. Deletes the feature branch with `git branch -d <name>`
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_

Expand All @@ -108,7 +114,7 @@ Given you are currently on a feature branch named `<name>`
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 <build-tag>` - Moves the hotfix branch to given tag
Expand All @@ -118,58 +124,58 @@ Given you are currently on a feature branch named `<name>`
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
Expand Down
4 changes: 2 additions & 2 deletions git-wf.1
Original file line number Diff line number Diff line change
Expand Up @@ -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 <buildTag> 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
Expand Down
2 changes: 1 addition & 1 deletion lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 11 additions & 4 deletions lib/commands/cut-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`, {
Expand All @@ -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));
},
};
27 changes: 17 additions & 10 deletions lib/commands/merge-back.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`');
Expand All @@ -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);
Expand All @@ -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`);
Expand All @@ -92,22 +99,20 @@ 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');

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} */
Expand All @@ -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));
},
};
4 changes: 2 additions & 2 deletions lib/commands/qa.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ 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);
else branch = (await git.branchLocal()).current;

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
Expand Down
Loading

0 comments on commit 2b8d9f2

Please sign in to comment.