├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── bin ├── gh-pages-clean.js └── gh-pages.js ├── changelog.md ├── eslint.config.mjs ├── lib ├── git.js ├── index.js └── util.js ├── package-lock.json ├── package.json ├── plugin.js ├── readme.md ├── tasks └── changelog.sh └── test ├── .eslintrc ├── bin ├── fixtures │ └── beforeAdd.js └── gh-pages.spec.js ├── helper.js ├── integration ├── basic.spec.js ├── beforeAdd.spec.js ├── cname.spec.js ├── cnameExists.spec.js ├── dest.spec.js ├── fixtures │ ├── basic │ │ ├── expected │ │ │ └── hello-world.txt │ │ ├── local │ │ │ └── hello-world.txt │ │ └── remote │ │ │ └── initial │ ├── beforeAdd │ │ ├── expected │ │ │ ├── hello-old-world.txt │ │ │ └── hello-world.txt │ │ ├── local │ │ │ └── hello-world.txt │ │ └── remote │ │ │ ├── hello-old-world.txt │ │ │ └── hello-outdated-world.txt │ ├── cname-exists │ │ ├── expected │ │ │ ├── CNAME │ │ │ └── hello-world.txt │ │ ├── local │ │ │ └── hello-world.txt │ │ └── remote │ │ │ ├── CNAME │ │ │ └── initial │ ├── cname │ │ ├── expected │ │ │ ├── CNAME │ │ │ └── hello-world.txt │ │ ├── local │ │ │ └── hello-world.txt │ │ └── remote │ │ │ └── initial │ ├── dest │ │ ├── expected │ │ │ ├── stable.txt │ │ │ └── target │ │ │ │ ├── one.txt │ │ │ │ └── two.txt │ │ ├── local │ │ │ ├── one.txt │ │ │ └── two.txt │ │ └── remote │ │ │ ├── stable.txt │ │ │ └── target │ │ │ └── removed.txt │ ├── include │ │ ├── expected │ │ │ └── good.js │ │ ├── local │ │ │ ├── bad.coffee │ │ │ └── good.js │ │ └── remote │ │ │ └── initial │ ├── nojekyll-exists │ │ ├── expected │ │ │ ├── .nojekyll │ │ │ └── hello-world.txt │ │ ├── local │ │ │ └── hello-world.txt │ │ └── remote │ │ │ ├── .nojekyll │ │ │ └── initial │ ├── nojekyll │ │ ├── expected │ │ │ ├── .nojekyll │ │ │ └── hello-world.txt │ │ ├── local │ │ │ └── hello-world.txt │ │ └── remote │ │ │ └── initial │ └── remove │ │ ├── expected │ │ ├── new.js │ │ └── stable.txt │ │ ├── local │ │ └── new.js │ │ └── remote │ │ ├── removed.css │ │ ├── removed.js │ │ └── stable.txt ├── include.spec.js ├── nojekyll.spec.js ├── nojekyllExists.spec.js └── remove.spec.js └── lib ├── index.spec.js └── util.spec.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: npm 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | open-pull-requests-limit: 10 9 | versioning-strategy: increase-if-necessary 10 | 11 | - package-ecosystem: github-actions 12 | directory: "/" 13 | schedule: 14 | interval: weekly 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | CI: true 13 | 14 | jobs: 15 | run: 16 | name: Node ${{ matrix.node }} / ${{ matrix.os }} 17 | runs-on: ${{ matrix.os }} 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | os: 23 | - ubuntu-latest 24 | node: 25 | - 18 26 | - 20 27 | - 22 28 | 29 | steps: 30 | - name: Clone repository 31 | uses: actions/checkout@v4 32 | 33 | - name: Set Node.js version 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: ${{ matrix.node }} 37 | 38 | - run: node --version 39 | - run: npm --version 40 | 41 | - name: Install npm dependencies 42 | run: npm install # switch to `ci` when Node.js 6.x is dropped 43 | 44 | - name: Run tests 45 | run: npm test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.cache/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 Tim Schaub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/gh-pages-clean.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const ghpages = require('../lib/index.js'); 4 | 5 | function main() { 6 | ghpages.clean(); 7 | } 8 | 9 | if (require.main === module) { 10 | main(); 11 | } 12 | 13 | module.exports = main; 14 | -------------------------------------------------------------------------------- /bin/gh-pages.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const {Command} = require('commander'); 5 | const addr = require('email-addresses'); 6 | const ghpages = require('../lib/index.js'); 7 | const pkg = require('../package.json'); 8 | 9 | function publish(dist, config) { 10 | return new Promise((resolve, reject) => { 11 | const basePath = path.resolve(process.cwd(), dist); 12 | ghpages.publish(basePath, config, (err) => { 13 | if (err) { 14 | return reject(err); 15 | } 16 | resolve(); 17 | }); 18 | }); 19 | } 20 | 21 | function main(args) { 22 | return Promise.resolve().then(() => { 23 | const program = new Command() 24 | .version(pkg.version) 25 | .requiredOption( 26 | '-d, --dist ', 27 | 'Base directory for all source files', 28 | ) 29 | .option( 30 | '-s, --src ', 31 | 'Pattern used to select which files to publish', 32 | ghpages.defaults.src, 33 | ) 34 | .option( 35 | '-b, --branch ', 36 | 'Name of the branch you are pushing to', 37 | ghpages.defaults.branch, 38 | ) 39 | .option( 40 | '-e, --dest ', 41 | 'Target directory within the destination branch (relative to the root)', 42 | ghpages.defaults.dest, 43 | ) 44 | .option('-a, --add', 'Only add, and never remove existing files') 45 | .option('-x, --silent', 'Do not output the repository url') 46 | .option( 47 | '-m, --message ', 48 | 'commit message', 49 | ghpages.defaults.message, 50 | ) 51 | .option('-g, --tag ', 'add tag to commit') 52 | .option('--git ', 'Path to git executable', ghpages.defaults.git) 53 | .option('-t, --dotfiles', 'Include dotfiles') 54 | .option('--nojekyll', 'Add a .nojekyll file to disable Jekyll') 55 | .option( 56 | '--cname ', 57 | 'Add a CNAME file with the name of your custom domain', 58 | ) 59 | .option('-r, --repo ', 'URL of the repository you are pushing to') 60 | .option('-p, --depth ', 'depth for clone', ghpages.defaults.depth) 61 | .option( 62 | '-o, --remote ', 63 | 'The name of the remote', 64 | ghpages.defaults.remote, 65 | ) 66 | .option( 67 | '-u, --user
', 68 | 'The name and email of the user (defaults to the git config). Format is "Your Name ".', 69 | ) 70 | .option( 71 | '-v, --remove ', 72 | 'Remove files that match the given pattern ' + 73 | '(ignored if used together with --add).', 74 | ghpages.defaults.remove, 75 | ) 76 | .option('-n, --no-push', 'Commit only (with no push)') 77 | .option( 78 | '-f, --no-history', 79 | 'Push force new commit without parent history', 80 | ) 81 | .option( 82 | '--before-add ', 83 | 'Execute the function exported by before "git add"', 84 | ) 85 | .parse(args); 86 | 87 | const options = program.opts(); 88 | 89 | let user; 90 | if (options.user) { 91 | const parts = addr.parseOneAddress(options.user); 92 | if (!parts) { 93 | throw new Error( 94 | `Could not parse name and email from user option "${options.user}" ` + 95 | '(format should be "Your Name ")', 96 | ); 97 | } 98 | user = {name: parts.name, email: parts.address}; 99 | } 100 | let beforeAdd; 101 | if (options.beforeAdd) { 102 | const m = require( 103 | require.resolve(options.beforeAdd, { 104 | paths: [process.cwd()], 105 | }), 106 | ); 107 | 108 | if (typeof m === 'function') { 109 | beforeAdd = m; 110 | } else if (typeof m === 'object' && typeof m.default === 'function') { 111 | beforeAdd = m.default; 112 | } else { 113 | throw new Error( 114 | `Could not find function to execute before adding files in ` + 115 | `"${options.beforeAdd}".\n `, 116 | ); 117 | } 118 | } 119 | 120 | const config = { 121 | repo: options.repo, 122 | silent: !!options.silent, 123 | branch: options.branch, 124 | src: options.src, 125 | dest: options.dest, 126 | message: options.message, 127 | tag: options.tag, 128 | git: options.git, 129 | depth: options.depth, 130 | dotfiles: !!options.dotfiles, 131 | nojekyll: !!options.nojekyll, 132 | cname: options.cname, 133 | add: !!options.add, 134 | remove: options.remove, 135 | remote: options.remote, 136 | push: !!options.push, 137 | history: !!options.history, 138 | user: user, 139 | beforeAdd: beforeAdd, 140 | }; 141 | 142 | return publish(options.dist, config); 143 | }); 144 | } 145 | 146 | if (require.main === module) { 147 | main(process.argv) 148 | .then(() => { 149 | process.stdout.write('Published\n'); 150 | }) 151 | .catch((err) => { 152 | process.stderr.write(`${err.stack}\n`, () => process.exit(1)); 153 | }); 154 | } 155 | 156 | module.exports = main; 157 | exports = module.exports; 158 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## v6.3.0 2 | 3 | This relesae includes a fix for filenames starting with `-` and a number of dependency updates. See below for details. 4 | 5 | * [#593](https://github.com/tschaub/gh-pages/pull/593) - Handle filenames starting with a dash ([@sherlockdoyle](https://github.com/sherlockdoyle)) 6 | * [#591](https://github.com/tschaub/gh-pages/pull/591) - Bump commander from 11.1.0 to 13.0.0 ([@tschaub](https://github.com/tschaub)) 7 | * [#587](https://github.com/tschaub/gh-pages/pull/587) - Bump eslint-config-tschaub from 14.1.2 to 15.1.0 ([@tschaub](https://github.com/tschaub)) 8 | * [#590](https://github.com/tschaub/gh-pages/pull/590) - Bump mocha from 10.8.2 to 11.0.1 ([@tschaub](https://github.com/tschaub)) 9 | * [#583](https://github.com/tschaub/gh-pages/pull/583) - Bump mocha from 10.7.3 to 10.8.2 ([@tschaub](https://github.com/tschaub)) 10 | * [#584](https://github.com/tschaub/gh-pages/pull/584) - Bump chai from 4.3.10 to 4.5.0 ([@tschaub](https://github.com/tschaub)) 11 | 12 | ## v6.2.0 13 | 14 | Assorted dependency updates and a documentation change. 15 | 16 | * [#581](https://github.com/tschaub/gh-pages/pull/581) - Update globby ([@tschaub](https://github.com/tschaub)) 17 | * [#578](https://github.com/tschaub/gh-pages/pull/578) - Bump sinon from 18.0.0 to 19.0.2 ([@tschaub](https://github.com/tschaub)) 18 | * [#579](https://github.com/tschaub/gh-pages/pull/579) - Bump eslint from 8.57.0 to 8.57.1 ([@tschaub](https://github.com/tschaub)) 19 | * [#576](https://github.com/tschaub/gh-pages/pull/576) - Bump async from 3.2.5 to 3.2.6 ([@tschaub](https://github.com/tschaub)) 20 | * [#573](https://github.com/tschaub/gh-pages/pull/573) - Bump mocha from 10.7.0 to 10.7.3 ([@tschaub](https://github.com/tschaub)) 21 | * [#571](https://github.com/tschaub/gh-pages/pull/571) - Bump mocha from 10.6.0 to 10.7.0 ([@tschaub](https://github.com/tschaub)) 22 | * [#569](https://github.com/tschaub/gh-pages/pull/569) - Bump mocha from 10.4.0 to 10.6.0 ([@tschaub](https://github.com/tschaub)) 23 | * [#563](https://github.com/tschaub/gh-pages/pull/563) - Bump braces from 3.0.2 to 3.0.3 ([@tschaub](https://github.com/tschaub)) 24 | * [#561](https://github.com/tschaub/gh-pages/pull/561) - Bump sinon from 17.0.2 to 18.0.0 ([@tschaub](https://github.com/tschaub)) 25 | * [#557](https://github.com/tschaub/gh-pages/pull/557) - Bump sinon from 17.0.1 to 17.0.2 ([@tschaub](https://github.com/tschaub)) 26 | * [#555](https://github.com/tschaub/gh-pages/pull/555) - Bump dir-compare from 4.2.0 to 5.0.0 ([@tschaub](https://github.com/tschaub)) 27 | * [#550](https://github.com/tschaub/gh-pages/pull/550) - Bump mocha from 10.3.0 to 10.4.0 ([@tschaub](https://github.com/tschaub)) 28 | * [#549](https://github.com/tschaub/gh-pages/pull/549) - Bump tmp from 0.2.1 to 0.2.3 ([@tschaub](https://github.com/tschaub)) 29 | * [#548](https://github.com/tschaub/gh-pages/pull/548) - Bump eslint from 8.56.0 to 8.57.0 ([@tschaub](https://github.com/tschaub)) 30 | * [#545](https://github.com/tschaub/gh-pages/pull/545) - Bump mocha from 10.2.0 to 10.3.0 ([@tschaub](https://github.com/tschaub)) 31 | * [#541](https://github.com/tschaub/gh-pages/pull/541) - fix: update instruction for next.js ([@multivoltage](https://github.com/multivoltage)) 32 | 33 | ## v6.1.1 34 | 35 | This release fixes an issue with the `--cname` option. 36 | 37 | * [#535](https://github.com/tschaub/gh-pages/pull/535) - fix: Add missing cname option not passed to the config ([@WillBAnders](https://github.com/WillBAnders)) 38 | * [#539](https://github.com/tschaub/gh-pages/pull/539) - Bump eslint from 8.55.0 to 8.56.0 ([@tschaub](https://github.com/tschaub)) 39 | 40 | ## v6.1.0 41 | 42 | This release adds support for `--nojekyll` and `--cname` options. 43 | 44 | * [#531](https://github.com/tschaub/gh-pages/pull/531) - Bump sinon from 15.2.0 to 17.0.1 ([@tschaub](https://github.com/tschaub)) 45 | * [#529](https://github.com/tschaub/gh-pages/pull/529) - Bump async from 3.2.4 to 3.2.5 ([@tschaub](https://github.com/tschaub)) 46 | * [#524](https://github.com/tschaub/gh-pages/pull/524) - Bump commander from 11.0.0 to 11.1.0 ([@tschaub](https://github.com/tschaub)) 47 | * [#530](https://github.com/tschaub/gh-pages/pull/530) - Bump eslint from 8.49.0 to 8.53.0 ([@tschaub](https://github.com/tschaub)) 48 | * [#520](https://github.com/tschaub/gh-pages/pull/520) - Bump chai from 4.3.8 to 4.3.10 ([@tschaub](https://github.com/tschaub)) 49 | * [#527](https://github.com/tschaub/gh-pages/pull/527) - Bump actions/setup-node from 3 to 4 ([@tschaub](https://github.com/tschaub)) 50 | * [#533](https://github.com/tschaub/gh-pages/pull/533) - Add --nojekyll and --cname options ([@tschaub](https://github.com/tschaub)) 51 | * [#512](https://github.com/tschaub/gh-pages/pull/512) - Bump dir-compare from 4.0.0 to 4.2.0 ([@tschaub](https://github.com/tschaub)) 52 | * [#513](https://github.com/tschaub/gh-pages/pull/513) - Bump chai from 4.3.7 to 4.3.8 ([@tschaub](https://github.com/tschaub)) 53 | * [#515](https://github.com/tschaub/gh-pages/pull/515) - Bump actions/checkout from 3 to 4 ([@tschaub](https://github.com/tschaub)) 54 | * [#516](https://github.com/tschaub/gh-pages/pull/516) - Bump eslint from 8.46.0 to 8.49.0 ([@tschaub](https://github.com/tschaub)) 55 | 56 | ## v6.0.0 57 | 58 | This release drops support for Node 14. Otherwise, there are no special upgrade considerations. 59 | 60 | * [#507](https://github.com/tschaub/gh-pages/pull/507) - Dependency updates and drop Node 14 ([@tschaub](https://github.com/tschaub)) 61 | * [#495](https://github.com/tschaub/gh-pages/pull/495) - Bump sinon from 15.0.3 to 15.2.0 ([@tschaub](https://github.com/tschaub)) 62 | * [#500](https://github.com/tschaub/gh-pages/pull/500) - Bump semver from 6.3.0 to 6.3.1 ([@tschaub](https://github.com/tschaub)) 63 | * [#506](https://github.com/tschaub/gh-pages/pull/506) - Bump eslint from 8.38.0 to 8.46.0 ([@tschaub](https://github.com/tschaub)) 64 | * [#505](https://github.com/tschaub/gh-pages/pull/505) - Bump word-wrap from 1.2.3 to 1.2.5 ([@tschaub](https://github.com/tschaub)) 65 | * [#504](https://github.com/tschaub/gh-pages/pull/504) - Add error message when --dist is not specified. ([@domsleee](https://github.com/domsleee)) 66 | * [#473](https://github.com/tschaub/gh-pages/pull/473) - Bump fs-extra from 8.1.0 to 11.1.1 ([@tschaub](https://github.com/tschaub)) 67 | * [#477](https://github.com/tschaub/gh-pages/pull/477) - Bump eslint from 8.32.0 to 8.38.0 ([@tschaub](https://github.com/tschaub)) 68 | * [#474](https://github.com/tschaub/gh-pages/pull/474) - Bump sinon from 15.0.1 to 15.0.3 ([@tschaub](https://github.com/tschaub)) 69 | * [#440](https://github.com/tschaub/gh-pages/pull/440) - Update readme.md ([@harveer07](https://github.com/harveer07)) 70 | 71 | ## v5.0.0 72 | 73 | Potentially breaking change: the `publish` method now always returns a promise. Previously, it did not return a promise in some error cases. This should not impact most users. 74 | 75 | Updates to the development dependencies required a minimum Node version of 14 for the tests. The library should still work on Node 12, but tests are no longer run in CI for version 12. A future major version of the library may drop support for version 12 altogether. 76 | 77 | * [#438](https://github.com/tschaub/gh-pages/pull/438) - Remove quotation marks ([@Vicropht](https://github.com/Vicropht)) 78 | * [#459](https://github.com/tschaub/gh-pages/pull/459) - Bump async from 2.6.4 to 3.2.4 ([@tschaub](https://github.com/tschaub)) 79 | * [#454](https://github.com/tschaub/gh-pages/pull/454) - Bump email-addresses from 3.0.1 to 5.0.0 ([@tschaub](https://github.com/tschaub)) 80 | * [#455](https://github.com/tschaub/gh-pages/pull/455) - Bump actions/setup-node from 1 to 3 ([@tschaub](https://github.com/tschaub)) 81 | * [#453](https://github.com/tschaub/gh-pages/pull/453) - Bump actions/checkout from 2 to 3 ([@tschaub](https://github.com/tschaub)) 82 | * [#445](https://github.com/tschaub/gh-pages/pull/445) - Update README to clarify project site configuration requirements with tools like CRA, webpack, Vite, etc. ([@Nezteb](https://github.com/Nezteb)) 83 | * [#452](https://github.com/tschaub/gh-pages/pull/452) - Assorted updates ([@tschaub](https://github.com/tschaub)) 84 | 85 | ## v4.0.0 86 | 87 | This release doesn't include any breaking changes, but due to updated development dependencies, tests are no longer run on Node 10. 88 | 89 | * [#432](https://github.com/tschaub/gh-pages/pull/432) - Updated dev dependencies and formatting ([@tschaub](https://github.com/tschaub)) 90 | * [#430](https://github.com/tschaub/gh-pages/pull/430) - Bump ansi-regex from 3.0.0 to 3.0.1 ([@tschaub](https://github.com/tschaub)) 91 | * [#431](https://github.com/tschaub/gh-pages/pull/431) - Bump path-parse from 1.0.6 to 1.0.7 ([@tschaub](https://github.com/tschaub)) 92 | * [#427](https://github.com/tschaub/gh-pages/pull/427) - Bump async from 2.6.1 to 2.6.4 ([@tschaub](https://github.com/tschaub)) 93 | * [#423](https://github.com/tschaub/gh-pages/pull/423) - Bump minimist from 1.2.5 to 1.2.6 ([@tschaub](https://github.com/tschaub)) 94 | 95 | 96 | ## v3.2.3 97 | 98 | * [#398](https://github.com/tschaub/gh-pages/pull/398) - Update glob-parent ([@tschaub](https://github.com/tschaub)) 99 | * [#395](https://github.com/tschaub/gh-pages/pull/395) - Switch from filenamify-url to filenamify ([@tw0517tw](https://github.com/tw0517tw)) 100 | 101 | ## v3.2.2 102 | 103 | * [#396](https://github.com/tschaub/gh-pages/pull/396) - Revert "security(deps): bump filenamify-url to 2.1.1" ([@tschaub](https://github.com/tschaub)) 104 | 105 | ## v3.2.1 106 | 107 | * [#393](https://github.com/tschaub/gh-pages/pull/393) - security(deps): bump filenamify-url to 2.1.1 ([@AviVahl](https://github.com/AviVahl)) 108 | 109 | ## v3.2.0 110 | 111 | This release updates a few development dependencies and adds a bit of documentation. 112 | 113 | * [#391](https://github.com/tschaub/gh-pages/pull/391) - Update dev dependencies ([@tschaub](https://github.com/tschaub)) 114 | * [#375](https://github.com/tschaub/gh-pages/pull/375) - Add note about domain problem ([@demee](https://github.com/demee)) 115 | * [#390](https://github.com/tschaub/gh-pages/pull/390) - Fix little typo in the README ([@cizordj](https://github.com/cizordj)) 116 | * [#388](https://github.com/tschaub/gh-pages/pull/388) - Bump hosted-git-info from 2.8.8 to 2.8.9 ([@tschaub](https://github.com/tschaub)) 117 | * [#387](https://github.com/tschaub/gh-pages/pull/387) - Bump y18n from 4.0.0 to 4.0.3 ([@tschaub](https://github.com/tschaub)) 118 | * [#378](https://github.com/tschaub/gh-pages/pull/378) - Add GitHub Actions tips to readme.md ([@mickelsonmichael](https://github.com/mickelsonmichael)) 119 | * [#386](https://github.com/tschaub/gh-pages/pull/386) - Bump lodash from 4.17.14 to 4.17.21 ([@tschaub](https://github.com/tschaub)) 120 | 121 | ## v3.1.0 122 | 123 | The cache directory used by `gh-pages` is now `node_modules/.cache/gh-pages`. If you want to use a different location, set the `CACHE_DIR` environment variable. 124 | 125 | * [#362](https://github.com/tschaub/gh-pages/pull/362) - Move the cache directory ([@tschaub](https://github.com/tschaub)) 126 | * [#361](https://github.com/tschaub/gh-pages/pull/361) - Update dev dependencies ([@tschaub](https://github.com/tschaub)) 127 | 128 | ## v3.0.0 129 | 130 | Breaking changes: 131 | 132 | None really. But tests are no longer run on Node < 10. Development dependencies were updated to address security warnings, and this meant tests could no longer be run on Node 6 or 8. If you still use these Node versions, you may still be able to use this library, but be warned that tests are no longer run on these versions. 133 | 134 | All changes: 135 | 136 | * [#357](https://github.com/tschaub/gh-pages/pull/357) - Dev dependency updates ([@tschaub](https://github.com/tschaub)) 137 | * [#333](https://github.com/tschaub/gh-pages/pull/333) - Update readme with command line options ([@Victoire44](https://github.com/Victoire44)) 138 | * [#356](https://github.com/tschaub/gh-pages/pull/356) - Test as a GitHub action ([@tschaub](https://github.com/tschaub)) 139 | * [#355](https://github.com/tschaub/gh-pages/pull/355) - feat(beforeAdd): allow custom script before git add ([@Xiphe](https://github.com/Xiphe)) 140 | * [#336](https://github.com/tschaub/gh-pages/pull/336) - Fix remove not working properly ([@sunghwan2789](https://github.com/sunghwan2789)) 141 | * [#328](https://github.com/tschaub/gh-pages/pull/328) - Update .travis.yml ([@XhmikosR](https://github.com/XhmikosR)) 142 | * [#327](https://github.com/tschaub/gh-pages/pull/327) - Fix typo ([@d-tsuji](https://github.com/d-tsuji)) 143 | 144 | ## v2.2.0 145 | 146 | * [#318](https://github.com/tschaub/gh-pages/pull/318) - Allow an absolute path as dist directory ([@okuryu](https://github.com/okuryu)) 147 | * [#319](https://github.com/tschaub/gh-pages/pull/319) - Added 'remove' documentation to the readme ([@Sag-Dev](https://github.com/Sag-Dev)) 148 | * [#323](https://github.com/tschaub/gh-pages/pull/323) - Update dependencies ([@tschaub](https://github.com/tschaub)) 149 | * [#277](https://github.com/tschaub/gh-pages/pull/277) - Add `--no-history` flag not to preserve deploy history ([@dplusic](https://github.com/dplusic)) 150 | 151 | ## v2.1.1 152 | 153 | * [#312](https://github.com/tschaub/gh-pages/pull/312) - Add default for '--git' option ([@tschaub](https://github.com/tschaub)) 154 | 155 | ## v2.1.0 156 | 157 | * [#307](https://github.com/tschaub/gh-pages/pull/307) - Dev dependency updates ([@tschaub](https://github.com/tschaub)) 158 | * [#303](https://github.com/tschaub/gh-pages/pull/303) - Support '--git' CLI option ([@JRJurman](https://github.com/JRJurman)) 159 | 160 | ## v2.0.1 161 | 162 | * [#268](https://github.com/tschaub/gh-pages/pull/268) - Continue even if no git configured user. 163 | 164 | ## v2.0.0 165 | 166 | Breaking changes: 167 | 168 | * Requires Node 6 and above. If you require support for Node 4, stick with v1.2.0. 169 | * The git user for commits is determined by running `git config user.name` and `git config user.email` in the current working directory when `gh-pages` is run. Ideally, this is what you want. In v1, the git user was determined based on the `gh-pages` install directory. If the package was installed globally, the git user might not have been what you expected when running in a directory with a locally configured git user. 170 | 171 | * [#264](https://github.com/tschaub/gh-pages/pull/264) - Better user handling (thanks @holloway for getting this going and @nuklearfiziks and @paulirish for pushing it over the edge) 172 | * [#263](https://github.com/tschaub/gh-pages/pull/263) - Infra: newer syntax and upgrade deps to latest stable versions ([@AviVahl](https://github.com/AviVahl)) 173 | 174 | 175 | ## v1.2.0 176 | 177 | * [#252](https://github.com/tschaub/gh-pages/pull/252) - Update dependencies ([@tschaub](https://github.com/tschaub)) 178 | * [#245](https://github.com/tschaub/gh-pages/pull/245) - Typos ([@thekevinscott](https://github.com/thekevinscott)) 179 | * [#251](https://github.com/tschaub/gh-pages/pull/251) - Update async to the latest version 🚀 ([@tschaub](https://github.com/tschaub)) 180 | * [#243](https://github.com/tschaub/gh-pages/pull/243) - docs(readme.md): add tips ([@polyglotm](https://github.com/polyglotm)) 181 | * [#241](https://github.com/tschaub/gh-pages/pull/241) - Update sinon to the latest version 🚀 ([@tschaub](https://github.com/tschaub)) 182 | * [#240](https://github.com/tschaub/gh-pages/pull/240) - Update eslint-config-tschaub to the latest version 🚀 ([@tschaub](https://github.com/tschaub)) 183 | * [#239](https://github.com/tschaub/gh-pages/pull/239) - Assorted updates ([@tschaub](https://github.com/tschaub)) 184 | * [#238](https://github.com/tschaub/gh-pages/pull/238) - fix(package): update commander to version 2.15.1 ([@tschaub](https://github.com/tschaub)) 185 | * [#237](https://github.com/tschaub/gh-pages/pull/237) - chore(package): update mocha to version 5.0.5 ([@tschaub](https://github.com/tschaub)) 186 | * [#232](https://github.com/tschaub/gh-pages/pull/232) - Update sinon to the latest version 🚀 ([@tschaub](https://github.com/tschaub)) 187 | 188 | ## v1.1.0 189 | 190 | * [#218](https://github.com/tschaub/gh-pages/pull/218) - Update dependencies, test on Node 8 ([@tschaub](https://github.com/tschaub)) 191 | * [#211](https://github.com/tschaub/gh-pages/pull/211) - Update async to the latest version 🚀 ([@tschaub](https://github.com/tschaub)) 192 | * [#202](https://github.com/tschaub/gh-pages/pull/202) - chore(package): update sinon to version 3.2.1 ([@tschaub](https://github.com/tschaub)) 193 | * [#201](https://github.com/tschaub/gh-pages/pull/201) - chore(package): update chai to version 4.1.1 ([@tschaub](https://github.com/tschaub)) 194 | * [#196](https://github.com/tschaub/gh-pages/pull/196) - fix(package): update fs-extra to version 4.0.1 ([@tschaub](https://github.com/tschaub)) 195 | * [#199](https://github.com/tschaub/gh-pages/pull/199) - Update tmp to the latest version 🚀 ([@tschaub](https://github.com/tschaub)) 196 | * [#193](https://github.com/tschaub/gh-pages/pull/193) - Return the promise in the publish function ([@Ambyjkl](https://github.com/Ambyjkl)) 197 | * [#188](https://github.com/tschaub/gh-pages/pull/188) - chore(package): update sinon to version 2.3.3 ([@tschaub](https://github.com/tschaub)) 198 | * [#185](https://github.com/tschaub/gh-pages/pull/185) - fix(package): update commander to version 2.11.0 ([@tschaub](https://github.com/tschaub)) 199 | * [#186](https://github.com/tschaub/gh-pages/pull/186) - chore(package): update eslint to version 4.1.1 ([@tschaub](https://github.com/tschaub)) 200 | * [#187](https://github.com/tschaub/gh-pages/pull/187) - fix(package): update async to version 2.5.0 ([@tschaub](https://github.com/tschaub)) 201 | * [#175](https://github.com/tschaub/gh-pages/pull/175) - Removed unnecessary path require ([@antialias](https://github.com/antialias)) 202 | 203 | 204 | ## v1.0.0 205 | 206 | This release includes a couple breaking changes: 207 | 208 | * Node 4+ is required. 209 | * The `logger` option has been removed. Set `NODE_DEBUG=gh-pages` to see debug output. 210 | 211 | If you are using Node 4+ and not using the `logger` option, upgrades should be painless. See below for a full list of changes: 212 | 213 | * [#174](https://github.com/tschaub/gh-pages/pull/174) - Remove the logger option and use util.debuglog() ([@tschaub](https://github.com/tschaub)) 214 | * [#173](https://github.com/tschaub/gh-pages/pull/173) - Dedicated cache directory per repo ([@tschaub](https://github.com/tschaub)) 215 | * [#172](https://github.com/tschaub/gh-pages/pull/172) - Provision for root path when splitting ([@esarbanis](https://github.com/esarbanis)) 216 | * [#171](https://github.com/tschaub/gh-pages/pull/171) - Add a dest option ([@lelandmiller](https://github.com/lelandmiller)) 217 | * [#73](https://github.com/tschaub/gh-pages/pull/73) - feat(plugin): add plugin support for semantic-release ([@tusharmath](https://github.com/tusharmath)) 218 | * [#170](https://github.com/tschaub/gh-pages/pull/170) - Integration tests ([@tschaub](https://github.com/tschaub)) 219 | * [#21](https://github.com/tschaub/gh-pages/pull/21) - Document that git 1.9+ is required. ([@warmhug](https://github.com/warmhug)) 220 | * [#169](https://github.com/tschaub/gh-pages/pull/169) - Fix noPush command argument and include regression tests for the CLI ([@thiagofelix](https://github.com/thiagofelix)) 221 | * [#168](https://github.com/tschaub/gh-pages/pull/168) - Clone with depth 1 by default ([@tschaub](https://github.com/tschaub)) 222 | * [#167](https://github.com/tschaub/gh-pages/pull/167) - Require Node 4+ ([@tschaub](https://github.com/tschaub)) 223 | * [#166](https://github.com/tschaub/gh-pages/pull/166) - Updates ([@tschaub](https://github.com/tschaub)) 224 | * [#158](https://github.com/tschaub/gh-pages/pull/158) - Update dependencies to enable Greenkeeper 🌴 ([@tschaub](https://github.com/tschaub)) 225 | * [#150](https://github.com/tschaub/gh-pages/pull/150) - Fix small typo ([@mandeldl](https://github.com/mandeldl)) 226 | 227 | ## v0.12.0 228 | 229 | * [#146](https://github.com/tschaub/gh-pages/pull/146) - Updates dependencies ([@tschaub](https://github.com/tschaub)) 230 | * [#138](https://github.com/tschaub/gh-pages/pull/138) - Updated readme.md with svg image ([@sobolevn](https://github.com/sobolevn)) 231 | * [#142](https://github.com/tschaub/gh-pages/pull/142) - Update globby to version 6.1.0 🚀 ([@tschaub](https://github.com/tschaub)) 232 | * [#134](https://github.com/tschaub/gh-pages/pull/134) - Update eslint to version 3.8.0 🚀 ([@tschaub](https://github.com/tschaub)) 233 | * [#135](https://github.com/tschaub/gh-pages/pull/135) - Update async to version 2.1.2 🚀 ([@tschaub](https://github.com/tschaub)) 234 | * [#130](https://github.com/tschaub/gh-pages/pull/130) - Update mocha to version 3.1.1 🚀 ([@tschaub](https://github.com/tschaub)) 235 | * [#112](https://github.com/tschaub/gh-pages/pull/112) - Update graceful-fs to version 4.1.6 🚀 ([@tschaub](https://github.com/tschaub)) 236 | * [#106](https://github.com/tschaub/gh-pages/pull/106) - Add a --tag option to the cli ([@donavon](https://github.com/donavon)) 237 | * [#102](https://github.com/tschaub/gh-pages/pull/102) - Update graceful-fs to version 4.1.5 🚀 ([@tschaub](https://github.com/tschaub)) 238 | * [#89](https://github.com/tschaub/gh-pages/pull/89) - Update globby to version 5.0.0 🚀 ([@tschaub](https://github.com/tschaub)) 239 | * [#87](https://github.com/tschaub/gh-pages/pull/87) - Update eslint-config-tschaub to version 5.0.0 🚀 ([@tschaub](https://github.com/tschaub)) 240 | * [#78](https://github.com/tschaub/gh-pages/pull/78) - Use rimraf for cleaning ([@tschaub](https://github.com/tschaub)) 241 | * [#75](https://github.com/tschaub/gh-pages/pull/75) - Expose a "remove" option to the CLI ([@tschaub](https://github.com/tschaub)) 242 | * [#76](https://github.com/tschaub/gh-pages/pull/76) - Update eslint to version 2.8.0 🚀 ([@tschaub](https://github.com/tschaub)) 243 | * [#70](https://github.com/tschaub/gh-pages/pull/70) - Update eslint to version 2.7.0 🚀 ([@tschaub](https://github.com/tschaub)) 244 | * [#63](https://github.com/tschaub/gh-pages/pull/63) - Update eslint to version 2.4.0 🚀 ([@tschaub](https://github.com/tschaub)) 245 | * [#62](https://github.com/tschaub/gh-pages/pull/62) - Update eslint to version 2.3.0 🚀 ([@tschaub](https://github.com/tschaub)) 246 | 247 | ## v0.11.0 248 | 249 | * [#61](https://github.com/tschaub/gh-pages/pull/61) - Support a custom remote. ([@marco-c](https://github.com/marco-c)) 250 | * [#60](https://github.com/tschaub/gh-pages/pull/60) - Update eslint-config-tschaub to version 4.0.0 🚀 ([@tschaub](https://github.com/tschaub)) 251 | * [#57](https://github.com/tschaub/gh-pages/pull/57) - Update eslint to version 2.2.0 🚀 ([@tschaub](https://github.com/tschaub)) 252 | * [#59](https://github.com/tschaub/gh-pages/pull/59) - Allow an array of src patterns to be provided. ([@tschaub](https://github.com/tschaub)) 253 | * [#54](https://github.com/tschaub/gh-pages/pull/54) - Ugrade ESLint and config. ([@tschaub](https://github.com/tschaub)) 254 | 255 | 256 | ## v0.10.0 257 | 258 | * [#50](https://github.com/tschaub/gh-pages/pull/50) - Update glob to version 7.0.0 🚀 ([@tschaub](https://github.com/tschaub)) 259 | * [#51](https://github.com/tschaub/gh-pages/pull/51) - Add --silent option to the bin ([@MoOx](https://github.com/MoOx)) 260 | 261 | 262 | ## v0.9.0 263 | 264 | * [#46](https://github.com/tschaub/gh-pages/pull/46) - cli: add `--no-push` flag to allow testing ([@amtrack](https://github.com/amtrack)) 265 | * [#43](https://github.com/tschaub/gh-pages/pull/43) - Update async to version 1.5.2 🚀 ([@tschaub](https://github.com/tschaub)) 266 | * [#41](https://github.com/tschaub/gh-pages/pull/41) - Update async to version 1.5.1 🚀 ([@tschaub](https://github.com/tschaub)) 267 | 268 | ## v0.8.0 269 | 270 | * [#38](https://github.com/tschaub/gh-pages/pull/38) - Update all dependencies 🌴 ([@tschaub](https://github.com/tschaub)) 271 | * [#39](https://github.com/tschaub/gh-pages/pull/39) - Add message option to cli. ([@markdalgleish](https://github.com/markdalgleish)) 272 | * [#37](https://github.com/tschaub/gh-pages/pull/37) - Rework callback error handling. ([@tschaub](https://github.com/tschaub)) 273 | * [#36](https://github.com/tschaub/gh-pages/pull/36) - Better error handling. ([@timaschew](https://github.com/timaschew)) 274 | * [#35](https://github.com/tschaub/gh-pages/pull/35) - Make CLI exit with error if publishing fails. ([@timaschew](https://github.com/timaschew)) 275 | 276 | ## v0.7.0 277 | 278 | * [#32](https://github.com/tschaub/gh-pages/pull/32) - Remove dependency on Lodash. ([@tschaub](https://github.com/tschaub)) 279 | 280 | ## v0.6.0 281 | 282 | * [#31](https://github.com/tschaub/gh-pages/pull/31) - Updated linter and assorted dependencies. ([@tschaub](https://github.com/tschaub)) 283 | * [#23](https://github.com/tschaub/gh-pages/pull/23) - Support `--repo` CLI option. ([@cvan](https://github.com/cvan)) 284 | 285 | ## v0.5.0 286 | 287 | * [#26](https://github.com/tschaub/gh-pages/pull/26) - Added support for the --add option to cli. ([@n1k0](https://github.com/n1k0)) 288 | 289 | ## v0.4.0 290 | 291 | * Option to include dotfiles. 292 | 293 | ## v0.3.0 294 | 295 | * [#18](https://github.com/tschaub/gh-pages/pull/18) - Support command line. ([@afc163](https://github.com/afc163)) 296 | 297 | ## v0.2.0 298 | 299 | * [#9](https://github.com/tschaub/gh-pages/pull/9) - Port readme and update to reflect API changes. ([@markdalgleish](https://github.com/markdalgleish)) 300 | * [#8](https://github.com/tschaub/gh-pages/pull/8) - Make base path required and options optional. ([@markdalgleish](https://github.com/markdalgleish)) 301 | * [#7](https://github.com/tschaub/gh-pages/pull/7) - Use glob and fs instead of Grunt, fixes #2. ([@markdalgleish](https://github.com/markdalgleish)) 302 | * [#6](https://github.com/tschaub/gh-pages/pull/6) - Move cache to install directory, fixes #4. ([@markdalgleish](https://github.com/markdalgleish)) 303 | * [#5](https://github.com/tschaub/gh-pages/pull/5) - Add util tests. ([@tschaub](https://github.com/tschaub)) 304 | * [#1](https://github.com/tschaub/gh-pages/pull/1) - Extract publish task logic from grunt-gh-pages. ([@markdalgleish](https://github.com/markdalgleish)) 305 | 306 | ## v0.1.0 307 | 308 | * Setup. 309 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import configs from 'eslint-config-tschaub'; 2 | 3 | export default [...configs]; 4 | -------------------------------------------------------------------------------- /lib/git.js: -------------------------------------------------------------------------------- 1 | const cp = require('child_process'); 2 | const path = require('path'); 3 | const util = require('util'); 4 | const fs = require('fs-extra'); 5 | 6 | /** 7 | * @function Object() { [native code] } 8 | * @param {number} code Error code. 9 | * @param {string} message Error message. 10 | */ 11 | function ProcessError(code, message) { 12 | const callee = arguments.callee; 13 | Error.apply(this, [message]); 14 | Error.captureStackTrace(this, callee); 15 | this.code = code; 16 | this.message = message; 17 | this.name = callee.name; 18 | } 19 | util.inherits(ProcessError, Error); 20 | 21 | /** 22 | * Util function for handling spawned processes as promises. 23 | * @param {string} exe Executable. 24 | * @param {Array} args Arguments. 25 | * @param {string} cwd Working directory. 26 | * @return {Promise} A promise. 27 | */ 28 | function spawn(exe, args, cwd) { 29 | return new Promise((resolve, reject) => { 30 | const child = cp.spawn(exe, args, {cwd: cwd || process.cwd()}); 31 | const buffer = []; 32 | child.stderr.on('data', (chunk) => { 33 | buffer.push(chunk.toString()); 34 | }); 35 | child.stdout.on('data', (chunk) => { 36 | buffer.push(chunk.toString()); 37 | }); 38 | child.on('close', (code) => { 39 | const output = buffer.join(''); 40 | if (code) { 41 | const msg = output || 'Process failed: ' + code; 42 | reject(new ProcessError(code, msg)); 43 | } else { 44 | resolve(output); 45 | } 46 | }); 47 | }); 48 | } 49 | 50 | /** 51 | * Create an object for executing git commands. 52 | * @param {string} cwd Repository directory. 53 | * @param {string} cmd Git executable (full path if not already on path). 54 | * @function Object() { [native code] } 55 | */ 56 | function Git(cwd, cmd) { 57 | this.cwd = cwd; 58 | this.cmd = cmd || 'git'; 59 | this.output = ''; 60 | } 61 | 62 | /** 63 | * Execute an arbitrary git command. 64 | * @param {Array} args Arguments (e.g. ['remote', 'update']). 65 | * @return {Promise} A promise. The promise will be resolved with this instance 66 | * or rejected with an error. 67 | */ 68 | Git.prototype.exec = function (...args) { 69 | return spawn(this.cmd, [...args], this.cwd).then((output) => { 70 | this.output = output; 71 | return this; 72 | }); 73 | }; 74 | 75 | /** 76 | * Initialize repository. 77 | * @return {Promise} A promise. 78 | */ 79 | Git.prototype.init = function () { 80 | return this.exec('init'); 81 | }; 82 | 83 | /** 84 | * Clean up unversioned files. 85 | * @return {Promise} A promise. 86 | */ 87 | Git.prototype.clean = function () { 88 | return this.exec('clean', '-f', '-d'); 89 | }; 90 | 91 | /** 92 | * Hard reset to remote/branch 93 | * @param {string} remote Remote alias. 94 | * @param {string} branch Branch name. 95 | * @return {Promise} A promise. 96 | */ 97 | Git.prototype.reset = function (remote, branch) { 98 | return this.exec('reset', '--hard', remote + '/' + branch); 99 | }; 100 | 101 | /** 102 | * Fetch from a remote. 103 | * @param {string} remote Remote alias. 104 | * @return {Promise} A promise. 105 | */ 106 | Git.prototype.fetch = function (remote) { 107 | return this.exec('fetch', remote); 108 | }; 109 | 110 | /** 111 | * Checkout a branch (create an orphan if it doesn't exist on the remote). 112 | * @param {string} remote Remote alias. 113 | * @param {string} branch Branch name. 114 | * @return {Promise} A promise. 115 | */ 116 | Git.prototype.checkout = function (remote, branch) { 117 | const treeish = remote + '/' + branch; 118 | return this.exec('ls-remote', '--exit-code', '.', treeish).then( 119 | () => { 120 | // branch exists on remote, hard reset 121 | return this.exec('checkout', branch) 122 | .then(() => this.clean()) 123 | .then(() => this.reset(remote, branch)); 124 | }, 125 | (error) => { 126 | if (error instanceof ProcessError && error.code === 2) { 127 | // branch doesn't exist, create an orphan 128 | return this.exec('checkout', '--orphan', branch); 129 | } else { 130 | // unhandled error 131 | throw error; 132 | } 133 | }, 134 | ); 135 | }; 136 | 137 | /** 138 | * Remove all unversioned files. 139 | * @param {string | Array} files Files argument. 140 | * @return {Promise} A promise. 141 | */ 142 | Git.prototype.rm = function (files) { 143 | if (!Array.isArray(files)) { 144 | files = [files]; 145 | } 146 | return this.exec('rm', '--ignore-unmatch', '-r', '-f', '--', ...files); 147 | }; 148 | 149 | /** 150 | * Add files. 151 | * @param {string | Array} files Files argument. 152 | * @return {Promise} A promise. 153 | */ 154 | Git.prototype.add = function (files) { 155 | if (!Array.isArray(files)) { 156 | files = [files]; 157 | } 158 | return this.exec('add', ...files); 159 | }; 160 | 161 | /** 162 | * Commit (if there are any changes). 163 | * @param {string} message Commit message. 164 | * @return {Promise} A promise. 165 | */ 166 | Git.prototype.commit = function (message) { 167 | return this.exec('diff-index', '--quiet', 'HEAD').catch(() => 168 | this.exec('commit', '-m', message), 169 | ); 170 | }; 171 | 172 | /** 173 | * Add tag 174 | * @param {string} name Name of tag. 175 | * @return {Promise} A promise. 176 | */ 177 | Git.prototype.tag = function (name) { 178 | return this.exec('tag', name); 179 | }; 180 | 181 | /** 182 | * Push a branch. 183 | * @param {string} remote Remote alias. 184 | * @param {string} branch Branch name. 185 | * @param {boolean} force Force push. 186 | * @return {Promise} A promise. 187 | */ 188 | Git.prototype.push = function (remote, branch, force) { 189 | const args = ['push', '--tags', remote, branch]; 190 | if (force) { 191 | args.push('--force'); 192 | } 193 | return this.exec.apply(this, args); 194 | }; 195 | 196 | /** 197 | * Get the URL for a remote. 198 | * @param {string} remote Remote alias. 199 | * @return {Promise} A promise for the remote URL. 200 | */ 201 | Git.prototype.getRemoteUrl = function (remote) { 202 | return this.exec('config', '--get', 'remote.' + remote + '.url') 203 | .then((git) => { 204 | const repo = git.output && git.output.split(/[\n\r]/).shift(); 205 | if (repo) { 206 | return repo; 207 | } else { 208 | throw new Error( 209 | 'Failed to get repo URL from options or current directory.', 210 | ); 211 | } 212 | }) 213 | .catch((err) => { 214 | throw new Error( 215 | 'Failed to get remote.' + 216 | remote + 217 | '.url (task must either be ' + 218 | 'run in a git repository with a configured ' + 219 | remote + 220 | ' remote ' + 221 | 'or must be configured with the "repo" option).', 222 | ); 223 | }); 224 | }; 225 | 226 | /** 227 | * Delete ref to remove branch history 228 | * @param {string} branch The branch name. 229 | * @return {Promise} A promise. The promise will be resolved with this instance 230 | * or rejected with an error. 231 | */ 232 | Git.prototype.deleteRef = function (branch) { 233 | return this.exec('update-ref', '-d', 'refs/heads/' + branch); 234 | }; 235 | 236 | /** 237 | * Clone a repo into the given dir if it doesn't already exist. 238 | * @param {string} repo Repository URL. 239 | * @param {string} dir Target directory. 240 | * @param {string} branch Branch name. 241 | * @param {options} options All options. 242 | * @return {Promise} A promise. 243 | */ 244 | Git.clone = function clone(repo, dir, branch, options) { 245 | return fs.exists(dir).then((exists) => { 246 | if (exists) { 247 | return Promise.resolve(new Git(dir, options.git)); 248 | } else { 249 | return fs.mkdirp(path.dirname(path.resolve(dir))).then(() => { 250 | const args = [ 251 | 'clone', 252 | repo, 253 | dir, 254 | '--branch', 255 | branch, 256 | '--single-branch', 257 | '--origin', 258 | options.remote, 259 | '--depth', 260 | options.depth, 261 | ]; 262 | return spawn(options.git, args) 263 | .catch((err) => { 264 | // try again without branch or depth options 265 | return spawn(options.git, [ 266 | 'clone', 267 | repo, 268 | dir, 269 | '--origin', 270 | options.remote, 271 | ]); 272 | }) 273 | .then(() => new Git(dir, options.git)); 274 | }); 275 | } 276 | }); 277 | }; 278 | 279 | module.exports = Git; 280 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const util = require('util'); 3 | const filenamify = require('filenamify'); 4 | const findCacheDir = require('find-cache-dir'); 5 | const fs = require('fs-extra'); 6 | const globby = require('globby'); 7 | const Git = require('./git.js'); 8 | const copy = require('./util.js').copy; 9 | const getUser = require('./util.js').getUser; 10 | 11 | const log = util.debuglog('gh-pages'); 12 | 13 | /** 14 | * Get the cache directory. 15 | * @param {string} [optPath] Optional path. 16 | * @return {string} The full path to the cache directory. 17 | */ 18 | function getCacheDir(optPath) { 19 | const dir = findCacheDir({name: 'gh-pages'}); 20 | if (!optPath) { 21 | return dir; 22 | } 23 | 24 | return path.join(dir, filenamify(optPath)); 25 | } 26 | 27 | /** 28 | * Clean the cache directory. 29 | */ 30 | exports.clean = function clean() { 31 | fs.removeSync(getCacheDir()); 32 | }; 33 | 34 | exports.defaults = { 35 | dest: '.', 36 | add: false, 37 | git: 'git', 38 | depth: 1, 39 | dotfiles: false, 40 | branch: 'gh-pages', 41 | remote: 'origin', 42 | src: '**/*', 43 | remove: '.', 44 | push: true, 45 | history: true, 46 | message: 'Updates', 47 | silent: false, 48 | }; 49 | 50 | exports.getCacheDir = getCacheDir; 51 | 52 | function getRepo(options) { 53 | if (options.repo) { 54 | return Promise.resolve(options.repo); 55 | } else { 56 | const git = new Git(process.cwd(), options.git); 57 | return git.getRemoteUrl(options.remote); 58 | } 59 | } 60 | 61 | /** 62 | * Push a git branch to a remote (pushes gh-pages by default). 63 | * @param {string} basePath The base path. 64 | * @param {Object} config Publish options. 65 | * @param {Function} callback Callback. 66 | * @return {Promise} A promise. 67 | */ 68 | exports.publish = function publish(basePath, config, callback) { 69 | if (typeof config === 'function') { 70 | callback = config; 71 | config = {}; 72 | } 73 | 74 | const options = Object.assign({}, exports.defaults, config); 75 | 76 | // For backward compatibility before fixing #334 77 | if (options.only) { 78 | options.remove = options.only; 79 | } 80 | 81 | if (!callback) { 82 | callback = function (err) { 83 | if (err) { 84 | log(err.message); 85 | } 86 | }; 87 | } 88 | 89 | function done(err) { 90 | try { 91 | callback(err); 92 | } catch (err2) { 93 | log('Publish callback threw: %s', err2.message); 94 | } 95 | } 96 | 97 | try { 98 | if (!fs.statSync(basePath).isDirectory()) { 99 | const err = new Error('The "base" option must be an existing directory'); 100 | done(err); 101 | return Promise.reject(err); 102 | } 103 | } catch (err) { 104 | done(err); 105 | return Promise.reject(err); 106 | } 107 | 108 | const files = globby 109 | .sync(options.src, { 110 | cwd: basePath, 111 | dot: options.dotfiles, 112 | }) 113 | .filter((file) => { 114 | return !fs.statSync(path.join(basePath, file)).isDirectory(); 115 | }); 116 | 117 | if (!Array.isArray(files) || files.length === 0) { 118 | done( 119 | new Error('The pattern in the "src" property didn\'t match any files.'), 120 | ); 121 | return; 122 | } 123 | 124 | let repoUrl; 125 | let userPromise; 126 | if (options.user) { 127 | userPromise = Promise.resolve(options.user); 128 | } else { 129 | userPromise = getUser(); 130 | } 131 | return userPromise.then((user) => 132 | getRepo(options) 133 | .then((repo) => { 134 | repoUrl = repo; 135 | const clone = getCacheDir(repo); 136 | log('Cloning %s into %s', repo, clone); 137 | return Git.clone(repo, clone, options.branch, options); 138 | }) 139 | .then((git) => { 140 | return git.getRemoteUrl(options.remote).then((url) => { 141 | if (url !== repoUrl) { 142 | const message = 143 | 'Remote url mismatch. Got "' + 144 | url + 145 | '" ' + 146 | 'but expected "' + 147 | repoUrl + 148 | '" in ' + 149 | git.cwd + 150 | '. Try running the `gh-pages-clean` script first.'; 151 | throw new Error(message); 152 | } 153 | return git; 154 | }); 155 | }) 156 | .then((git) => { 157 | // only required if someone mucks with the checkout between builds 158 | log('Cleaning'); 159 | return git.clean(); 160 | }) 161 | .then((git) => { 162 | log('Fetching %s', options.remote); 163 | return git.fetch(options.remote); 164 | }) 165 | .then((git) => { 166 | log('Checking out %s/%s ', options.remote, options.branch); 167 | return git.checkout(options.remote, options.branch); 168 | }) 169 | .then((git) => { 170 | if (!options.history) { 171 | return git.deleteRef(options.branch); 172 | } else { 173 | return git; 174 | } 175 | }) 176 | .then((git) => { 177 | if (options.add) { 178 | return git; 179 | } 180 | 181 | log('Removing files'); 182 | const files = globby 183 | .sync(options.remove, { 184 | cwd: path.join(git.cwd, options.dest), 185 | }) 186 | .map((file) => path.join(options.dest, file)); 187 | if (files.length > 0) { 188 | return git.rm(files); 189 | } else { 190 | return git; 191 | } 192 | }) 193 | .then((git) => { 194 | if (options.nojekyll) { 195 | log('Creating .nojekyll'); 196 | fs.createFileSync(path.join(git.cwd, '.nojekyll')); 197 | } 198 | if (options.cname) { 199 | log('Creating CNAME for %s', options.cname); 200 | fs.writeFileSync(path.join(git.cwd, 'CNAME'), options.cname); 201 | } 202 | log('Copying files'); 203 | return copy(files, basePath, path.join(git.cwd, options.dest)).then( 204 | function () { 205 | return git; 206 | }, 207 | ); 208 | }) 209 | .then((git) => { 210 | return Promise.resolve( 211 | options.beforeAdd && options.beforeAdd(git), 212 | ).then(() => git); 213 | }) 214 | .then((git) => { 215 | log('Adding all'); 216 | return git.add('.'); 217 | }) 218 | .then((git) => { 219 | if (!user) { 220 | return git; 221 | } 222 | return git.exec('config', 'user.email', user.email).then(() => { 223 | if (!user.name) { 224 | return git; 225 | } 226 | return git.exec('config', 'user.name', user.name); 227 | }); 228 | }) 229 | .then((git) => { 230 | log('Committing'); 231 | return git.commit(options.message); 232 | }) 233 | .then((git) => { 234 | if (options.tag) { 235 | log('Tagging'); 236 | return git.tag(options.tag).catch((error) => { 237 | // tagging failed probably because this tag alredy exists 238 | log(error); 239 | log('Tagging failed, continuing'); 240 | return git; 241 | }); 242 | } else { 243 | return git; 244 | } 245 | }) 246 | .then((git) => { 247 | if (options.push) { 248 | log('Pushing'); 249 | return git.push(options.remote, options.branch, !options.history); 250 | } else { 251 | return git; 252 | } 253 | }) 254 | .then( 255 | () => done(), 256 | (error) => { 257 | if (options.silent) { 258 | error = new Error( 259 | 'Unspecified error (run without silent option for detail)', 260 | ); 261 | } 262 | done(error); 263 | }, 264 | ), 265 | ); 266 | }; 267 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const async = require('async'); 3 | const fs = require('fs-extra'); 4 | const Git = require('./git.js'); 5 | 6 | /** 7 | * Generate a list of unique directory paths given a list of file paths. 8 | * @param {Array} files List of file paths. 9 | * @return {Array} List of directory paths. 10 | */ 11 | function uniqueDirs(files) { 12 | const dirs = new Set(); 13 | files.forEach((filepath) => { 14 | const parts = path.dirname(filepath).split(path.sep); 15 | let partial = parts[0] || '/'; 16 | dirs.add(partial); 17 | for (let i = 1, ii = parts.length; i < ii; ++i) { 18 | partial = path.join(partial, parts[i]); 19 | dirs.add(partial); 20 | } 21 | }); 22 | return Array.from(dirs); 23 | } 24 | 25 | /** 26 | * Sort function for paths. Sorter paths come first. Paths of equal length are 27 | * sorted alphanumerically in path segment order. 28 | * @param {string} a First path. 29 | * @param {string} b Second path. 30 | * @return {number} Comparison. 31 | */ 32 | function byShortPath(a, b) { 33 | const aParts = a.split(path.sep); 34 | const bParts = b.split(path.sep); 35 | const aLength = aParts.length; 36 | const bLength = bParts.length; 37 | let cmp = 0; 38 | if (aLength < bLength) { 39 | cmp = -1; 40 | } else if (aLength > bLength) { 41 | cmp = 1; 42 | } else { 43 | let aPart, bPart; 44 | for (let i = 0; i < aLength; ++i) { 45 | aPart = aParts[i]; 46 | bPart = bParts[i]; 47 | if (aPart < bPart) { 48 | cmp = -1; 49 | break; 50 | } else if (aPart > bPart) { 51 | cmp = 1; 52 | break; 53 | } 54 | } 55 | } 56 | return cmp; 57 | } 58 | exports.byShortPath = byShortPath; 59 | 60 | /** 61 | * Generate a list of directories to create given a list of file paths. 62 | * @param {Array} files List of file paths. 63 | * @return {Array} List of directory paths ordered by path length. 64 | */ 65 | function dirsToCreate(files) { 66 | return uniqueDirs(files).sort(byShortPath); 67 | } 68 | exports.copy = function (files, base, dest) { 69 | return new Promise((resolve, reject) => { 70 | const pairs = []; 71 | const destFiles = []; 72 | files.forEach((file) => { 73 | const src = path.resolve(base, file); 74 | const relative = path.relative(base, src); 75 | const target = path.join(dest, relative); 76 | pairs.push({ 77 | src: src, 78 | dest: target, 79 | }); 80 | destFiles.push(target); 81 | }); 82 | 83 | async.eachSeries(dirsToCreate(destFiles), makeDir, (err) => { 84 | if (err) { 85 | return reject(err); 86 | } 87 | async.each(pairs, copyFile, (err) => { 88 | if (err) { 89 | return reject(err); 90 | } else { 91 | return resolve(); 92 | } 93 | }); 94 | }); 95 | }); 96 | }; 97 | 98 | exports.copyFile = copyFile; 99 | 100 | exports.dirsToCreate = dirsToCreate; 101 | 102 | /** 103 | * Copy a file. 104 | * @param {Object} obj Object with src and dest properties. 105 | * @param {function(Error):void} callback Callback 106 | */ 107 | function copyFile(obj, callback) { 108 | let called = false; 109 | function done(err) { 110 | if (!called) { 111 | called = true; 112 | callback(err); 113 | } 114 | } 115 | 116 | const read = fs.createReadStream(obj.src); 117 | read.on('error', (err) => { 118 | done(err); 119 | }); 120 | 121 | const write = fs.createWriteStream(obj.dest); 122 | write.on('error', (err) => { 123 | done(err); 124 | }); 125 | write.on('close', () => { 126 | done(); 127 | }); 128 | 129 | read.pipe(write); 130 | } 131 | 132 | /** 133 | * Make directory, ignoring errors if directory already exists. 134 | * @param {string} path Directory path. 135 | * @param {function(Error):void} callback Callback. 136 | */ 137 | function makeDir(path, callback) { 138 | fs.mkdir(path, (err) => { 139 | if (err) { 140 | // check if directory exists 141 | fs.stat(path, (err2, stat) => { 142 | if (err2 || !stat.isDirectory()) { 143 | callback(err); 144 | } else { 145 | callback(); 146 | } 147 | }); 148 | } else { 149 | callback(); 150 | } 151 | }); 152 | } 153 | 154 | /** 155 | * Copy a list of files. 156 | * @param {Array} files Files to copy. 157 | * @param {string} base Base directory. 158 | * @param {string} dest Destination directory. 159 | * @return {Promise} A promise. 160 | */ 161 | 162 | exports.getUser = function (cwd) { 163 | return Promise.all([ 164 | new Git(cwd).exec('config', 'user.name'), 165 | new Git(cwd).exec('config', 'user.email'), 166 | ]) 167 | .then((results) => { 168 | return {name: results[0].output.trim(), email: results[1].output.trim()}; 169 | }) 170 | .catch((err) => { 171 | // git config exits with 1 if name or email is not set 172 | return null; 173 | }); 174 | }; 175 | 176 | exports.uniqueDirs = uniqueDirs; 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gh-pages", 3 | "version": "6.3.0", 4 | "description": "Publish to a gh-pages branch on GitHub (or any other branch on any other remote)", 5 | "keywords": [ 6 | "git", 7 | "gh-pages", 8 | "github" 9 | ], 10 | "author": { 11 | "name": "Tim Schaub", 12 | "url": "http://tschaub.net/" 13 | }, 14 | "license": "MIT", 15 | "homepage": "https://github.com/tschaub/gh-pages", 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/tschaub/gh-pages.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/tschaub/gh-pages/issues" 22 | }, 23 | "main": "lib/index.js", 24 | "scripts": { 25 | "lint": "eslint lib test bin plugin.js", 26 | "pretest": "npm run lint", 27 | "test": "mocha --recursive test" 28 | }, 29 | "files": [ 30 | "lib", 31 | "bin" 32 | ], 33 | "engines": { 34 | "node": ">=10" 35 | }, 36 | "dependencies": { 37 | "async": "^3.2.4", 38 | "commander": "^14.0.0", 39 | "email-addresses": "^5.0.0", 40 | "filenamify": "^4.3.0", 41 | "find-cache-dir": "^3.3.1", 42 | "fs-extra": "^11.1.1", 43 | "globby": "^11.1.0" 44 | }, 45 | "devDependencies": { 46 | "chai": "^4.3.7", 47 | "dir-compare": "^5.0.0", 48 | "eslint": "^9.16.0", 49 | "eslint-config-tschaub": "^15.2.0", 50 | "mocha": "^11.0.1", 51 | "sinon": "^20.0.0", 52 | "tmp": "^0.2.1" 53 | }, 54 | "bin": { 55 | "gh-pages": "bin/gh-pages.js", 56 | "gh-pages-clean": "bin/gh-pages-clean.js" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ghPages = require('./lib/index.js'); 3 | 4 | module.exports = function (pluginConfig, config, callback) { 5 | ghPages.publish(path.join(process.cwd(), config.basePath), config, callback); 6 | }; 7 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # gh-pages 3 | 4 | Publish files to a `gh-pages` branch on GitHub (or any other branch anywhere else). 5 | 6 | ## Getting Started 7 | 8 | ```shell 9 | npm install gh-pages --save-dev 10 | ``` 11 | 12 | This module requires Git >= 1.9 and Node > 14. 13 | 14 | ## Basic Usage 15 | 16 | ```js 17 | var ghpages = require('gh-pages'); 18 | 19 | ghpages.publish('dist', function(err) {}); 20 | ``` 21 | 22 | 23 | ## `publish` 24 | 25 | ```js 26 | ghpages.publish(dir, callback); 27 | // or... 28 | ghpages.publish(dir, options, callback); 29 | ``` 30 | 31 | Calling this function will create a temporary clone of the current repository, create a `gh-pages` branch if one doesn't already exist, copy over all files from the base path, or only those that match patterns from the optional `src` configuration, commit all changes, and push to the `origin` remote. 32 | 33 | If a `gh-pages` branch already exists, it will be updated with all commits from the remote before adding any commits from the provided `src` files. 34 | 35 | **Note** that any files in the `gh-pages` branch that are *not* in the `src` files **will be removed**. See the [`add` option](#optionsadd) if you don't want any of the existing files removed. 36 | 37 | 38 | ### `dir` 39 | * type: `string` 40 | 41 | The base directory for all source files (those listed in the `src` config property). 42 | 43 | Example use: 44 | 45 | ```js 46 | /** 47 | * Given the following directory structure: 48 | * 49 | * dist/ 50 | * index.html 51 | * js/ 52 | * site.js 53 | * 54 | * The usage below will create a `gh-pages` branch that looks like this: 55 | * 56 | * index.html 57 | * js/ 58 | * site.js 59 | * 60 | */ 61 | ghpages.publish('dist', callback); 62 | ``` 63 | 64 | 65 | ### Options 66 | 67 | The default options work for simple cases. The options described below let you push to alternate branches, customize your commit messages and more. 68 | 69 | 70 | #### options.src 71 | * type: `string|Array` 72 | * default: `'**/*'` 73 | 74 | The [minimatch](https://github.com/isaacs/minimatch) pattern or array of patterns is used to select which files should be published. 75 | 76 | 77 | #### options.branch 78 | * type: `string` 79 | * default: `'gh-pages'` 80 | * `-b | --branch ` 81 | 82 | The name of the branch you'll be pushing to. The default uses GitHub's `gh-pages` branch, but this can be configured to push to any branch on any remote. 83 | 84 | Example use of the `branch` option: 85 | 86 | ```js 87 | /** 88 | * This task pushes to the `master` branch of the configured `repo`. 89 | */ 90 | ghpages.publish('dist', { 91 | branch: 'master', 92 | repo: 'https://example.com/other/repo.git' 93 | }, callback); 94 | ``` 95 | 96 | 97 | #### options.dest 98 | * type: `string` 99 | * default: `'.'` 100 | 101 | The destination folder within the destination branch. By default, all files are published to the root of the repository. 102 | 103 | Example use of the `dest` option: 104 | 105 | ```js 106 | /** 107 | * Place content in the static/project subdirectory of the target 108 | * branch. 109 | */ 110 | ghpages.publish('dist', { 111 | dest: 'static/project' 112 | }, callback); 113 | ``` 114 | 115 | #### options.dotfiles 116 | * type: `boolean` 117 | * default: `false` 118 | 119 | Include dotfiles. By default, files starting with `.` are ignored unless they are explicitly provided in the `src` array. If you want to also include dotfiles that otherwise match your `src` patterns, set `dotfiles: true` in your options. 120 | 121 | Example use of the `dotfiles` option: 122 | 123 | ```js 124 | /** 125 | * The usage below will push dotfiles (directories and files) 126 | * that otherwise match the `src` pattern. 127 | */ 128 | ghpages.publish('dist', {dotfiles: true}, callback); 129 | ``` 130 | 131 | #### options.nojekyll 132 | * type: `boolean` 133 | * default: `false` 134 | 135 | Write out a `.nojekyll` file to [bypass Jekyll on GitHub Pages](https://github.blog/2009-12-29-bypassing-jekyll-on-github-pages/). 136 | 137 | Example use of the `nojekyll` option: 138 | 139 | ```js 140 | /** 141 | * The usage below will add a `.nojekyll` file to the output. 142 | */ 143 | ghpages.publish('dist', {nojekyll: true}, callback); 144 | ``` 145 | 146 | #### options.cname 147 | * type: `string` 148 | 149 | Write out a `CNAME` file with a custom domain name. 150 | 151 | Example use of the `cname` option: 152 | 153 | ```js 154 | /** 155 | * The usage below will add a `CNAME` file to the output. 156 | */ 157 | ghpages.publish('dist', {cname: 'custom-domain.com'}, callback); 158 | ``` 159 | 160 | 161 | #### options.add 162 | * type: `boolean` 163 | * default: `false` 164 | 165 | Only add, and never remove existing files. By default, existing files in the target branch are removed before adding the ones from your `src` config. If you want the task to add new `src` files but leave existing ones untouched, set `add: true` in your options. 166 | 167 | Example use of the `add` option: 168 | 169 | ```js 170 | /** 171 | * The usage below will only add files to the `gh-pages` branch, never removing 172 | * any existing files (even if they don't exist in the `src` config). 173 | */ 174 | ghpages.publish('dist', {add: true}, callback); 175 | ``` 176 | 177 | 178 | #### options.repo 179 | * type: `string` 180 | * default: url for the origin remote of the current dir (assumes a git repository) 181 | * `-r | --repo ` 182 | 183 | By default, `gh-pages` assumes that the current working directory is a git repository, and that you want to push changes to the `origin` remote. 184 | 185 | If instead your script is not in a git repository, or if you want to push to another repository, you can provide the repository URL in the `repo` option. 186 | 187 | Example use of the `repo` option: 188 | 189 | ```js 190 | /** 191 | * If the current directory is not a clone of the repository you want to work 192 | * with, set the URL for the repository in the `repo` option. This usage will 193 | * push all files in the `src` config to the `gh-pages` branch of the `repo`. 194 | */ 195 | ghpages.publish('dist', { 196 | repo: 'https://example.com/other/repo.git' 197 | }, callback); 198 | ``` 199 | 200 | 201 | #### options.remote 202 | * type: `string` 203 | * default: `'origin'` 204 | 205 | The name of the remote you'll be pushing to. The default is your `'origin'` remote, but this can be configured to push to any remote. 206 | 207 | Example use of the `remote` option: 208 | 209 | ```js 210 | /** 211 | * This task pushes to the `gh-pages` branch of of your `upstream` remote. 212 | */ 213 | ghpages.publish('dist', { 214 | remote: 'upstream' 215 | }, callback); 216 | ``` 217 | 218 | 219 | #### options.tag 220 | * type: `string` 221 | * default: `''` 222 | 223 | Create a tag after committing changes on the target branch. By default, no tag is created. To create a tag, provide the tag name as the option value. 224 | 225 | 226 | #### options.message 227 | * type: `string` 228 | * default: `'Updates'` 229 | 230 | The commit message for all commits. 231 | 232 | Example use of the `message` option: 233 | 234 | ```js 235 | /** 236 | * This adds commits with a custom message. 237 | */ 238 | ghpages.publish('dist', { 239 | message: 'Auto-generated commit' 240 | }, callback); 241 | ``` 242 | 243 | 244 | #### options.user 245 | * type: `Object` 246 | * default: `null` 247 | 248 | If you are running the `gh-pages` task in a repository without a `user.name` or `user.email` git config properties (or on a machine without these global config properties), you must provide user info before git allows you to commit. The `options.user` object accepts `name` and `email` string values to identify the committer. 249 | 250 | Example use of the `user` option: 251 | 252 | ```js 253 | ghpages.publish('dist', { 254 | user: { 255 | name: 'Joe Code', 256 | email: 'coder@example.com' 257 | } 258 | }, callback); 259 | ``` 260 | 261 | #### options.remove 262 | * type: `string` 263 | * default: `'.'` 264 | 265 | Removes files that match the given pattern (Ignored if used together with 266 | `--add`). By default, `gh-pages` removes everything inside the target branch 267 | auto-generated directory before copying the new files from `dir`. 268 | 269 | Example use of the `remove` option: 270 | 271 | ```js 272 | ghpages.publish('dist', { 273 | remove: "*.json" 274 | }, callback); 275 | ``` 276 | 277 | 278 | #### options.push 279 | * type: `boolean` 280 | * default: `true` 281 | 282 | Push branch to remote. To commit only (with no push) set to `false`. 283 | 284 | Example use of the `push` option: 285 | 286 | ```js 287 | ghpages.publish('dist', {push: false}, callback); 288 | ``` 289 | 290 | 291 | #### options.history 292 | * type: `boolean` 293 | * default: `true` 294 | 295 | Push force new commit without parent history. 296 | 297 | Example use of the `history` option: 298 | 299 | ```js 300 | ghpages.publish('dist', {history: false}, callback); 301 | ``` 302 | 303 | 304 | #### options.silent 305 | * type: `boolean` 306 | * default: `false` 307 | 308 | Avoid showing repository URLs or other information in errors. 309 | 310 | Example use of the `silent` option: 311 | 312 | ```js 313 | /** 314 | * This configuration will avoid logging the GH_TOKEN if there is an error. 315 | */ 316 | ghpages.publish('dist', { 317 | repo: 'https://' + process.env.GH_TOKEN + '@github.com/user/private-repo.git', 318 | silent: true 319 | }, callback); 320 | ``` 321 | 322 | 323 | #### options.beforeAdd 324 | * type: `function` 325 | * default: `null` 326 | 327 | Custom callback that is executed right before `git add`. 328 | 329 | The CLI expects a file exporting the beforeAdd function 330 | 331 | ```bash 332 | gh-pages --before-add ./cleanup.js 333 | ``` 334 | 335 | Example use of the `beforeAdd` option: 336 | 337 | ```js 338 | /** 339 | * beforeAdd makes most sense when `add` option is active 340 | * Assuming we want to keep everything on the gh-pages branch 341 | * but remove just `some-outdated-file.txt` 342 | */ 343 | ghpages.publish('dist', { 344 | add: true, 345 | async beforeAdd(git) { 346 | return git.rm('./some-outdated-file.txt'); 347 | } 348 | }, callback); 349 | ``` 350 | 351 | 352 | #### options.git 353 | * type: `string` 354 | * default: `'git'` 355 | 356 | Your `git` executable. 357 | 358 | Example use of the `git` option: 359 | 360 | ```js 361 | /** 362 | * If `git` is not on your path, provide the path as shown below. 363 | */ 364 | ghpages.publish('dist', { 365 | git: '/path/to/git' 366 | }, callback); 367 | ``` 368 | 369 | ## Command Line Utility 370 | 371 | Installing the package creates a `gh-pages` command line utility. Run `gh-pages --help` to see a list of supported options. 372 | 373 | With a local install of `gh-pages`, you can set up a package script with something like the following: 374 | 375 | ```shell 376 | "scripts": { 377 | "deploy": "gh-pages -d dist" 378 | } 379 | ``` 380 | 381 | And then to publish everything from your `dist` folder to your `gh-pages` branch, you'd run this: 382 | 383 | ```shell 384 | npm run deploy 385 | ``` 386 | 387 | ## GitHub Pages Project Sites 388 | 389 | There are three types of GitHub Pages sites: [project, user, and organization](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages#types-of-github-pages-sites). Since project sites are not hosted on the root `.github.io` domain and instead under a URL path based on the repository name, they often require configuration tweaks for various build tools and frameworks. If not configured properly, a browser will usually log `net::ERR_ABORTED 404` errors when looking for compiled assets. 390 | 391 | Examples: 392 | - Create React App (which uses webpack under the hood) [requires the user to set a `"homepage"` property in their `package.json` so that built assets are referenced correctly in the final compiled HTML](https://create-react-app.dev/docs/deployment/#building-for-relative-paths). 393 | - This [has been often been thought of as an issue with `gh-pages`](https://github.com/tschaub/gh-pages/issues/285#issuecomment-805321474), though this package isn't able to control a project's build configuration. 394 | - Vite [requires a `"base"` property in its `vite.config.js`](https://vitejs.dev/guide/static-deploy.html#github-pages) 395 | - Next.js [requires a `"basePath"` property in its `next.config.js`](https://nextjs.org/docs/pages/api-reference/next-config-js/basePath) 396 | 397 | When using a project site, be sure to read the documentation for your particular build tool or framework to learn how to configure correct asset paths. 398 | 399 | ## Debugging 400 | 401 | To get additional output from the `gh-pages` script, set `NODE_DEBUG=gh-pages`. For example: 402 | 403 | ```shell 404 | NODE_DEBUG=gh-pages npm run deploy 405 | ``` 406 | 407 | ## Dependencies 408 | 409 | Note that this plugin requires Git 1.9 or higher (because it uses the `--exit-code` option for `git ls-remote`). If you'd like to see this working with earlier versions of Git, please [open an issue](https://github.com/tschaub/gh-pages/issues). 410 | 411 | ![Test Status](https://github.com/tschaub/gh-pages/workflows/Test/badge.svg) 412 | 413 | ## Tips 414 | 415 | ### when get error `branch already exists` 416 | ``` 417 | { ProcessError: fatal: A branch named 'gh-pages' already exists. 418 | 419 | at ChildProcess. (~/node_modules/gh-pages/lib/git.js:42:16) 420 | at ChildProcess.emit (events.js:180:13) 421 | at maybeClose (internal/child_process.js:936:16) 422 | at Process.ChildProcess._handle.onexit (internal/child_process.js:220:5) 423 | code: 128, 424 | message: 'fatal: A branch named \'gh-pages\' already exists.\n', 425 | name: 'ProcessError' } 426 | ``` 427 | 428 | The `gh-pages` module writes temporary files to a `node_modules/.cache/gh-pages` directory. The location of this directory can be customized by setting the `CACHE_DIR` environment variable. 429 | 430 | If `gh-pages` fails, you may find that you need to manually clean up the cache directory. To remove the cache directory, run `node_modules/gh-pages/bin/gh-pages-clean` or remove `node_modules/.cache/gh-pages`. 431 | 432 | ### Deploying to github pages with custom domain 433 | 434 | Use the `--cname` option to create a `CNAME` file with the name of your custom domain. See [the GitHub docs](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site) for more detail. 435 | 436 | ``` 437 | gh-pages -d build --cname custom-domain.com" 438 | ``` 439 | 440 | ### Deploying with GitHub Actions 441 | 442 | In order to deploy with GitHub Actions, you will need to define a user and set the git repository for the process. See the example step below 443 | 444 | ```yaml 445 | - name: Deploy with gh-pages 446 | run: | 447 | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 448 | npx gh-pages -d build -u "github-actions-bot " 449 | env: 450 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 451 | ``` 452 | 453 | The `secrets.GITHUB_TOKEN` is provided automatically as part of the GitHub Action and does not require any further configuration, but simply needs to be passed in as an environmental variable to the step. `GITHUB_REPOSITORY` is the owner and repository name and is also passed in automatically, but does not need to be added to the `env` list. 454 | 455 | See [Issue #345](https://github.com/tschaub/gh-pages/issues/345) for more information 456 | 457 | #### Deploying with GitHub Actions and a named script 458 | 459 | If you are using a named script in the `package.json` file to deploy, you will need to ensure you pass the variables properly to the wrapped `gh-pages` script. Given the `package.json` script below: 460 | 461 | ```json 462 | "scripts": { 463 | "deploy": "gh-pages -d build" 464 | } 465 | ``` 466 | 467 | You will need to utilize the `--` option to pass any additional arguments: 468 | 469 | ```yaml 470 | - name: Deploy with gh-pages 471 | run: | 472 | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 473 | npm run deploy -- -u "github-actions-bot " 474 | env: 475 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 476 | ``` 477 | 478 | See [Pull Request #368](https://github.com/tschaub/gh-pages/pull/368) for more information. 479 | -------------------------------------------------------------------------------- /tasks/changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Generate a Markdown-formatted changelog from merge commits. 4 | # 5 | 6 | set -o errexit 7 | 8 | # 9 | # Regex to match the standard pull request commit message. Creates capture 10 | # groups for pull request number, GitHub username, and commit message body. 11 | # 12 | MERGE_RE=Merge\ pull\ request\ #\([0-9]+\)\ from\ \([^/]+\)\/[^\ ]+\ \(.*\) 13 | 14 | GITHUB_URL=https://github.com 15 | PULLS_URL=${GITHUB_URL}/tschaub/gh-pages/pull 16 | 17 | display_usage() { 18 | cat <<-EOF 19 | 20 | Usage: ${1} 21 | 22 | Creates a Markdown-formatted changelog given a revision range. 23 | 24 | E.g. 25 | ${1} v3.0.0.. > changelog/v3.1.0.md 26 | 27 | See git-log(1) for details on the revision range syntax. 28 | 29 | EOF 30 | } 31 | 32 | # 33 | # Scan the git log for merge commit messages and output Markdown. This only 34 | # follows the first parent of merge commits to avoid merges within a topic 35 | # branch (instead only showing merges to main). 36 | # 37 | main() { 38 | git log --first-parent --format='%s %b' ${1} | 39 | { 40 | while read l; do 41 | if [[ ${l} =~ ${MERGE_RE} ]] ; then 42 | number="${BASH_REMATCH[1]}" 43 | author="${BASH_REMATCH[2]}" 44 | summary="${BASH_REMATCH[3]}" 45 | echo " * [#${number}](${PULLS_URL}/${number}) - ${summary} ([@${author}](${GITHUB_URL}/${author}))" 46 | fi 47 | done 48 | } 49 | } 50 | 51 | if test ${#} -ne 1; then 52 | display_usage ${0} 53 | exit 1 54 | else 55 | main ${1} 56 | fi 57 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/bin/fixtures/beforeAdd.js: -------------------------------------------------------------------------------- 1 | module.exports = function myBeforeAdd() { 2 | /* noop */ 3 | }; 4 | -------------------------------------------------------------------------------- /test/bin/gh-pages.spec.js: -------------------------------------------------------------------------------- 1 | const {afterEach, beforeEach, describe, it} = require('mocha'); 2 | const sinon = require('sinon'); 3 | const cli = require('../../bin/gh-pages.js'); 4 | const ghpages = require('../../lib/index.js'); 5 | const assert = require('../helper.js').assert; 6 | const beforeAdd = require('./fixtures/beforeAdd.js'); 7 | 8 | describe('gh-pages', () => { 9 | describe('main', () => { 10 | beforeEach(() => { 11 | sinon 12 | .stub(ghpages, 'publish') 13 | .callsFake((basePath, config, callback) => callback()); 14 | }); 15 | 16 | afterEach(() => { 17 | ghpages.publish.restore(); 18 | }); 19 | 20 | const scenarios = [ 21 | { 22 | args: ['--dist', 'lib'], 23 | dist: 'lib', 24 | config: ghpages.defaults, 25 | }, 26 | { 27 | args: ['--dist', 'lib', '-n'], 28 | dist: 'lib', 29 | config: {push: false}, 30 | }, 31 | { 32 | args: ['--dist', 'lib', '-f'], 33 | dist: 'lib', 34 | config: {history: false}, 35 | }, 36 | { 37 | args: ['--dist', 'lib', '-x'], 38 | dist: 'lib', 39 | config: {silent: true}, 40 | }, 41 | { 42 | args: ['--dist', 'lib', '--dotfiles'], 43 | dist: 'lib', 44 | config: {dotfiles: true}, 45 | }, 46 | { 47 | args: ['--dist', 'lib', '--nojekyll'], 48 | dist: 'lib', 49 | config: {nojekyll: true}, 50 | }, 51 | { 52 | args: ['--dist', 'lib', '--cname', 'CNAME'], 53 | dist: 'lib', 54 | config: {cname: 'CNAME'}, 55 | }, 56 | { 57 | args: ['--dist', 'lib', '--dest', 'target'], 58 | dist: 'lib', 59 | config: {dest: 'target'}, 60 | }, 61 | { 62 | args: ['--dist', 'lib', '-a'], 63 | dist: 'lib', 64 | config: {add: true}, 65 | }, 66 | { 67 | args: ['--dist', 'lib', '--git', 'path/to/git'], 68 | dist: 'lib', 69 | config: {git: 'path/to/git'}, 70 | }, 71 | { 72 | args: ['--dist', 'lib', '--user', 'Full Name '], 73 | dist: 'lib', 74 | config: {user: {name: 'Full Name', email: 'email@example.com'}}, 75 | }, 76 | { 77 | args: ['--dist', 'lib', '--user', 'email@example.com'], 78 | dist: 'lib', 79 | config: {user: {name: null, email: 'email@example.com'}}, 80 | }, 81 | { 82 | args: ['--dist', 'lib', '-u', 'Full Name '], 83 | dist: 'lib', 84 | config: {user: {name: 'Full Name', email: 'email@example.com'}}, 85 | }, 86 | { 87 | args: [ 88 | '--dist', 89 | 'lib', 90 | '--before-add', 91 | require.resolve('./fixtures/beforeAdd'), 92 | ], 93 | dist: 'lib', 94 | config: {beforeAdd}, 95 | }, 96 | { 97 | args: ['--dist', 'lib', '-u', 'junk email'], 98 | dist: 'lib', 99 | error: 100 | 'Could not parse name and email from user option "junk email" (format should be "Your Name ")', 101 | }, 102 | ]; 103 | 104 | scenarios.forEach(({args, dist, config, error}) => { 105 | let title = args.join(' '); 106 | if (error) { 107 | title += ' (user error)'; 108 | } 109 | it(title, async () => { 110 | try { 111 | await cli(['node', 'gh-pages'].concat(args)); 112 | } catch (err) { 113 | if (!error) { 114 | throw err; 115 | } 116 | assert.equal(err.message, error); 117 | return; 118 | } 119 | 120 | if (error) { 121 | throw new Error(`Expected error "${error}" but got success`); 122 | } 123 | 124 | sinon.assert.calledWithMatch(ghpages.publish, dist, config); 125 | }); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const compare = require('dir-compare').compareSync; 4 | const fs = require('fs-extra'); 5 | const tmp = require('tmp'); 6 | const Git = require('../lib/git.js'); 7 | 8 | /** 9 | * Turn off maxListeners warning during the tests 10 | * See: https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n 11 | */ 12 | require('events').EventEmitter.prototype._maxListeners = 0; 13 | 14 | /** @type {boolean} */ 15 | chai.config.includeStack = true; 16 | 17 | /** 18 | * Chai's assert function configured to include stacks on failure. 19 | * @type {Function} 20 | */ 21 | exports.assert = chai.assert; 22 | 23 | const fixtures = path.join(__dirname, 'integration', 'fixtures'); 24 | 25 | /** 26 | * @return {Promise} A promise that resolves to the path. 27 | */ 28 | function mkdtemp() { 29 | return new Promise((resolve, reject) => { 30 | tmp.dir({unsafeCleanup: true}, (err, tmpPath) => { 31 | if (err) { 32 | return reject(err); 33 | } 34 | resolve(tmpPath); 35 | }); 36 | }); 37 | } 38 | 39 | /** 40 | * Creates a git repo with the contents of a fixture. 41 | * @param {string} fixtureName Name of fixture. 42 | * @param {Object} options Repo options. 43 | * @return {Promise} A promise for the path to the repo. 44 | */ 45 | function setupRepo(fixtureName, options) { 46 | const branch = options.branch || 'gh-pages'; 47 | const userEmail = (options.user && options.user.email) || 'user@email.com'; 48 | const userName = (options.user && options.user.name) || 'User Name'; 49 | return mkdtemp() 50 | .then((dir) => { 51 | const fixturePath = path.join(fixtures, fixtureName, 'remote'); 52 | return fs.copy(fixturePath, dir).then(() => new Git(dir)); 53 | }) 54 | .then((git) => git.init()) 55 | .then((git) => git.exec('config', 'user.email', userEmail)) 56 | .then((git) => git.exec('config', 'user.name', userName)) 57 | .then((git) => git.exec('checkout', '--orphan', branch)) 58 | .then((git) => git.add('.')) 59 | .then((git) => git.commit('Initial commit')) 60 | .then((git) => git.cwd); 61 | } 62 | 63 | /** 64 | * Creates a git repo with the contents of a fixture and pushes to a remote. 65 | * @param {string} fixtureName Name of the fixture. 66 | * @param {Object} options Repo options. 67 | * @return {Promise} A promise. 68 | */ 69 | function setupRemote(fixtureName, options) { 70 | const branch = options.branch || 'gh-pages'; 71 | return setupRepo(fixtureName, options).then((dir) => 72 | mkdtemp() 73 | .then((remote) => { 74 | return new Git(remote).exec('init', '--bare').then(() => remote); 75 | }) 76 | .then((remote) => { 77 | const git = new Git(dir); 78 | const url = 'file://' + remote; 79 | return git.exec('push', url, branch).then(() => url); 80 | }), 81 | ); 82 | } 83 | 84 | /** 85 | * @param {string} dir The dir. 86 | * @param {string} url The url. 87 | * @param {string} branch The branch. 88 | * @return {Promise} A promise. 89 | */ 90 | function assertContentsMatch(dir, url, branch) { 91 | return mkdtemp() 92 | .then((root) => { 93 | const clone = path.join(root, 'repo'); 94 | const options = {git: 'git', remote: 'origin', depth: 1}; 95 | return Git.clone(url, clone, branch, options); 96 | }) 97 | .then((git) => { 98 | const comparison = compare(dir, git.cwd, {excludeFilter: '.git'}); 99 | if (comparison.same) { 100 | return true; 101 | } else { 102 | const message = comparison.diffSet 103 | .map((entry) => { 104 | const state = { 105 | equal: '==', 106 | left: '->', 107 | right: '<-', 108 | distinct: '<>', 109 | }[entry.state]; 110 | const name1 = entry.name1 ? entry.name1 : ''; 111 | const name2 = entry.name2 ? entry.name2 : ''; 112 | 113 | return [name1, state, name2].join(' '); 114 | }) 115 | .join('\n'); 116 | throw new Error('Directories do not match:\n' + message); 117 | } 118 | }); 119 | } 120 | 121 | exports.assertContentsMatch = assertContentsMatch; 122 | exports.setupRemote = setupRemote; 123 | exports.setupRepo = setupRepo; 124 | -------------------------------------------------------------------------------- /test/integration/basic.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const ghPages = require('../../lib/index.js'); 4 | const helper = require('../helper.js'); 5 | 6 | const fixtures = path.join(__dirname, 'fixtures'); 7 | const fixtureName = 'basic'; 8 | 9 | beforeEach(() => { 10 | ghPages.clean(); 11 | }); 12 | 13 | describe('basic usage', () => { 14 | it('pushes the contents of a directory to a gh-pages branch', (done) => { 15 | const local = path.join(fixtures, fixtureName, 'local'); 16 | const expected = path.join(fixtures, fixtureName, 'expected'); 17 | const branch = 'gh-pages'; 18 | 19 | helper.setupRemote(fixtureName, {branch}).then((url) => { 20 | const options = { 21 | repo: url, 22 | user: { 23 | name: 'User Name', 24 | email: 'user@email.com', 25 | }, 26 | }; 27 | ghPages.publish(local, options, (err) => { 28 | if (err) { 29 | return done(err); 30 | } 31 | helper 32 | .assertContentsMatch(expected, url, branch) 33 | .then(() => done()) 34 | .catch(done); 35 | }); 36 | }); 37 | }); 38 | 39 | it('can push to a different branch', (done) => { 40 | const local = path.join(fixtures, fixtureName, 'local'); 41 | const branch = 'master'; 42 | 43 | helper.setupRemote(fixtureName, {branch}).then((url) => { 44 | const options = { 45 | repo: url, 46 | branch: branch, 47 | user: { 48 | name: 'User Name', 49 | email: 'user@email.com', 50 | }, 51 | }; 52 | ghPages.publish(local, options, (err) => { 53 | if (err) { 54 | return done(err); 55 | } 56 | helper 57 | .assertContentsMatch(local, url, branch) 58 | .then(() => done()) 59 | .catch(done); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/integration/beforeAdd.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const ghPages = require('../../lib/index.js'); 4 | const helper = require('../helper.js'); 5 | 6 | const fixtures = path.join(__dirname, 'fixtures'); 7 | const fixtureName = 'beforeAdd'; 8 | 9 | beforeEach(() => { 10 | ghPages.clean(); 11 | }); 12 | 13 | describe('the beforeAdd option', () => { 14 | it('runs a provided async function before adding files', (done) => { 15 | const local = path.join(fixtures, fixtureName, 'local'); 16 | const expected = path.join(fixtures, fixtureName, 'expected'); 17 | const branch = 'gh-pages'; 18 | 19 | helper.setupRemote(fixtureName, {branch}).then((url) => { 20 | const options = { 21 | repo: url, 22 | add: true, 23 | beforeAdd(git) { 24 | return Promise.resolve().then(() => { 25 | return git.rm('hello-outdated-world.txt'); 26 | }); 27 | }, 28 | user: { 29 | name: 'User Name', 30 | email: 'user@email.com', 31 | }, 32 | }; 33 | ghPages.publish(local, options, (err) => { 34 | if (err) { 35 | return done(err); 36 | } 37 | helper 38 | .assertContentsMatch(expected, url, branch) 39 | .then(() => done()) 40 | .catch(done); 41 | }); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/integration/cname.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const ghPages = require('../../lib/index.js'); 4 | const helper = require('../helper.js'); 5 | 6 | const fixtures = path.join(__dirname, 'fixtures'); 7 | const fixtureName = 'cname'; 8 | 9 | beforeEach(() => { 10 | ghPages.clean(); 11 | }); 12 | 13 | describe('the --cname option', () => { 14 | it('adds a CNAME file', (done) => { 15 | const local = path.join(fixtures, fixtureName, 'local'); 16 | const expected = path.join(fixtures, fixtureName, 'expected'); 17 | const branch = 'gh-pages'; 18 | 19 | helper.setupRemote(fixtureName, {branch}).then((url) => { 20 | const options = { 21 | repo: url, 22 | user: { 23 | name: 'User Name', 24 | email: 'user@email.com', 25 | }, 26 | cname: 'custom-domain.com', 27 | }; 28 | ghPages.publish(local, options, (err) => { 29 | if (err) { 30 | return done(err); 31 | } 32 | helper 33 | .assertContentsMatch(expected, url, branch) 34 | .then(() => done()) 35 | .catch(done); 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/integration/cnameExists.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const ghPages = require('../../lib/index.js'); 4 | const helper = require('../helper.js'); 5 | 6 | const fixtures = path.join(__dirname, 'fixtures'); 7 | const fixtureName = 'cname-exists'; 8 | 9 | beforeEach(() => { 10 | ghPages.clean(); 11 | }); 12 | 13 | describe('the --cname option', () => { 14 | it('works even if the CNAME file already exists AND replaces any existing value', (done) => { 15 | const local = path.join(fixtures, fixtureName, 'local'); 16 | const expected = path.join(fixtures, fixtureName, 'expected'); 17 | const branch = 'gh-pages'; 18 | 19 | helper.setupRemote(fixtureName, {branch}).then((url) => { 20 | const options = { 21 | repo: url, 22 | user: { 23 | name: 'User Name', 24 | email: 'user@email.com', 25 | }, 26 | cname: 'custom-domain.com', 27 | }; 28 | ghPages.publish(local, options, (err) => { 29 | if (err) { 30 | return done(err); 31 | } 32 | helper 33 | .assertContentsMatch(expected, url, branch) 34 | .then(() => done()) 35 | .catch(done); 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/integration/dest.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const ghPages = require('../../lib/index.js'); 4 | const helper = require('../helper.js'); 5 | 6 | const fixtures = path.join(__dirname, 'fixtures'); 7 | const fixtureName = 'dest'; 8 | 9 | beforeEach(() => { 10 | ghPages.clean(); 11 | }); 12 | 13 | describe('the dest option', () => { 14 | it('allows publishing to a subdirectory within a branch', (done) => { 15 | const local = path.join(fixtures, fixtureName, 'local'); 16 | const expected = path.join(fixtures, fixtureName, 'expected'); 17 | const branch = 'gh-pages'; 18 | const dest = 'target'; 19 | 20 | helper.setupRemote(fixtureName, {branch}).then((url) => { 21 | const options = { 22 | repo: url, 23 | dest: dest, 24 | user: { 25 | name: 'User Name', 26 | email: 'user@email.com', 27 | }, 28 | }; 29 | 30 | ghPages.publish(local, options, (err) => { 31 | if (err) { 32 | return done(err); 33 | } 34 | helper 35 | .assertContentsMatch(expected, url, branch) 36 | .then(() => done()) 37 | .catch(done); 38 | }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/integration/fixtures/basic/expected/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/basic/local/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/basic/remote/initial: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/basic/remote/initial -------------------------------------------------------------------------------- /test/integration/fixtures/beforeAdd/expected/hello-old-world.txt: -------------------------------------------------------------------------------- 1 | Hello Old World! -------------------------------------------------------------------------------- /test/integration/fixtures/beforeAdd/expected/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /test/integration/fixtures/beforeAdd/local/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /test/integration/fixtures/beforeAdd/remote/hello-old-world.txt: -------------------------------------------------------------------------------- 1 | Hello Old World! -------------------------------------------------------------------------------- /test/integration/fixtures/beforeAdd/remote/hello-outdated-world.txt: -------------------------------------------------------------------------------- 1 | Hello Outdated World! -------------------------------------------------------------------------------- /test/integration/fixtures/cname-exists/expected/CNAME: -------------------------------------------------------------------------------- 1 | custom-domain.com 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/cname-exists/expected/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/cname-exists/local/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/cname-exists/remote/CNAME: -------------------------------------------------------------------------------- 1 | existing-domain.com 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/cname-exists/remote/initial: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/cname-exists/remote/initial -------------------------------------------------------------------------------- /test/integration/fixtures/cname/expected/CNAME: -------------------------------------------------------------------------------- 1 | custom-domain.com -------------------------------------------------------------------------------- /test/integration/fixtures/cname/expected/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/cname/local/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/cname/remote/initial: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/cname/remote/initial -------------------------------------------------------------------------------- /test/integration/fixtures/dest/expected/stable.txt: -------------------------------------------------------------------------------- 1 | This file should not be removed. 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/dest/expected/target/one.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/dest/expected/target/one.txt -------------------------------------------------------------------------------- /test/integration/fixtures/dest/expected/target/two.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/dest/expected/target/two.txt -------------------------------------------------------------------------------- /test/integration/fixtures/dest/local/one.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/dest/local/one.txt -------------------------------------------------------------------------------- /test/integration/fixtures/dest/local/two.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/dest/local/two.txt -------------------------------------------------------------------------------- /test/integration/fixtures/dest/remote/stable.txt: -------------------------------------------------------------------------------- 1 | This file should not be removed. 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/dest/remote/target/removed.txt: -------------------------------------------------------------------------------- 1 | This file will be removed since the directory is used as the `dest` option. 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/include/expected/good.js: -------------------------------------------------------------------------------- 1 | // this file should be included 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/include/local/bad.coffee: -------------------------------------------------------------------------------- 1 | # this file should not be included 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/include/local/good.js: -------------------------------------------------------------------------------- 1 | // this file should be included 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/include/remote/initial: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/include/remote/initial -------------------------------------------------------------------------------- /test/integration/fixtures/nojekyll-exists/expected/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/nojekyll-exists/expected/.nojekyll -------------------------------------------------------------------------------- /test/integration/fixtures/nojekyll-exists/expected/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/nojekyll-exists/local/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/nojekyll-exists/remote/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/nojekyll-exists/remote/.nojekyll -------------------------------------------------------------------------------- /test/integration/fixtures/nojekyll-exists/remote/initial: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/nojekyll-exists/remote/initial -------------------------------------------------------------------------------- /test/integration/fixtures/nojekyll/expected/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/nojekyll/expected/.nojekyll -------------------------------------------------------------------------------- /test/integration/fixtures/nojekyll/expected/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/nojekyll/local/hello-world.txt: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/nojekyll/remote/initial: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tschaub/gh-pages/ffdd1238f73e5e3e7499e0782d6d789259489ec6/test/integration/fixtures/nojekyll/remote/initial -------------------------------------------------------------------------------- /test/integration/fixtures/remove/expected/new.js: -------------------------------------------------------------------------------- 1 | // to be copied 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/remove/expected/stable.txt: -------------------------------------------------------------------------------- 1 | This file should not be removed. 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/remove/local/new.js: -------------------------------------------------------------------------------- 1 | // to be copied 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/remove/remote/removed.css: -------------------------------------------------------------------------------- 1 | /* to be removed */ 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/remove/remote/removed.js: -------------------------------------------------------------------------------- 1 | // to be removed 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/remove/remote/stable.txt: -------------------------------------------------------------------------------- 1 | This file should not be removed. 2 | -------------------------------------------------------------------------------- /test/integration/include.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const ghPages = require('../../lib/index.js'); 4 | const helper = require('../helper.js'); 5 | 6 | const fixtures = path.join(__dirname, 'fixtures'); 7 | const fixtureName = 'include'; 8 | 9 | beforeEach(() => { 10 | ghPages.clean(); 11 | }); 12 | 13 | describe('the src option', () => { 14 | it('can be used to limit which files are included', (done) => { 15 | const local = path.join(fixtures, fixtureName, 'local'); 16 | const expected = path.join(fixtures, fixtureName, 'expected'); 17 | const branch = 'gh-pages'; 18 | 19 | helper.setupRemote(fixtureName, {branch}).then((url) => { 20 | const options = { 21 | repo: url, 22 | src: '**/*.js', 23 | user: { 24 | name: 'User Name', 25 | email: 'user@email.com', 26 | }, 27 | }; 28 | 29 | ghPages.publish(local, options, (err) => { 30 | if (err) { 31 | return done(err); 32 | } 33 | helper 34 | .assertContentsMatch(expected, url, branch) 35 | .then(() => done()) 36 | .catch(done); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/integration/nojekyll.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const ghPages = require('../../lib/index.js'); 4 | const helper = require('../helper.js'); 5 | 6 | const fixtures = path.join(__dirname, 'fixtures'); 7 | const fixtureName = 'nojekyll'; 8 | 9 | beforeEach(() => { 10 | ghPages.clean(); 11 | }); 12 | 13 | describe('the --nojekyll option', () => { 14 | it('adds a .nojekyll file', (done) => { 15 | const local = path.join(fixtures, fixtureName, 'local'); 16 | const expected = path.join(fixtures, fixtureName, 'expected'); 17 | const branch = 'gh-pages'; 18 | 19 | helper.setupRemote(fixtureName, {branch}).then((url) => { 20 | const options = { 21 | repo: url, 22 | user: { 23 | name: 'User Name', 24 | email: 'user@email.com', 25 | }, 26 | nojekyll: true, 27 | }; 28 | ghPages.publish(local, options, (err) => { 29 | if (err) { 30 | return done(err); 31 | } 32 | helper 33 | .assertContentsMatch(expected, url, branch) 34 | .then(() => done()) 35 | .catch(done); 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/integration/nojekyllExists.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const ghPages = require('../../lib/index.js'); 4 | const helper = require('../helper.js'); 5 | 6 | const fixtures = path.join(__dirname, 'fixtures'); 7 | const fixtureName = 'nojekyll-exists'; 8 | 9 | beforeEach(() => { 10 | ghPages.clean(); 11 | }); 12 | 13 | describe('the --nojekyll option', () => { 14 | it('works even if the .nojekyll file already exists', (done) => { 15 | const local = path.join(fixtures, fixtureName, 'local'); 16 | const expected = path.join(fixtures, fixtureName, 'expected'); 17 | const branch = 'gh-pages'; 18 | 19 | helper.setupRemote(fixtureName, {branch}).then((url) => { 20 | const options = { 21 | repo: url, 22 | user: { 23 | name: 'User Name', 24 | email: 'user@email.com', 25 | }, 26 | nojekyll: true, 27 | }; 28 | ghPages.publish(local, options, (err) => { 29 | if (err) { 30 | return done(err); 31 | } 32 | helper 33 | .assertContentsMatch(expected, url, branch) 34 | .then(() => done()) 35 | .catch(done); 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/integration/remove.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const ghPages = require('../../lib/index.js'); 4 | const helper = require('../helper.js'); 5 | 6 | const fixtures = path.join(__dirname, 'fixtures'); 7 | const fixtureName = 'remove'; 8 | 9 | beforeEach(() => { 10 | ghPages.clean(); 11 | }); 12 | 13 | describe('the remove option', () => { 14 | it('removes matched files in remote branch', (done) => { 15 | const local = path.join(fixtures, fixtureName, 'local'); 16 | const expected = path.join(fixtures, fixtureName, 'expected'); 17 | const branch = 'gh-pages'; 18 | const remove = '*.{js,css}'; 19 | 20 | helper.setupRemote(fixtureName, {branch}).then((url) => { 21 | const options = { 22 | repo: url, 23 | user: { 24 | name: 'User Name', 25 | email: 'user@email.com', 26 | }, 27 | remove: remove, 28 | }; 29 | 30 | ghPages.publish(local, options, (err) => { 31 | if (err) { 32 | return done(err); 33 | } 34 | helper 35 | .assertContentsMatch(expected, url, branch) 36 | .then(() => done()) 37 | .catch(done); 38 | }); 39 | }); 40 | }); 41 | 42 | it('skips removing files if there are no files to be removed', (done) => { 43 | const local = path.join(fixtures, fixtureName, 'remote'); 44 | const branch = 'gh-pages'; 45 | const remove = 'non-exist-file'; 46 | 47 | helper.setupRemote(fixtureName, {branch}).then((url) => { 48 | const options = { 49 | repo: url, 50 | user: { 51 | name: 'User Name', 52 | email: 'user@email.com', 53 | }, 54 | remove: remove, 55 | }; 56 | 57 | ghPages.publish(local, options, (err) => { 58 | if (err) { 59 | return done(err); 60 | } 61 | helper 62 | .assertContentsMatch(local, url, branch) 63 | .then(() => done()) 64 | .catch(done); 65 | }); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/lib/index.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {describe, it} = require('mocha'); 3 | const index = require('../../lib/index.js'); 4 | const assert = require('../helper.js').assert; 5 | 6 | describe('index', () => { 7 | describe('getCacheDir()', () => { 8 | it('works for git@github.com:/.git', () => { 9 | const dir = index.getCacheDir( 10 | 'git@github.com:example-user/example-project.git', 11 | ); 12 | 13 | const expected = path.join( 14 | '.cache', 15 | 'gh-pages', 16 | 'git@github.com!example-user!example-project.git', 17 | ); 18 | assert(dir.endsWith(expected), `unexpected cache dir: ${dir}`); 19 | }); 20 | 21 | it('works for https://github.com//.git', () => { 22 | const dir = index.getCacheDir( 23 | 'https://github.com/example-user/example-project.git', 24 | ); 25 | 26 | const expected = path.join( 27 | '.cache', 28 | 'gh-pages', 29 | 'https!github.com!example-user!example-project.git', 30 | ); 31 | assert(dir.endsWith(expected), `unexpected cache dir: ${dir}`); 32 | }); 33 | 34 | it('works for https://:@github.com//.git', () => { 35 | const dir = index.getCacheDir( 36 | 'https://user:pass@github.com/example-user/example-project.git', 37 | ); 38 | 39 | const expected = path.join( 40 | '.cache', 41 | 'gh-pages', 42 | 'https!user!pass@github.com!example-user!example-project.git', 43 | ); 44 | assert(dir.endsWith(expected), `unexpected cache dir: ${dir}`); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/lib/util.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {beforeEach, describe, it} = require('mocha'); 3 | const util = require('../../lib/util.js'); 4 | const assert = require('../helper.js').assert; 5 | const helper = require('../helper.js'); 6 | 7 | describe('util', () => { 8 | let files; 9 | beforeEach(() => { 10 | files = [ 11 | path.join('a1', 'b1', 'c2', 'd2.txt'), 12 | path.join('a1', 'b2', 'c2', 'd1.txt'), 13 | path.join('a2.txt'), 14 | path.join('a1', 'b1', 'c1', 'd1.txt'), 15 | path.join('a1', 'b1', 'c2', 'd1.txt'), 16 | path.join('a1', 'b1.txt'), 17 | path.join('a2', 'b1', 'c2.txt'), 18 | path.join('a1', 'b1', 'c2', 'd3.txt'), 19 | path.join('a1', 'b2', 'c1', 'd1.txt'), 20 | path.join('a1.txt'), 21 | path.join('a2', 'b1', 'c1.txt'), 22 | path.join('a2', 'b1.txt'), 23 | ].slice(); 24 | }); 25 | 26 | describe('byShortPath', () => { 27 | it('sorts an array of filepaths, shortest first', () => { 28 | files.sort(util.byShortPath); 29 | 30 | const expected = [ 31 | path.join('a1.txt'), 32 | path.join('a2.txt'), 33 | path.join('a1', 'b1.txt'), 34 | path.join('a2', 'b1.txt'), 35 | path.join('a2', 'b1', 'c1.txt'), 36 | path.join('a2', 'b1', 'c2.txt'), 37 | path.join('a1', 'b1', 'c1', 'd1.txt'), 38 | path.join('a1', 'b1', 'c2', 'd1.txt'), 39 | path.join('a1', 'b1', 'c2', 'd2.txt'), 40 | path.join('a1', 'b1', 'c2', 'd3.txt'), 41 | path.join('a1', 'b2', 'c1', 'd1.txt'), 42 | path.join('a1', 'b2', 'c2', 'd1.txt'), 43 | ]; 44 | 45 | assert.deepEqual(files, expected); 46 | }); 47 | }); 48 | 49 | describe('uniqueDirs', () => { 50 | it('gets a list of unique directory paths', () => { 51 | // not comparing order here, so we sort both 52 | const got = util.uniqueDirs(files).sort(); 53 | 54 | const expected = [ 55 | '.', 56 | 'a1', 57 | 'a2', 58 | path.join('a1', 'b1'), 59 | path.join('a1', 'b1', 'c1'), 60 | path.join('a1', 'b1', 'c2'), 61 | path.join('a1', 'b2'), 62 | path.join('a1', 'b2', 'c1'), 63 | path.join('a1', 'b2', 'c2'), 64 | path.join('a2', 'b1'), 65 | ].sort(); 66 | 67 | assert.deepEqual(got, expected); 68 | }); 69 | 70 | it('gets a list of unique directories on absolute paths', () => { 71 | const absoluteFiles = files.map((path) => { 72 | return '/' + path; 73 | }); 74 | // not comparing order here, so we sort both 75 | const got = util.uniqueDirs(absoluteFiles).sort(); 76 | 77 | const expected = [ 78 | '/', 79 | '/a1', 80 | '/a2', 81 | path.join('/a1', 'b1'), 82 | path.join('/a1', 'b1', 'c1'), 83 | path.join('/a1', 'b1', 'c2'), 84 | path.join('/a1', 'b2'), 85 | path.join('/a1', 'b2', 'c1'), 86 | path.join('/a1', 'b2', 'c2'), 87 | path.join('/a2', 'b1'), 88 | ].sort(); 89 | 90 | assert.deepEqual(got, expected); 91 | }); 92 | }); 93 | 94 | describe('dirsToCreate', () => { 95 | it('gets a sorted list of directories to create', () => { 96 | const got = util.dirsToCreate(files); 97 | 98 | const expected = [ 99 | '.', 100 | 'a1', 101 | 'a2', 102 | path.join('a1', 'b1'), 103 | path.join('a1', 'b2'), 104 | path.join('a2', 'b1'), 105 | path.join('a1', 'b1', 'c1'), 106 | path.join('a1', 'b1', 'c2'), 107 | path.join('a1', 'b2', 'c1'), 108 | path.join('a1', 'b2', 'c2'), 109 | ]; 110 | 111 | assert.deepEqual(got, expected); 112 | }); 113 | }); 114 | 115 | describe('getUser', () => { 116 | it('gets the locally configured user', (done) => { 117 | const name = 'Full Name'; 118 | const email = 'email@example.com'; 119 | 120 | helper.setupRepo('basic', {user: {name, email}}).then((dir) => { 121 | util 122 | .getUser(dir) 123 | .then((user) => { 124 | assert.equal(user.name, name); 125 | assert.equal(user.email, email); 126 | done(); 127 | }) 128 | .catch(done); 129 | }); 130 | }); 131 | }); 132 | }); 133 | --------------------------------------------------------------------------------