├── .editorconfig
├── .eslintrc.js
├── .gitattributes
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── src
├── _locales
│ └── en
│ │ └── messages.json
├── images
│ ├── icon-128.png
│ ├── icon-16.png
│ ├── icon-19.png
│ └── icon-38.png
├── manifest.json
├── manifest.v2.json
├── pages
│ └── options.html
├── scripts
│ ├── background.js
│ ├── content.js
│ ├── options.js
│ └── popup.js
└── styles
│ └── main.css
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.json]
15 | indent_size = 2
16 |
17 | # We recommend you to keep these unchanged
18 | end_of_line = lf
19 | charset = utf-8
20 | trim_trailing_whitespace = true
21 | insert_final_newline = true
22 |
23 | [*.md]
24 | trim_trailing_whitespace = false
25 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'standard',
3 | globals: {
4 | chrome: false
5 | },
6 | rules: {
7 | 'semi': [2, 'always'],
8 | 'no-mixed-operators': 'off',
9 | 'no-unused-expressions': 'off',
10 | 'no-var': 'error',
11 | 'one-var': [2, 'never'],
12 | 'prefer-const': 'error',
13 | 'operator-linebreak': [
14 | 'error',
15 | 'after',
16 | { overrides: { '?': 'before', ':': 'before', '|>': 'before' } }
17 | ],
18 | radix: 'error',
19 | 'require-await': 'error'
20 | },
21 | env: {
22 | browser: true
23 | },
24 | overrides: [
25 | {
26 | files: ['src/scripts/content.js'],
27 | globals: {
28 | marked: false
29 | }
30 | }
31 | ]
32 | };
33 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | temp
3 | .tmp
4 | dist
5 | .sass-cache
6 | app/bower_components
7 | test/bower_components
8 | package
9 | dist.zip
10 | .idea
11 | .DS_Store
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # chrome-pullrequest-templates
2 | Chrome extension to pre-populate pull requests with a template on GitHub and Bitbucket.
3 | Templates are loaded from a URL.
4 |
5 | ## Installation
6 | Get it from the chrome web store: [Link to extension](https://chrome.google.com/webstore/detail/git-pull-request-template/dlflgkjacacpmhdpiggkdiaieddfmkia?hl=en-US)
7 |
8 | ## Options
9 | Access the options page from your Chrome extensions page.
10 | There are checkboxes for GitHub/Bitbucket, and each can have its own template.
11 |
12 | ## License
13 | MIT
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "git-pull-request-template",
3 | "version": "0.4.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/tcrammond/chrome-pullrequest-templates.git"
7 | },
8 | "bugs": {
9 | "url": "https://github.com/tcrammond/chrome-pullrequest-templates/issues"
10 | },
11 | "scripts": {
12 | "start": "NODE_ENV=development webpack --watch",
13 | "build": "NODE_ENV=production webpack"
14 | },
15 | "dependencies": {
16 | "bootstrap": "^3.3.5",
17 | "dompurify": "^2.3.9",
18 | "marked": "4.0.18"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "^7.18.6",
22 | "babel-loader": "^8.2.5",
23 | "clean-webpack-plugin": "^4.0.0",
24 | "copy-webpack-plugin": "^11.0.0",
25 | "filemanager-webpack-plugin": "^7.0.0",
26 | "webextension-polyfill": "^0.9.0",
27 | "webpack": "^5.73.0",
28 | "webpack-cli": "^4.10.0"
29 | },
30 | "engines": {
31 | "node": ">=12.0.0"
32 | },
33 | "license": "MIT"
34 | }
35 |
--------------------------------------------------------------------------------
/src/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": {
3 | "message": "Git Pull Request Templates",
4 | "description": "The name of the application"
5 | },
6 | "appDescription": {
7 | "message": "Pre-populate pull requests with a template on Github and Bitbucket.",
8 | "description": "The description of the application"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/images/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tcrammond/chrome-pullrequest-templates/06712a56390494e26a1bfd0cd0d26aa4a4c64ddc/src/images/icon-128.png
--------------------------------------------------------------------------------
/src/images/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tcrammond/chrome-pullrequest-templates/06712a56390494e26a1bfd0cd0d26aa4a4c64ddc/src/images/icon-16.png
--------------------------------------------------------------------------------
/src/images/icon-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tcrammond/chrome-pullrequest-templates/06712a56390494e26a1bfd0cd0d26aa4a4c64ddc/src/images/icon-19.png
--------------------------------------------------------------------------------
/src/images/icon-38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tcrammond/chrome-pullrequest-templates/06712a56390494e26a1bfd0cd0d26aa4a4c64ddc/src/images/icon-38.png
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__MSG_appName__",
3 | "version": "0.5.0",
4 | "manifest_version": 3,
5 | "description": "__MSG_appDescription__",
6 | "default_locale": "en",
7 | "applications": {
8 | "gecko": {
9 | "id": "{210cf697-fb1e-445c-b0c5-06b8de83bd1d}",
10 | "strict_min_version": "50.0"
11 | }
12 | },
13 | "icons": {
14 | "16": "images/icon-16.png",
15 | "128": "images/icon-128.png"
16 | },
17 | "background": {
18 | "service_worker": "scripts/background.js"
19 | },
20 | "options_ui": {
21 | "page": "pages/options.html"
22 | },
23 | "content_scripts": [
24 | {
25 | "matches": [
26 | "*://github.com/*/compare/*",
27 | "*://bitbucket.org/*/pull-request/new*",
28 | "*://bitbucket.org/*/pull-requests/new*",
29 | "*://*/pull-request/*"
30 | ],
31 | "js": [
32 | "scripts/content.js"
33 | ],
34 | "run_at": "document_end",
35 | "all_frames": false
36 | }
37 | ],
38 | "permissions": [
39 | "storage"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/src/manifest.v2.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__MSG_appName__",
3 | "version": "0.5.0",
4 | "manifest_version": 2,
5 | "description": "__MSG_appDescription__",
6 | "default_locale": "en",
7 | "applications": {
8 | "gecko": {
9 | "id": "{210cf697-fb1e-445c-b0c5-06b8de83bd1d}",
10 | "strict_min_version": "50.0"
11 | }
12 | },
13 | "icons": {
14 | "16": "images/icon-16.png",
15 | "128": "images/icon-128.png"
16 | },
17 | "background": {
18 | "scripts": [
19 | "scripts/background.js"
20 | ]
21 | },
22 | "options_ui": {
23 | "page": "pages/options.html"
24 | },
25 | "content_scripts": [
26 | {
27 | "matches": [
28 | "*://github.com/*/compare/*",
29 | "*://bitbucket.org/*/pull-request/new*",
30 | "*://bitbucket.org/*/pull-requests/new*",
31 | "*://*/pull-request/*"
32 | ],
33 | "js": [
34 | "scripts/content.js"
35 | ],
36 | "run_at": "document_end",
37 | "all_frames": false
38 | }
39 | ],
40 | "permissions": [
41 | "storage"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/src/pages/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
17 |
18 |
19 |
20 |
21 |
Git Pull Request Templates
22 |
23 |
You can configure the PR template used for each service.
24 |
You can either:
25 |
26 |
27 | - Supply a link to load a raw markdown template from (not supported for GitHub, it doesn't allow external requests)
28 | - Enter the PR template manually
29 |
30 |
31 |
If you enter the template manually the URL will be ignored.
32 |
33 |
34 |
37 |
38 |
39 |
40 |
GitHub
41 |
Hey! These days GitHub has its own support for templates. This extension still works though.
42 |
43 |
46 |
47 |
48 |
49 |
50 |
51 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
Bitbucket
64 |
65 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 |
79 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
Custom Repository
92 |
93 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | Settings saved.
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/src/scripts/background.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const browser = require('webextension-polyfill');
3 | browser.browserAction.setBadgeText({ text: 'PR' });
4 |
--------------------------------------------------------------------------------
/src/scripts/content.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const DOMPurify = require('dompurify');
3 | const browser = require('webextension-polyfill');
4 | const marked = require('marked');
5 |
6 | (function () {
7 | const defaultUrl = 'https://raw.github.com/sprintly/sprint.ly-culture/master/pr-template.md';
8 | let options;
9 | let isCustom;
10 |
11 | const isGH = window.location.href.match(/github.com/);
12 | const isBB = window.location.href.match(/bitbucket.org/);
13 |
14 | loadOptions(getTemplate);
15 |
16 | function loadOptions (cb) {
17 | browser.storage.sync
18 | .get({
19 | githubEnabled: true,
20 | githubTemplateUrl: defaultUrl,
21 | githubTemplateContent: '',
22 | bitbucketEnabled: true,
23 | bitbucketTemplateUrl: defaultUrl,
24 | bitbucketTemplateContent: '',
25 | bitbucketOverwrite: true,
26 |
27 | customEnabled: true,
28 | customTemplateUrl: defaultUrl,
29 | customRepoRegex: '',
30 | customRepoDescriptionID: ''
31 | }).then((items) => {
32 | options = items;
33 | cb();
34 | }).catch((e) => {
35 | console.error('Could not fecth options.', e);
36 | });
37 | }
38 |
39 | function insertTemplate (template) {
40 | let el = null;
41 | let isBitbucketProseEditor = false;
42 |
43 | if (isGH && options.githubEnabled) {
44 | el = document.getElementById('pull_request_body');
45 | } else if (isBB && options.bitbucketEnabled) {
46 | // If this looks like an "Edit PR" page, do not insert the template.
47 | if (window.location.href.indexOf('/update') !== -1) return;
48 |
49 | // Check for new beta editor, falling back to default
50 | const test = document.getElementById('ak_editor_description');
51 | isBitbucketProseEditor = !!test;
52 |
53 | // Deal with bitbucket immediately since it's getting a bit bespoke now.
54 | // One day, we'll re-write this whole thing..
55 | if (isBitbucketProseEditor) {
56 | setTimeout(function () {
57 | el = document.getElementsByClassName('ProseMirror')[0];
58 | insertContenteditable(el, template, options.bitbucketOverwrite);
59 | }, 2500);
60 | return;
61 | } else {
62 | el = document.getElementById('id_description');
63 | insertInput(el, template, options.bitbucketOverwrite);
64 | }
65 | } else if (isCustom && options.customEnabled && options.customRepoDescriptionID) {
66 | el = document.getElementById(options.customRepoDescriptionID.toString());
67 | }
68 |
69 | if (el === null) return;
70 |
71 | if (isGH) { return insertInput(el, template, true); }
72 |
73 | // If this looks like an "Edit PR" page, do not insert the template.
74 | if (window.location.href.indexOf('/update') !== -1) return;
75 |
76 | if (isCustom) { insertInput(el, template, options.bitbucketOverwrite); }
77 | }
78 |
79 | // Old textarea editor
80 | function insertInput (el, template, overwrite) {
81 | if (overwrite) {
82 | el.value = template;
83 | } else {
84 | setTimeout(function () {
85 | el.value = el.value + ((el.value && el.value.length ? '\r\n' : '') + template);
86 | }, 1000);
87 | }
88 | }
89 |
90 | // New contenteditable editor
91 | function insertContenteditable (el, template, overwrite) {
92 | setTimeout(function () {
93 | if (overwrite) {
94 | el.innerHTML = DOMPurify.sanitize(marked.parse(template));
95 | } else {
96 | const hasContent = el.innerHTML && el.innerHTML.length;
97 | el.innerHTML = `${el.innerHTML}${hasContent ? '
' : ''}${DOMPurify.sanitize(marked.parse(template))}`;
98 | }
99 | }, 1000);
100 | }
101 |
102 | function getTemplate () {
103 | let templateToLoad;
104 | const xhr = new XMLHttpRequest();
105 |
106 | isCustom = (options.customRepoRegex) ? new RegExp(options.customRepoRegex).test(window.location.href) : false;
107 |
108 | xhr.onreadystatechange = function () {
109 | if (xhr.readyState === 4) {
110 | insertTemplate(xhr.responseText);
111 | }
112 | };
113 |
114 | if (isGH) {
115 | // GitHub cannot retrieve from external URL due to cross origin rules.
116 | insertTemplate(options.githubTemplateContent);
117 | } else if (isBB) {
118 | if (options.bitbucketTemplateContent) return insertTemplate(options.bitbucketTemplateContent);
119 | templateToLoad = options.bitbucketTemplateUrl;
120 | } else if (isCustom) {
121 | if (options.customTemplateContent) return insertTemplate(options.customTemplateContent);
122 | templateToLoad = options.customTemplateUrl;
123 | }
124 |
125 | if (templateToLoad) {
126 | xhr.open('GET', (templateToLoad), true);
127 | xhr.send();
128 | }
129 | }
130 | })();
131 |
--------------------------------------------------------------------------------
/src/scripts/options.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | (function () {
3 | const browser = require('webextension-polyfill');
4 |
5 | // Saves options to browser.storage
6 | function saveOptions () {
7 | const githubEnabled = document.getElementById('githubEnabled').checked;
8 | const githubTemplateUrl = document.getElementById('githubTemplateUrl').value;
9 | const githubTemplateContent = document.getElementById('githubTemplateContent').value;
10 | const bitbucketEnabled = document.getElementById('bitbucketEnabled').checked;
11 | const bitbucketTemplateContent = document.getElementById('bitbucketTemplateContent').value;
12 | const bitbucketTemplateUrl = document.getElementById('bitbucketTemplateUrl').value;
13 | const bitbucketOverwrite = document.getElementById('bitbucketOverwrite').checked;
14 |
15 | const customEnabled = document.getElementById('customRepoEnabled').checked;
16 | const customTemplateContent = document.getElementById('customRepoTemplateContent').value;
17 | const customTemplateUrl = document.getElementById('customRepoTemplateUrl').value;
18 | const customRepoRegex = document.getElementById('customRepoRegex').value;
19 | const customRepoDescriptionID = document.getElementById('customRepoDescriptionID').value;
20 |
21 | browser.storage.sync.set({
22 | githubEnabled: githubEnabled,
23 | githubTemplateUrl: githubTemplateUrl || '',
24 | githubTemplateContent: githubTemplateContent || '',
25 | bitbucketEnabled: bitbucketEnabled,
26 | bitbucketTemplateUrl: bitbucketTemplateUrl || '',
27 | bitbucketTemplateContent: bitbucketTemplateContent || '',
28 | customEnabled: customEnabled,
29 | customTemplateUrl: customTemplateUrl || '',
30 | customTemplateContent: customTemplateContent || '',
31 | customRepoRegex: customRepoRegex || '',
32 | customRepoDescriptionID: customRepoDescriptionID || '',
33 | bitbucketOverwrite: bitbucketOverwrite
34 |
35 | }).then(() => {
36 | // Update status to let user know options were saved.
37 | const result = document.getElementById('save-result');
38 | result.className = result.className.replace(/\bhide\b/, '');
39 |
40 | setTimeout(function () {
41 | result.className = result.className + ' hide';
42 | }, 1500);
43 | }).catch((e) => {
44 | console.error('Could not save options.', e);
45 | });
46 | }
47 |
48 | // Restores select box and checkbox state using the preferences
49 | // stored in browser.storage.
50 | function restoreOptions () {
51 | browser.storage.sync.get({
52 | githubEnabled: true,
53 | githubTemplateUrl: '',
54 | githubTemplateContent: '',
55 | bitbucketEnabled: true,
56 | bitbucketTemplateUrl: '',
57 | bitbucketTemplateContent: '',
58 | bitbucketOverwrite: true,
59 |
60 | customEnabled: true,
61 | customTemplateUrl: '',
62 | customTemplateContent: '',
63 | customRepoRegex: '',
64 | customRepoDescriptionID: ''
65 |
66 | }).then((items) => {
67 | document.getElementById('githubEnabled').checked = items.githubEnabled;
68 | document.getElementById('githubTemplateUrl').value = items.githubTemplateUrl;
69 | document.getElementById('githubTemplateContent').value = items.githubTemplateContent;
70 | document.getElementById('bitbucketEnabled').checked = items.bitbucketEnabled;
71 | document.getElementById('bitbucketTemplateUrl').value = items.bitbucketTemplateUrl;
72 | document.getElementById('bitbucketTemplateContent').value = items.bitbucketTemplateContent;
73 | document.getElementById('bitbucketOverwrite').checked = items.bitbucketOverwrite;
74 |
75 | document.getElementById('customRepoEnabled').checked = items.customEnabled;
76 | document.getElementById('customRepoTemplateUrl').value = items.customTemplateUrl;
77 | document.getElementById('customRepoTemplateContent').value = items.customTemplateContent;
78 | document.getElementById('customRepoRegex').value = items.customRepoRegex;
79 | document.getElementById('customRepoDescriptionID').value = items.customRepoDescriptionID;
80 | }).catch((e) => {
81 | console.error('Could not retrieve options.', e);
82 | });
83 | }
84 |
85 | document.addEventListener('DOMContentLoaded', restoreOptions);
86 | document.getElementById('save').addEventListener('click', saveOptions);
87 | })();
88 |
--------------------------------------------------------------------------------
/src/scripts/popup.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
--------------------------------------------------------------------------------
/src/styles/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 20px;
3 | }
4 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
3 | const CopyPlugin = require('copy-webpack-plugin');
4 | const FileManagerPlugin = require('filemanager-webpack-plugin');
5 | const { EnvironmentPlugin } = require('webpack');
6 |
7 | module.exports = {
8 | target: 'web',
9 | mode: process.env.NODE_ENV || 'development',
10 | devtool: 'cheap-module-source-map',
11 | entry: {
12 | 'scripts/background.js': './src/scripts/background.js',
13 | 'scripts/content.js': './src/scripts/content.js',
14 | 'scripts/popup.js': './src/scripts/popup.js',
15 | 'scripts/options.js': './src/scripts/options.js'
16 | },
17 | output: {
18 | path: path.resolve(__dirname, 'dist'),
19 | filename: '[name]'
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.js$/,
25 | exclude: /node_modules/,
26 | use: 'babel-loader'
27 | }
28 | ]
29 | },
30 |
31 | plugins: [
32 | new EnvironmentPlugin({
33 | DEBUG: process.env.NODE_ENV === 'development'
34 | }),
35 | new CleanWebpackPlugin(),
36 | new CopyPlugin({
37 | patterns: [{
38 | from: 'src/images',
39 | to: 'images'
40 | },
41 | {
42 | from: 'src/pages',
43 | to: 'pages'
44 | },
45 | {
46 | from: 'src/styles',
47 | to: 'styles'
48 | },
49 | {
50 | from: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
51 | to: 'styles'
52 | },
53 | {
54 | from: 'src/_locales',
55 | to: '_locales'
56 | },
57 | {
58 | from: !!process.env.V2 ? 'src/manifest.v2.json' : 'src/manifest.json',
59 | to: 'manifest.json'
60 | }]
61 | }),
62 | process.env.NODE_ENV === 'production' &&
63 | new FileManagerPlugin({
64 | events: {
65 | onEnd: [
66 | {
67 | archive: [
68 | {
69 | source: 'dist/',
70 | destination: `dist/chrome-pullrequest-templates.zip`
71 | }
72 | ]
73 | }
74 | ]
75 | }
76 | })
77 | ].filter(Boolean)
78 | };
79 |
--------------------------------------------------------------------------------