Skip to content

Commit

Permalink
[#59, #70] Make PR and branch name configurable (#83)
Browse files Browse the repository at this point in the history
* [#59, #70] Make PR and branch name configurable

Use configurable template strings for generating branch names, commit
messages and shell commands.

The configured template strings are compiled into formatter functions.
Since browsers do not allow `eval`, `Function` etc. in web extensions
(Content-Security Policy), we compile custom formatter functions that
work without `eval`-ing and do not allow arbitrary JavaScript.

While working on this feature, we implemented an initial spike using
`lodash.template` (which uses `Function`). We decided not to pursue this
approach, as it required sandboxing the templates — something that is
not easily implemented in a way that works for different browsers.

The formatted branch name, commit message and command is now attached to
each ticket on load now instead of generating them on-the-fly.

Notable changes:

- Add custom template implementation and helper functions for formatting
  values used inside of templates (e.g. lowercase, dasherize, …)
- Format tickets on load (attach output to tickets as "fmt" property)
- Update webpack configurations for WebExtension API browsers and Safari
  - Generate an options.html and options.js for WebExtension-compatible
    browsers to allow the user to customize the templates
  - Add `Settings.plist` to implement template preferences in Safari
- Create custom options.scss

Additional changes:

- Prevent chrome from checking whether it is the default browser when
  launching via chrome-launcher
- Update "ExtractTextPlugin" filename template to automatically name
  extracted CSS after the entry: `[name].css`

References:

Safari Extensions Development Guide, "Settings and Local Storage":
https://web.archive.org/web/20171215000607/https://developer.apple.com/library/content/documentation/Tools/Conceptual/SafariExtensionGuide/ExtensionSettings/ExtensionSettings.html

Google Chrome: Using eval in Chrome Extensions. Safely.:
https://web.archive.org/web/20171215234603/https://developer.chrome.com/extensions/sandboxingEval

Google Chrome: Options:
https://web.archive.org/web/20171216121413/https://developer.chrome.com/extensions/optionsV2

MDN web docs: Add-ons: Implement a settings page
https://web.archive.org/web/20171216121512/https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Implement_a_settings_page

* Simplify formatter creation

* Extract and unit-test enhancer function

* Update documentation

* Bump version number for the next release
  • Loading branch information
pmeinhardt authored and andreasknoepfle committed Feb 16, 2018
1 parent 4101b64 commit a612e07
Show file tree
Hide file tree
Showing 42 changed files with 980 additions and 256 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"rules": {
"jsx-a11y/anchor-is-valid": ["error", {
"specialLink": ["to"]
}],
"jsx-a11y/label-has-for": ["error", {
"required": { "some": ["nesting", "id"] }
}]
}
}
62 changes: 47 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# Tickety-Tick [![Build Status](https://travis-ci.org/bitcrowd/tickety-tick.svg?branch=master)](https://travis-ci.org/bitcrowd/tickety-tick)

*How do you name this branch? What is the message for that commit?*
> #### How do you name this branch? What is the message for that commit?
> A browser extension to generate these for you,
> based on the ticket you're working on.
![Tickety-Tick's user interface](./screenshots/interface.png)

At bitcrowd we love conventions. One of them is how we name branches and
commits. This makes it easy to relate a particular branch or commit to a
certain ticket.

![screenshot](./screenshot.png)

**Branches** always follow the format `type/id-title`, where:
**Branches** follow the format `type/id-title` by default, where:

- `type` is usually one of:
- `feature` (default)
Expand All @@ -18,10 +20,15 @@ certain ticket.
- `id` is the identifier of the ticket in your ticketing system
- `title` is a lowercase, dasherized version of the ticket title

**Commits** always contain `[#id] title`.
**Commits** contain `[#id] title` by default.

Additionally, Tickety-Tick generates [**commands**](#generated-commands) to
set up a branch with the proper name and to prepare the commit message for your
source code management tool. Out of the box, it is set up to work with Git.

Additionally, Tickety-Tick generates [**git commands**](#generated-commands) to
set up a branch with the proper name and to prepare the commit message.
If you need your commit messages, branch names or commands to look different,
you can [configure](#advanced-configuration) Tickety-Tick to use a custom
format.

## Supported ticket systems

Expand All @@ -42,7 +49,7 @@ Tickety-Tick is available for every major browser:
- [Chrome/Chromium](https://chrome.google.com/webstore/detail/ciakolhgmfijpjbpcofoalfjiladihbg)
- [Firefox](https://addons.mozilla.org/firefox/addon/tickety-tick/)
- [Opera](https://addons.opera.com/extensions/details/tickety-tick/)
- For Safari, you need to build it yourself (see below)
- For Safari, you need to build it yourself ([see below](#safari))

## Keyboard Shortcuts

Expand Down Expand Up @@ -118,12 +125,20 @@ To test-drive a development version, you can use:

```shell
yarn open:chrome
yarn open:firefox
yarn open:firefox-local
```

You can run both `watch:[browser]` and `open:[browser]` scripts in parallel to
automatically rebuild and reload the extension as you make changes.

### Running automated checks

To execute the automated source code checks, run:

```shell
yarn checks
```

### Generating coverage reports

In order to generate code coverage reports locally, just run:
Expand All @@ -144,16 +159,16 @@ open coverage/lcov-report/index.html
### Generated commands

As mentioned earlier, in addition to branch names and commit messages,
Tickety-Tick generates git commands to set up a branch with the proper name
Tickety-Tick generates commands to set up a branch with the proper name
and to prepare the commit message.

The code generated for copying will look like this:
By default, the code generated for copying will look like this:

```shell
git checkout -b BRANCH-NAME && git commit --allow-empty -m COMMIT-MESSAGE
```

The generated commands make a few assumptions:
These default generated commands make a few assumptions:

1. *You're using git (obviously).* The branch names and commit messages
Tickety-Tick generates may work with other version control systems,
Expand All @@ -165,6 +180,23 @@ The generated commands make a few assumptions:
message title generated by Tickety-Tick when setting up the branch.

This approach works nicely with our git workflow, for which the above
assumptions are true. Yours may be different though, in which case you may
still like Tickety-Tick's ability to generate the branch names and commit
messages for you.
assumptions are true. Yours may be different though, in which case you might
want to [configure](#advanced-configuration) Tickety-Tick differently.

### Advanced configuration

If you have different conventions regarding commit messages, branch names or
you're just using a different source code management tool, Tickety-Tick allows
you to customize the output format for all of these.

In Firefox, open `about:addons` and select the Tickety-Tick preferences.

![Firefox preferences](./screenshots/firefox-preferences.png)

In Chrome, open `chrome://extensions/` and select "Options".

![Chrome preferences](./screenshots/chrome-preferences.png)

In Safari, open "Preferences" (`cmd + ,`), then select "Extensions".

![Safari preferences](./screenshots/safari-preferences.png)
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"private": true,
"version": "1.3.0",
"version": "2.0.0",
"name": "tickety-tick",
"description": "A browser extension that helps you to create commit messages and branch names from story trackers.",
"author": "Bodo Tasche <[email protected]>",
Expand All @@ -18,13 +18,13 @@
"watch:firefox-local": "npm run build:firefox-local -- --watch",
"watch:safari": "npm run build:safari -- --watch",
"open:chrome": "./script/open-in-chrome ./dist/chrome https://github.com/bitcrowd/tickety-tick",
"open:firefox": "web-ext run --source-dir ./dist/firefox --pref startup.homepage_welcome_url=https://github.com/bitcrowd/tickety-tick",
"open:firefox-local": "web-ext run --source-dir ./dist/firefox-local --pref startup.homepage_welcome_url=https://github.com/bitcrowd/tickety-tick",
"bundle:chrome": "cross-env BUNDLE=true npm run build:chrome",
"bundle:firefox": "cross-env BUNDLE=true npm run build:firefox",
"lint": "eslint '**/*.{js,jsx}'",
"test": "cross-env NODE_ENV=test jasmine",
"test:coverage": "nyc npm test",
"test:watch": "cross-env NODE_ENV=test nodemon --exec jasmine",
"test:watch": "cross-env NODE_ENV=test nodemon --ext js,jsx,json --exec jasmine",
"checks": "npm run lint && npm run test"
},
"devDependencies": {
Expand Down
Binary file added screenshots/chrome-preferences.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/firefox-preferences.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added screenshots/safari-preferences.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion script/open-in-chrome
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const dir = process.argv[2] || path.join(__dirname, '..', 'dist', 'chrome');
const url = process.argv[3] || 'https://github.com/bitcrowd/tickety-tick';

const options = {
chromeFlags: [`--load-extension=${dir}`],
chromeFlags: ['--no-default-browser-check', `--load-extension=${dir}`],
enableExtensions: true,
startingUrl: url,
};
Expand Down
26 changes: 26 additions & 0 deletions spec/enhance-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import enhance from '../src/common/enhance';
import format, { defaults } from '../src/common/format';

describe('ticket enhancer', () => {
const ticket = {
id: 'BTC-042',
title: 'Add more tests for src/common/format.js',
type: 'enhancement',
};

const templates = defaults;

it('attaches format output to tickets as "fmt" property', () => {
const formatter = format(templates);
const enhancer = enhance(templates);

expect(enhancer(ticket)).toEqual({
fmt: {
branch: formatter.branch(ticket),
commit: formatter.commit(ticket),
command: formatter.command(ticket),
},
...ticket,
});
});
});
138 changes: 138 additions & 0 deletions spec/format-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import format, { helpers } from '../src/common/format';

describe('ticket formatting', () => {
const ticket = {
id: 'BTC-042',
title: 'Add more tests for src/common/format.js',
type: 'enhancement',
};

describe('default format', () => {
const fmt = format({});

describe('commit', () => {
it('includes ticket id and title', () => {
const formatted = fmt.commit(ticket);
expect(formatted).toBe(`[#${ticket.id}] ${ticket.title}`);
});
});

describe('branch', () => {
const { slugify } = helpers;

it('includes ticket type, id and title', () => {
const formatted = fmt.branch(ticket);
expect(formatted).toBe(`${ticket.type}/${slugify(ticket.id)}-${slugify(ticket.title)}`);
});

it('formats type to "feature" if not provided', () => {
const typeless = { id: ticket.id, title: ticket.title };
const formatted = fmt.branch(typeless);
expect(formatted).toBe(`feature/${slugify(ticket.id)}-${slugify(ticket.title)}`);
});
});

describe('command', () => {
const { shellquote } = helpers;

it('includes the quoted branch name and commit message', () => {
const branch = fmt.branch(ticket);
const commit = fmt.commit(ticket);

const formatted = fmt.command(ticket);

expect(formatted).toBe(`git checkout -b ${shellquote(branch)}`
+ ` && git commit --allow-empty -m ${shellquote(commit)}`);
});
});
});

describe('helpers', () => {
describe('lowercase', () => {
const { lowercase } = helpers;

it('lowercases strings', () => {
expect(lowercase('QUIET')).toBe('quiet');
});
});

describe('shellquote', () => {
const { shellquote } = helpers;

it('wraps the input in single-quotes', () => {
expect(shellquote('echo "pwned"')).toBe('\'echo "pwned"\'');
});

it('escapes any single-quotes in the input', () => {
const input = 'you\'; echo aren\'t "pwned"';
const quoted = '\'you\'\\\'\'; echo aren\'\\\'\'t "pwned"\'';
expect(shellquote(input)).toBe(quoted);
});
});

describe('slugify', () => {
const { slugify } = helpers;

it('formats normal strings', () => {
const formatted = slugify('hello');
expect(formatted).toBe('hello');
});

it('lowercases strings', () => {
const formatted = slugify('Bitcrowd');
expect(formatted).toBe('bitcrowd');
});

it('formats spaces to dashes', () => {
const formatted = slugify('hello bitcrowd');
expect(formatted).toBe('hello-bitcrowd');
});

it('formats special characters', () => {
const formatted = slugify('Señor Dévèloper');
expect(formatted).toBe('senor-developer');
});

it('formats umlauts', () => {
const formatted = slugify('äöüß');
expect(formatted).toBe('aeoeuess');
});

it('strips brackets', () => {
const formatted = slugify('[#23] Add (more)');
expect(formatted).toBe('23-add-more');
});

it('formats slashes to dashes', () => {
const formatted = slugify('src/js/format');
expect(formatted).toBe('src-js-format');
});

it('formats dots to dashes', () => {
const formatted = slugify('format.js');
expect(formatted).toBe('format-js');
});

it('strips hashes', () => {
const formatted = slugify('##23 #hashtag');
expect(formatted).toBe('23-hashtag');
});
});

describe('trim', () => {
const { trim } = helpers;

it('removes leading and trailing whitespace', () => {
expect(trim('\t black\t\t ')).toBe('black');
});
});

describe('uppercase', () => {
const { uppercase } = helpers;

it('uppercases strings', () => {
expect(uppercase('loud')).toBe('LOUD');
});
});
});
});
Loading

0 comments on commit a612e07

Please sign in to comment.