├── .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 | 30 | 31 |

If you enter the template manually the URL will be ignored.

32 | 33 |
34 |
35 |

Do you have feedback or feature request? Please submit an issue.

36 |
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 | 51 |
52 | 53 |

Need inspiration? Check out Sprint.ly's template

54 | 55 |
56 |
57 |
58 | 59 |
60 | 61 |
62 |
63 |

Bitbucket

64 |
65 | 68 |
69 |
70 | 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 |

Need inspiration? Check out Sprint.ly's template

82 | 83 |
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 | --------------------------------------------------------------------------------