├── test ├── fixtures │ ├── add │ │ ├── first │ │ │ ├── first.txt │ │ │ └── sub │ │ │ │ └── sub.txt │ │ ├── second │ │ │ ├── second.txt │ │ │ └── sub │ │ │ │ └── sub.txt │ │ └── Gruntfile.js │ ├── multitask │ │ ├── one.txt │ │ ├── two.txt │ │ └── Gruntfile.js │ ├── deep-clone-dir │ │ ├── hello.txt │ │ └── Gruntfile.js │ ├── different-repo │ │ ├── hello.txt │ │ ├── repo │ │ │ └── hello.txt │ │ └── Gruntfile.js │ ├── dotfiles-option │ │ ├── dist │ │ │ ├── .one │ │ │ └── foo │ │ │ │ └── .bar │ │ │ │ └── two │ │ └── Gruntfile.js │ ├── unpushed │ │ ├── hello.txt │ │ ├── repo │ │ │ └── hello.txt │ │ └── Gruntfile.js │ ├── custom-clone-dir │ │ ├── hello.txt │ │ └── Gruntfile.js │ ├── deep-base │ │ ├── built │ │ │ └── pages │ │ │ │ └── hello.txt │ │ └── Gruntfile.js │ └── same-repo │ │ └── Gruntfile.js ├── .eslintrc ├── custom-clone-dir.spec.js ├── deep-clone-dir.spec.js ├── same-repo.spec.js ├── unpushed.spec.js ├── deep-base.spec.js ├── multitask.spec.js ├── different-repo.spec.js ├── dotfiles-option.spec.js ├── lib │ └── util.spec.js ├── helper.js └── add.spec.js ├── .gitignore ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── Gruntfile.js ├── LICENSE ├── changelog.md ├── package.json ├── lib ├── util.js └── git.js ├── tasks └── gh-pages.js └── README.md /test/fixtures/add/first/first.txt: -------------------------------------------------------------------------------- 1 | first -------------------------------------------------------------------------------- /test/fixtures/add/first/sub/sub.txt: -------------------------------------------------------------------------------- 1 | first -------------------------------------------------------------------------------- /test/fixtures/add/second/second.txt: -------------------------------------------------------------------------------- 1 | second -------------------------------------------------------------------------------- /test/fixtures/add/second/sub/sub.txt: -------------------------------------------------------------------------------- 1 | second -------------------------------------------------------------------------------- /test/fixtures/multitask/one.txt: -------------------------------------------------------------------------------- 1 | one 2 | -------------------------------------------------------------------------------- /test/fixtures/multitask/two.txt: -------------------------------------------------------------------------------- 1 | two 2 | -------------------------------------------------------------------------------- /test/fixtures/deep-clone-dir/hello.txt: -------------------------------------------------------------------------------- 1 | world 2 | -------------------------------------------------------------------------------- /test/fixtures/different-repo/hello.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /test/fixtures/dotfiles-option/dist/.one: -------------------------------------------------------------------------------- 1 | one 2 | -------------------------------------------------------------------------------- /test/fixtures/unpushed/hello.txt: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /test/fixtures/unpushed/repo/hello.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /test/fixtures/custom-clone-dir/hello.txt: -------------------------------------------------------------------------------- 1 | world 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /tmp/ 3 | /npm-debug.log 4 | -------------------------------------------------------------------------------- /test/fixtures/deep-base/built/pages/hello.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /test/fixtures/dotfiles-option/dist/foo/.bar/two: -------------------------------------------------------------------------------- 1 | two 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/different-repo/repo/hello.txt: -------------------------------------------------------------------------------- 1 | should not be pushing this content 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | env: 11 | CI: true 12 | 13 | jobs: 14 | run: 15 | name: Node ${{ matrix.node }} 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node: [8, 10, 12] 22 | 23 | steps: 24 | - name: Clone repository 25 | uses: actions/checkout@v2 26 | 27 | - name: Set Node.js version 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node }} 31 | 32 | - run: node --version 33 | - run: npm --version 34 | 35 | - name: Install npm dependencies 36 | run: npm ci 37 | 38 | - name: Run tests 39 | run: npm test 40 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Object} grunt Grunt. 3 | */ 4 | module.exports = function(grunt) { 5 | const tasksSrc = 'tasks/**/*.js'; 6 | const testSrc = 'test/**/*.js'; 7 | const fixturesSrc = 'test/fixtures/**/*.js'; 8 | 9 | grunt.initConfig({ 10 | mochaTest: { 11 | options: { 12 | reporter: 'spec' 13 | }, 14 | all: { 15 | src: testSrc, 16 | newer: true 17 | } 18 | }, 19 | watch: { 20 | tasks: { 21 | files: tasksSrc, 22 | tasks: ['mochaTest'] 23 | }, 24 | test: { 25 | files: testSrc, 26 | tasks: ['mochaTest'] 27 | }, 28 | fixtures: { 29 | files: fixturesSrc, 30 | tasks: ['mochaTest'] 31 | } 32 | } 33 | }); 34 | 35 | grunt.loadNpmTasks('grunt-mocha-test'); 36 | grunt.loadNpmTasks('grunt-contrib-watch'); 37 | 38 | grunt.registerTask('test', ['mochaTest']); 39 | 40 | grunt.registerTask('default', ['test']); 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tim Schaub 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/custom-clone-dir.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helper = require('./helper'); 5 | 6 | const assert = helper.assert; 7 | 8 | describe('custom-clone-dir', () => { 9 | let fixture; 10 | let repo; 11 | 12 | before(function(done) { 13 | this.timeout(3000); 14 | helper.buildFixture('custom-clone-dir', (error, dir) => { 15 | if (error) { 16 | return done(error); 17 | } 18 | fixture = dir; 19 | repo = path.join(fixture, 'clone-dir'); 20 | done(); 21 | }); 22 | }); 23 | 24 | after(done => { 25 | helper.afterFixture(fixture, done); 26 | }); 27 | 28 | it('creates clone-dir directory', done => { 29 | fs.stat(repo, (error, stats) => { 30 | if (error) { 31 | return done(error); 32 | } 33 | assert.isTrue(stats.isDirectory(), 'directory'); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('pushes the gh-pages branch to remote', done => { 39 | helper 40 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo) 41 | .then(() => { 42 | done(); 43 | }) 44 | .catch(done); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/deep-clone-dir.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helper = require('./helper'); 5 | 6 | const assert = helper.assert; 7 | 8 | describe('deep-clone-dir', () => { 9 | let fixture; 10 | let repo; 11 | 12 | before(function(done) { 13 | this.timeout(3000); 14 | helper.buildFixture('deep-clone-dir', (error, dir) => { 15 | if (error) { 16 | return done(error); 17 | } 18 | fixture = dir; 19 | repo = path.join(fixture, 'path/to/clone-dir'); 20 | done(); 21 | }); 22 | }); 23 | 24 | after(done => { 25 | helper.afterFixture(fixture, done); 26 | }); 27 | 28 | it('creates clone-dir directory', done => { 29 | fs.stat(repo, (error, stats) => { 30 | if (error) { 31 | return done(error); 32 | } 33 | assert.isTrue(stats.isDirectory(), 'directory'); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('pushes the gh-pages branch to remote', done => { 39 | helper 40 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo) 41 | .then(() => { 42 | done(); 43 | }) 44 | .catch(done); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/fixtures/different-repo/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const git = require('../../../lib/git'); 3 | 4 | /** @param {Object} grunt Grunt. */ 5 | module.exports = function(grunt) { 6 | grunt.initConfig({ 7 | 'gh-pages': { 8 | options: { 9 | repo: path.resolve('./repo'), 10 | user: { 11 | name: 'My Name', 12 | email: 'mail@example.com' 13 | } 14 | }, 15 | src: ['hello.txt'] 16 | } 17 | }); 18 | 19 | grunt.loadTasks('../../../tasks'); 20 | 21 | grunt.registerTask('init', function() { 22 | const done = this.async(); 23 | const cwd = path.join(__dirname, 'repo'); 24 | git 25 | .init(cwd) 26 | .then(() => { 27 | return git.add('.', cwd); 28 | }) 29 | .then(() => { 30 | return git(['config', 'user.email', 'mail@example.com'], cwd); 31 | }) 32 | .then(() => { 33 | return git(['config', 'user.name', 'My Name'], cwd); 34 | }) 35 | .then(() => { 36 | return git.commit('Initial commit', cwd); 37 | }) 38 | .then(done, done); 39 | }); 40 | 41 | grunt.registerTask('default', ['init', 'gh-pages']); 42 | }; 43 | -------------------------------------------------------------------------------- /test/fixtures/unpushed/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const git = require('../../../lib/git'); 3 | 4 | /** @param {Object} grunt Grunt. */ 5 | module.exports = function(grunt) { 6 | grunt.initConfig({ 7 | 'gh-pages': { 8 | options: { 9 | repo: path.resolve('./repo'), 10 | push: false, 11 | user: { 12 | name: 'My Name', 13 | email: 'mail@example.com' 14 | } 15 | }, 16 | src: ['hello.txt'] 17 | } 18 | }); 19 | 20 | grunt.loadTasks('../../../tasks'); 21 | 22 | grunt.registerTask('init', function() { 23 | const done = this.async(); 24 | const cwd = path.join(__dirname, 'repo'); 25 | git 26 | .init(cwd) 27 | .then(() => { 28 | return git.add('.', cwd); 29 | }) 30 | .then(() => { 31 | return git(['config', 'user.email', 'mail@example.com'], cwd); 32 | }) 33 | .then(() => { 34 | return git(['config', 'user.name', 'My Name'], cwd); 35 | }) 36 | .then(() => { 37 | return git.commit('Initial commit', cwd); 38 | }) 39 | .then(done, done); 40 | }); 41 | 42 | grunt.registerTask('default', ['init', 'gh-pages']); 43 | }; 44 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 4.0.0 4 | 5 | Drop support for Node 6 (see [#88][#88]) 6 | 7 | ## 3.1.0 8 | 9 | Downgrade to `q-io@1.13.6` for a security fix (see [#73][#73]) 10 | 11 | ## 3.0.0 12 | 13 | Updated dependencies and stopped testing on Node 4 (see [#72][#72]) 14 | 15 | ## 2.1.0 16 | 17 | Upgrade to `q-io@2.0.6` based on npm audit. 18 | 19 | ## 2.0.0 20 | 21 | Tests are no longer run on Node < 4. The task may still work on older versions of Node, but since it is no longer tested there, use at your own risk. Tests are currently run on Node 4 and Node 6. 22 | 23 | There are no API breaking changes in this release, it only includes dependency updates. 24 | 25 | * Replace `wrench` with `fs-extra` to avoid deprecation warning (see [#65][#65]). 26 | * Upgrade to `q-io@2.0.2` to avoid overriding `Array.prototype.find` (thanks @jaridmargolin, see [#62][#62]). 27 | 28 | ## 1.2.0 29 | 30 | * Upgrade to `graceful-fs@4` to get rid of warning (see [#66][#66]) 31 | 32 | [#62]: https://github.com/tschaub/grunt-gh-pages/pull/62 33 | [#65]: https://github.com/tschaub/grunt-gh-pages/pull/65 34 | [#66]: https://github.com/tschaub/grunt-gh-pages/pull/66 35 | [#72]: https://github.com/tschaub/grunt-gh-pages/pull/72 36 | [#73]: https://github.com/tschaub/grunt-gh-pages/pull/73 37 | [#88]: https://github.com/tschaub/grunt-gh-pages/pull/88 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-gh-pages", 3 | "description": "Publish to GitHub Pages with Grunt.", 4 | "version": "4.0.0", 5 | "homepage": "https://github.com/tschaub/grunt-gh-pages", 6 | "author": "Tim Schaub (http://tschaub.net/)", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/tschaub/grunt-gh-pages.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/tschaub/grunt-gh-pages/issues" 13 | }, 14 | "license": "MIT", 15 | "main": "tasks/gh-pages.js", 16 | "scripts": { 17 | "eslint": "eslint .", 18 | "pretest": "npm run eslint", 19 | "test": "grunt test" 20 | }, 21 | "dependencies": { 22 | "async": "^3.2.0", 23 | "fs-extra": "^8.1.0", 24 | "graceful-fs": "^4.2.3", 25 | "url-safe": "^2.0.0" 26 | }, 27 | "devDependencies": { 28 | "chai": "^4.2.0", 29 | "eslint": "^5.16.0", 30 | "eslint-config-tschaub": "^13.1.0", 31 | "grunt": "^1.0.3", 32 | "grunt-contrib-watch": "^1.1.0", 33 | "grunt-mocha-test": "^0.13.3", 34 | "mocha": "^7.1.1", 35 | "tmp": "^0.1.0" 36 | }, 37 | "peerDependencies": { 38 | "grunt": ">=0.4.0" 39 | }, 40 | "eslintConfig": { 41 | "extends": "tschaub" 42 | }, 43 | "keywords": [ 44 | "gruntplugin", 45 | "git", 46 | "grunt", 47 | "gh-pages", 48 | "github" 49 | ], 50 | "files": [ 51 | "{lib,tasks}/*.js" 52 | ], 53 | "engines": { 54 | "node": ">=6" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/same-repo.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helper = require('./helper'); 5 | 6 | const assert = helper.assert; 7 | 8 | describe('same-repo', () => { 9 | let fixture; 10 | let repo; 11 | 12 | before(function(done) { 13 | this.timeout(3000); 14 | helper.buildFixture('same-repo', (error, dir) => { 15 | if (error) { 16 | return done(error); 17 | } 18 | fixture = dir; 19 | repo = path.join(fixture, '.grunt/grunt-gh-pages/gh-pages/src'); 20 | done(); 21 | }); 22 | }); 23 | 24 | after(done => { 25 | helper.afterFixture(fixture, done); 26 | }); 27 | 28 | it('creates .grunt/grunt-gh-pages/gh-pages/src directory', done => { 29 | fs.stat(repo, (error, stats) => { 30 | assert.isTrue(!error, 'no error'); 31 | assert.isTrue(stats.isDirectory(), 'directory'); 32 | done(error); 33 | }); 34 | }); 35 | 36 | it('creates a gh-pages branch', done => { 37 | helper 38 | .git(['rev-parse', '--abbrev-ref', 'HEAD'], repo) 39 | .then(branch => { 40 | assert.strictEqual(branch, 'gh-pages\n', 'branch created'); 41 | done(); 42 | }) 43 | .catch(done); 44 | }); 45 | 46 | it('pushes the gh-pages branch to remote', done => { 47 | helper 48 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo) 49 | .then(() => { 50 | done(); 51 | }) 52 | .catch(done); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/fixtures/same-repo/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const tmp = require('tmp'); 2 | const git = require('../../../lib/git'); 3 | 4 | /** @param {Object} grunt Grunt. */ 5 | module.exports = function(grunt) { 6 | grunt.initConfig({ 7 | 'gh-pages': { 8 | options: { 9 | user: { 10 | name: 'My Name', 11 | email: 'mail@example.com' 12 | } 13 | }, 14 | src: ['Gruntfile.js'] 15 | } 16 | }); 17 | 18 | grunt.loadTasks('../../../tasks'); 19 | 20 | grunt.registerTask('init', function() { 21 | const done = this.async(); 22 | tmp.dir((error, remote) => { 23 | if (error) { 24 | return done(error); 25 | } 26 | git(['init', '--bare'], remote) 27 | .then(() => { 28 | return git.init(__dirname); 29 | }) 30 | .then(() => { 31 | return git.add('Gruntfile.js', __dirname); 32 | }) 33 | .then(() => { 34 | return git(['config', 'user.email', 'mail@example.com'], __dirname); 35 | }) 36 | .then(() => { 37 | return git(['config', 'user.name', 'My Name'], __dirname); 38 | }) 39 | .then(() => { 40 | return git.commit('Initial commit', __dirname); 41 | }) 42 | .then(() => { 43 | return git(['remote', 'add', 'origin', remote], __dirname); 44 | }) 45 | .then(() => { 46 | return git(['push', 'origin', 'master'], __dirname); 47 | }) 48 | .then(done, done); 49 | }); 50 | }); 51 | 52 | grunt.registerTask('default', ['init', 'gh-pages']); 53 | }; 54 | -------------------------------------------------------------------------------- /test/fixtures/deep-base/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const tmp = require('tmp'); 2 | const git = require('../../../lib/git'); 3 | 4 | /** @param {Object} grunt Grunt. */ 5 | module.exports = function(grunt) { 6 | grunt.initConfig({ 7 | 'gh-pages': { 8 | options: { 9 | user: { 10 | name: 'My Name', 11 | email: 'mail@example.com' 12 | }, 13 | base: 'built/pages' 14 | }, 15 | src: ['hello.txt'] 16 | } 17 | }); 18 | 19 | grunt.loadTasks('../../../tasks'); 20 | 21 | grunt.registerTask('init', function() { 22 | const done = this.async(); 23 | tmp.dir((error, remote) => { 24 | if (error) { 25 | return done(error); 26 | } 27 | git(['init', '--bare'], remote) 28 | .then(() => { 29 | return git.init(__dirname); 30 | }) 31 | .then(() => { 32 | return git.add('Gruntfile.js', __dirname); 33 | }) 34 | .then(() => { 35 | return git(['config', 'user.email', 'mail@example.com'], __dirname); 36 | }) 37 | .then(() => { 38 | return git(['config', 'user.name', 'My Name'], __dirname); 39 | }) 40 | .then(() => { 41 | return git.commit('Initial commit', __dirname); 42 | }) 43 | .then(() => { 44 | return git(['remote', 'add', 'origin', remote], __dirname); 45 | }) 46 | .then(() => { 47 | return git(['push', 'origin', 'master'], __dirname); 48 | }) 49 | .then(done, done); 50 | }); 51 | }); 52 | 53 | grunt.registerTask('default', ['init', 'gh-pages']); 54 | }; 55 | -------------------------------------------------------------------------------- /test/fixtures/custom-clone-dir/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const tmp = require('tmp'); 2 | const git = require('../../../lib/git'); 3 | 4 | /** @param {Object} grunt Grunt. */ 5 | module.exports = function(grunt) { 6 | grunt.initConfig({ 7 | 'gh-pages': { 8 | options: { 9 | user: { 10 | name: 'My Name', 11 | email: 'mail@example.com' 12 | }, 13 | clone: 'clone-dir' 14 | }, 15 | src: ['hello.txt'] 16 | } 17 | }); 18 | 19 | grunt.loadTasks('../../../tasks'); 20 | 21 | grunt.registerTask('init', function() { 22 | const done = this.async(); 23 | tmp.dir((error, remote) => { 24 | if (error) { 25 | return done(error); 26 | } 27 | git(['init', '--bare'], remote) 28 | .then(() => { 29 | return git.init(__dirname); 30 | }) 31 | .then(() => { 32 | return git.add('Gruntfile.js', __dirname); 33 | }) 34 | .then(() => { 35 | return git(['config', 'user.email', 'mail@example.com'], __dirname); 36 | }) 37 | .then(() => { 38 | return git(['config', 'user.name', 'My Name'], __dirname); 39 | }) 40 | .then(() => { 41 | return git.commit('Initial commit', __dirname); 42 | }) 43 | .then(() => { 44 | return git(['remote', 'add', 'origin', remote], __dirname); 45 | }) 46 | .then(() => { 47 | return git(['push', 'origin', 'master'], __dirname); 48 | }) 49 | .then(done, done); 50 | }); 51 | }); 52 | 53 | grunt.registerTask('default', ['init', 'gh-pages']); 54 | }; 55 | -------------------------------------------------------------------------------- /test/fixtures/deep-clone-dir/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const tmp = require('tmp'); 2 | const git = require('../../../lib/git'); 3 | 4 | /** @param {Object} grunt Grunt. */ 5 | module.exports = function(grunt) { 6 | grunt.initConfig({ 7 | 'gh-pages': { 8 | options: { 9 | user: { 10 | name: 'My Name', 11 | email: 'mail@example.com' 12 | }, 13 | clone: 'path/to/clone-dir' 14 | }, 15 | src: ['hello.txt'] 16 | } 17 | }); 18 | 19 | grunt.loadTasks('../../../tasks'); 20 | 21 | grunt.registerTask('init', function() { 22 | const done = this.async(); 23 | tmp.dir((error, remote) => { 24 | if (error) { 25 | return done(error); 26 | } 27 | git(['init', '--bare'], remote) 28 | .then(() => { 29 | return git.init(__dirname); 30 | }) 31 | .then(() => { 32 | return git.add('Gruntfile.js', __dirname); 33 | }) 34 | .then(() => { 35 | return git(['config', 'user.email', 'mail@example.com'], __dirname); 36 | }) 37 | .then(() => { 38 | return git(['config', 'user.name', 'My Name'], __dirname); 39 | }) 40 | .then(() => { 41 | return git.commit('Initial commit', __dirname); 42 | }) 43 | .then(() => { 44 | return git(['remote', 'add', 'origin', remote], __dirname); 45 | }) 46 | .then(() => { 47 | return git(['push', 'origin', 'master'], __dirname); 48 | }) 49 | .then(done, done); 50 | }); 51 | }); 52 | 53 | grunt.registerTask('default', ['init', 'gh-pages']); 54 | }; 55 | -------------------------------------------------------------------------------- /test/fixtures/dotfiles-option/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const tmp = require('tmp'); 2 | const git = require('../../../lib/git'); 3 | 4 | /** @param {Object} grunt Grunt. */ 5 | module.exports = function(grunt) { 6 | grunt.initConfig({ 7 | 'gh-pages': { 8 | options: { 9 | user: { 10 | name: 'My Name', 11 | email: 'mail@example.com' 12 | }, 13 | dotfiles: true, 14 | base: 'dist' 15 | }, 16 | src: '**' 17 | } 18 | }); 19 | 20 | grunt.loadTasks('../../../tasks'); 21 | 22 | grunt.registerTask('init', function() { 23 | const done = this.async(); 24 | tmp.dir((error, remote) => { 25 | if (error) { 26 | return done(error); 27 | } 28 | git(['init', '--bare'], remote) 29 | .then(() => { 30 | return git.init(__dirname); 31 | }) 32 | .then(() => { 33 | return git.add('Gruntfile.js', __dirname); 34 | }) 35 | .then(() => { 36 | return git(['config', 'user.email', 'mail@example.com'], __dirname); 37 | }) 38 | .then(() => { 39 | return git(['config', 'user.name', 'My Name'], __dirname); 40 | }) 41 | .then(() => { 42 | return git.commit('Initial commit', __dirname); 43 | }) 44 | .then(() => { 45 | return git(['remote', 'add', 'origin', remote], __dirname); 46 | }) 47 | .then(() => { 48 | return git(['push', 'origin', 'master'], __dirname); 49 | }) 50 | .then(done, done); 51 | }); 52 | }); 53 | 54 | grunt.registerTask('default', ['init', 'gh-pages']); 55 | }; 56 | -------------------------------------------------------------------------------- /test/unpushed.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helper = require('./helper'); 5 | 6 | const assert = helper.assert; 7 | 8 | describe('unpushed', () => { 9 | let fixture; 10 | let repo; 11 | 12 | before(function(done) { 13 | this.timeout(3000); 14 | helper.buildFixture('unpushed', (error, dir) => { 15 | if (error) { 16 | return done(error); 17 | } 18 | fixture = dir; 19 | repo = path.join(fixture, '.grunt/grunt-gh-pages/gh-pages/src'); 20 | done(); 21 | }); 22 | }); 23 | 24 | after(done => { 25 | helper.afterFixture(fixture, done); 26 | }); 27 | 28 | it('creates .grunt/grunt-gh-pages/gh-pages/src directory', done => { 29 | fs.stat(repo, (error, stats) => { 30 | assert.isTrue(!error, 'no error'); 31 | assert.isTrue(stats.isDirectory(), 'directory'); 32 | done(error); 33 | }); 34 | }); 35 | 36 | it('creates a gh-pages branch', done => { 37 | helper 38 | .git(['rev-parse', '--abbrev-ref', 'HEAD'], repo) 39 | .then(branch => { 40 | assert.strictEqual(branch, 'gh-pages\n', 'branch created'); 41 | done(); 42 | }) 43 | .catch(done); 44 | }); 45 | 46 | it('does not push the gh-pages branch to remote', done => { 47 | helper 48 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo) 49 | .then(() => { 50 | done(new Error('Expected not to find origin/gh-pages')); 51 | }) 52 | .catch(() => { 53 | // failure on the ls-remote is what we're looking for (no push) 54 | done(); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/fixtures/multitask/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const tmp = require('tmp'); 2 | const git = require('../../../lib/git'); 3 | 4 | /** @param {Object} grunt Grunt. */ 5 | module.exports = function(grunt) { 6 | grunt.initConfig({ 7 | 'gh-pages': { 8 | options: { 9 | user: { 10 | name: 'My Name', 11 | email: 'mail@example.com' 12 | } 13 | }, 14 | first: { 15 | src: ['one.txt'] 16 | }, 17 | second: { 18 | src: ['two.txt'], 19 | options: { 20 | branch: 'branch-two' 21 | } 22 | } 23 | } 24 | }); 25 | 26 | grunt.loadTasks('../../../tasks'); 27 | 28 | grunt.registerTask('init', function() { 29 | const done = this.async(); 30 | tmp.dir((error, remote) => { 31 | if (error) { 32 | return done(error); 33 | } 34 | git(['init', '--bare'], remote) 35 | .then(() => { 36 | return git.init(__dirname); 37 | }) 38 | .then(() => { 39 | return git.add('Gruntfile.js', __dirname); 40 | }) 41 | .then(() => { 42 | return git(['config', 'user.email', 'mail@example.com'], __dirname); 43 | }) 44 | .then(() => { 45 | return git(['config', 'user.name', 'My Name'], __dirname); 46 | }) 47 | .then(() => { 48 | return git.commit('Initial commit', __dirname); 49 | }) 50 | .then(() => { 51 | return git(['remote', 'add', 'origin', remote], __dirname); 52 | }) 53 | .then(() => { 54 | return git(['push', 'origin', 'master'], __dirname); 55 | }) 56 | .then(done, done); 57 | }); 58 | }); 59 | 60 | grunt.registerTask('default', ['init', 'gh-pages']); 61 | }; 62 | -------------------------------------------------------------------------------- /test/deep-base.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helper = require('./helper'); 5 | 6 | const assert = helper.assert; 7 | 8 | describe('deep-base', () => { 9 | let fixture; 10 | let repo; 11 | 12 | before(function(done) { 13 | this.timeout(3000); 14 | helper.buildFixture('deep-base', (error, dir) => { 15 | if (error) { 16 | return done(error); 17 | } 18 | fixture = dir; 19 | repo = path.join(fixture, '.grunt/grunt-gh-pages/gh-pages/src'); 20 | done(); 21 | }); 22 | }); 23 | 24 | after(done => { 25 | helper.afterFixture(fixture, done); 26 | }); 27 | 28 | it('creates .grunt/grunt-gh-pages/gh-pages/src directory', done => { 29 | fs.stat(repo, (error, stats) => { 30 | assert.isTrue(!error, 'no error'); 31 | assert.isTrue(stats.isDirectory(), 'directory'); 32 | done(error); 33 | }); 34 | }); 35 | 36 | it('creates a gh-pages branch', done => { 37 | helper 38 | .git(['rev-parse', '--abbrev-ref', 'HEAD'], repo) 39 | .then(branch => { 40 | assert.strictEqual(branch, 'gh-pages\n', 'branch created'); 41 | done(); 42 | }) 43 | .catch(done); 44 | }); 45 | 46 | it('copies source files relative to the base', done => { 47 | fs.exists(path.join(repo, 'hello.txt'), exists => { 48 | if (exists) { 49 | done(); 50 | } else { 51 | done(new Error('Failed to find "hello.txt" in repo: ') + repo); 52 | } 53 | }); 54 | }); 55 | 56 | it('pushes the gh-pages branch to remote', done => { 57 | helper 58 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo) 59 | .then(() => { 60 | done(); 61 | }) 62 | .catch(done); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/multitask.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helper = require('./helper'); 5 | 6 | const assert = helper.assert; 7 | 8 | describe('multitask', () => { 9 | let fixture; 10 | let repo1; 11 | let repo2; 12 | 13 | before(function(done) { 14 | this.timeout(3000); 15 | helper.buildFixture('multitask', (error, dir) => { 16 | if (error) { 17 | return done(error); 18 | } 19 | fixture = dir; 20 | repo1 = path.join(fixture, '.grunt/grunt-gh-pages/gh-pages/first'); 21 | repo2 = path.join(fixture, '.grunt/grunt-gh-pages/gh-pages/second'); 22 | done(); 23 | }); 24 | }); 25 | 26 | after(done => { 27 | helper.afterFixture(fixture, done); 28 | }); 29 | 30 | it('creates .grunt/grunt-gh-pages/gh-pages/first directory', done => { 31 | fs.stat(repo1, (error, stats) => { 32 | assert.isTrue(!error, 'no error'); 33 | assert.isTrue(stats.isDirectory(), 'directory'); 34 | done(error); 35 | }); 36 | }); 37 | 38 | it('creates .grunt/grunt-gh-pages/gh-pages/second directory', done => { 39 | fs.stat(repo2, (error, stats) => { 40 | assert.isTrue(!error, 'no error'); 41 | assert.isTrue(stats.isDirectory(), 'directory'); 42 | done(error); 43 | }); 44 | }); 45 | 46 | it('pushes the gh-pages branch to remote', done => { 47 | helper 48 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo1) 49 | .then(() => { 50 | done(); 51 | }) 52 | .catch(done); 53 | }); 54 | 55 | it('pushes the branch-two branch to remote', done => { 56 | helper 57 | .git(['ls-remote', '--exit-code', '.', 'origin/branch-two'], repo2) 58 | .then(() => { 59 | done(); 60 | }) 61 | .catch(done); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/fixtures/add/Gruntfile.js: -------------------------------------------------------------------------------- 1 | const tmp = require('tmp'); 2 | const git = require('../../../lib/git'); 3 | 4 | /** @param {Object} grunt Grunt. */ 5 | module.exports = function(grunt) { 6 | grunt.initConfig({ 7 | 'gh-pages': { 8 | options: { 9 | user: { 10 | name: 'My Name', 11 | email: 'mail@example.com' 12 | } 13 | }, 14 | first: { 15 | options: { 16 | base: 'first' 17 | }, 18 | src: '**/*' 19 | }, 20 | // we want this target to add new files and not remove existing ones 21 | second: { 22 | options: { 23 | base: 'second', 24 | add: true 25 | }, 26 | src: '**/*' 27 | } 28 | } 29 | }); 30 | 31 | grunt.loadTasks('../../../tasks'); 32 | 33 | grunt.registerTask('init', function() { 34 | const done = this.async(); 35 | tmp.dir((error, remote) => { 36 | if (error) { 37 | return done(error); 38 | } 39 | git(['init', '--bare'], remote) 40 | .then(() => { 41 | return git.init(__dirname); 42 | }) 43 | .then(() => { 44 | return git.add('Gruntfile.js', __dirname); 45 | }) 46 | .then(() => { 47 | return git(['config', 'user.email', 'mail@example.com'], __dirname); 48 | }) 49 | .then(() => { 50 | return git(['config', 'user.name', 'My Name'], __dirname); 51 | }) 52 | .then(() => { 53 | return git.commit('Initial commit', __dirname); 54 | }) 55 | .then(() => { 56 | return git(['remote', 'add', 'origin', remote], __dirname); 57 | }) 58 | .then(() => { 59 | return git(['push', 'origin', 'master'], __dirname); 60 | }) 61 | .then(done, done); 62 | }); 63 | }); 64 | 65 | grunt.registerTask('default', ['init', 'gh-pages:first', 'gh-pages:second']); 66 | }; 67 | -------------------------------------------------------------------------------- /test/different-repo.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helper = require('./helper'); 5 | 6 | const assert = helper.assert; 7 | 8 | describe('different-repo', () => { 9 | let fixture; 10 | let repo; 11 | 12 | before(function(done) { 13 | this.timeout(3000); 14 | helper.buildFixture('different-repo', (error, dir) => { 15 | if (error) { 16 | return done(error); 17 | } 18 | fixture = dir; 19 | repo = path.join(fixture, '.grunt/grunt-gh-pages/gh-pages/src'); 20 | done(); 21 | }); 22 | }); 23 | 24 | after(done => { 25 | helper.afterFixture(fixture, done); 26 | }); 27 | 28 | it('creates .grunt/grunt-gh-pages/gh-pages/src directory', done => { 29 | fs.stat(repo, (error, stats) => { 30 | assert.isTrue(!error, 'no error'); 31 | assert.isTrue(stats.isDirectory(), 'directory'); 32 | done(error); 33 | }); 34 | }); 35 | 36 | it('creates a gh-pages branch', done => { 37 | helper 38 | .git(['rev-parse', '--abbrev-ref', 'HEAD'], repo) 39 | .then(branch => { 40 | assert.strictEqual(branch, 'gh-pages\n', 'branch created'); 41 | done(); 42 | }) 43 | .catch(done); 44 | }); 45 | 46 | it('copies source files', done => { 47 | fs.exists(path.join(repo, 'hello.txt'), exists => { 48 | if (exists) { 49 | done(); 50 | } else { 51 | done(new Error('Failed to find "hello.txt" in repo: ') + repo); 52 | } 53 | }); 54 | }); 55 | 56 | it('copies correct source files', done => { 57 | fs.readFile(path.join(repo, 'hello.txt'), (err, data) => { 58 | if (err) { 59 | done(err); 60 | } else { 61 | assert.strictEqual(String(data), 'hello\n'); 62 | done(); 63 | } 64 | }); 65 | }); 66 | 67 | it('pushes the gh-pages branch to remote', done => { 68 | helper 69 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo) 70 | .then(() => { 71 | done(); 72 | }) 73 | .catch(done); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/dotfiles-option.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helper = require('./helper'); 5 | 6 | const assert = helper.assert; 7 | 8 | describe('dotfiles-option', () => { 9 | let fixture; 10 | let repo; 11 | 12 | before(function(done) { 13 | this.timeout(3000); 14 | helper.buildFixture('dotfiles-option', (error, dir) => { 15 | if (error) { 16 | return done(error); 17 | } 18 | fixture = dir; 19 | repo = path.join(fixture, '.grunt/grunt-gh-pages/gh-pages/src'); 20 | done(); 21 | }); 22 | }); 23 | 24 | after(done => { 25 | helper.afterFixture(fixture, done); 26 | }); 27 | 28 | it('creates .grunt/grunt-gh-pages/gh-pages/src directory', done => { 29 | fs.stat(repo, (error, stats) => { 30 | assert.isTrue(!error, 'no error'); 31 | assert.isTrue(stats.isDirectory(), 'directory'); 32 | done(error); 33 | }); 34 | }); 35 | 36 | it('creates a gh-pages branch', done => { 37 | helper 38 | .git(['rev-parse', '--abbrev-ref', 'HEAD'], repo) 39 | .then(branch => { 40 | assert.strictEqual(branch, 'gh-pages\n', 'branch created'); 41 | done(); 42 | }) 43 | .catch(done); 44 | }); 45 | 46 | it('pushes the gh-pages branch to remote', done => { 47 | helper 48 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo) 49 | .then(() => { 50 | done(); 51 | }) 52 | .catch(done); 53 | }); 54 | 55 | it('copies files with dots', done => { 56 | fs.exists(path.join(repo, '.one'), exists => { 57 | if (exists) { 58 | done(); 59 | } else { 60 | done(new Error('Failed to find ".one" in repo: ') + repo); 61 | } 62 | }); 63 | }); 64 | 65 | it('copies files in directories with dots', done => { 66 | fs.exists(path.join(repo, 'foo', '.bar', 'two'), exists => { 67 | if (exists) { 68 | done(); 69 | } else { 70 | done(new Error('Failed to find "foo/.bar/two" in repo: ') + repo); 71 | } 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/lib/util.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const assert = require('../helper').assert; 4 | 5 | const util = require('../../lib/util'); 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.deepStrictEqual(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.deepStrictEqual(got, expected); 68 | }); 69 | }); 70 | 71 | describe('dirsToCreate', () => { 72 | it('gets a sorted list of directories to create', () => { 73 | const got = util.dirsToCreate(files); 74 | 75 | const expected = [ 76 | '.', 77 | 'a1', 78 | 'a2', 79 | path.join('a1', 'b1'), 80 | path.join('a1', 'b2'), 81 | path.join('a2', 'b1'), 82 | path.join('a1', 'b1', 'c1'), 83 | path.join('a1', 'b1', 'c2'), 84 | path.join('a1', 'b2', 'c1'), 85 | path.join('a1', 'b2', 'c2') 86 | ]; 87 | 88 | assert.deepStrictEqual(got, expected); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | const cp = require('child_process'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const fse = require('fs-extra'); 5 | const chai = require('chai'); 6 | const tmp = require('tmp'); 7 | 8 | const fixtures = path.join(__dirname, 'fixtures'); 9 | const tmpDir = 'tmp'; 10 | 11 | /** 12 | * Spawn a Grunt process. 13 | * @param {string} dir Directory with Gruntfile.js. 14 | * @param {function(Error, Process)} done Callback. 15 | */ 16 | function spawnGrunt(dir, done) { 17 | if (fs.existsSync(path.join(dir, 'Gruntfile.js'))) { 18 | const node = process.argv[0]; 19 | const grunt = process.argv[1]; // assumes grunt drives these tests 20 | const child = cp.spawn(node, [grunt, '--verbose'], {cwd: dir}); 21 | done(null, child); 22 | } else { 23 | done(new Error(`Cannot find Gruntfile.js in dir: ${dir}`)); 24 | } 25 | } 26 | 27 | /** 28 | * Set up before running tests. 29 | * @param {string} name Fixture name. 30 | * @param {function} done Callback. 31 | */ 32 | function cloneFixture(name, done) { 33 | const fixture = path.join(fixtures, name); 34 | if (!fs.existsSync(tmpDir)) { 35 | fs.mkdirSync(tmpDir); 36 | } 37 | 38 | tmp.dir({dir: tmpDir}, (error, dir) => { 39 | if (error) { 40 | return done(error); 41 | } 42 | const scratch = path.join(dir, name); 43 | fse.copy(fixture, scratch, error => { 44 | done(error, scratch); 45 | }); 46 | }); 47 | } 48 | 49 | /** 50 | * Clone a fixture and run the default Grunt task in it. 51 | * @param {string} name Fixture name. 52 | * @param {function(Error, scratch)} done Called with an error if the task 53 | * fails. Called with the cloned fixture directory if the task succeeds. 54 | */ 55 | exports.buildFixture = function(name, done) { 56 | cloneFixture(name, (error, scratch) => { 57 | if (error) { 58 | return done(error); 59 | } 60 | spawnGrunt(scratch, (error, child) => { 61 | if (error) { 62 | return done(error); 63 | } 64 | const messages = []; 65 | child.stderr.on('data', chunk => { 66 | messages.push(chunk.toString()); 67 | }); 68 | child.stdout.on('data', chunk => { 69 | messages.push(chunk.toString()); 70 | }); 71 | child.on('close', code => { 72 | if (code === 0) { 73 | done(null, scratch); 74 | } else { 75 | done(new Error(`Task failed: ${messages.join('')}`)); 76 | } 77 | }); 78 | }); 79 | }); 80 | }; 81 | 82 | /** 83 | * Clean up after running tests. 84 | * @param {string} scratch Path to scratch directory. 85 | * @param {function} done Callback. 86 | */ 87 | exports.afterFixture = function(scratch, done) { 88 | let error; 89 | try { 90 | fse.removeSync(scratch); 91 | fse.removeSync(tmpDir); 92 | } catch (err) { 93 | error = err; 94 | } 95 | done(error); 96 | }; 97 | 98 | /** 99 | * Util function for handling spawned git processes as promises. 100 | * @param {Array.} args Arguments. 101 | * @param {string} cwd Working directory. 102 | * @return {Promise} A promise. 103 | */ 104 | exports.git = require('../lib/git'); 105 | 106 | /** @type {boolean} */ 107 | chai.config.includeStack = true; 108 | 109 | /** 110 | * Chai's assert function configured to include stacks on failure. 111 | * @type {function} 112 | */ 113 | exports.assert = chai.assert; 114 | -------------------------------------------------------------------------------- /test/add.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helper = require('./helper'); 5 | 6 | const assert = helper.assert; 7 | 8 | describe('add', () => { 9 | let fixture; 10 | let repo1; 11 | let repo2; 12 | 13 | before(function(done) { 14 | this.timeout(3000); 15 | helper.buildFixture('add', (error, dir) => { 16 | if (error) { 17 | return done(error); 18 | } 19 | fixture = dir; 20 | repo1 = path.join(fixture, '.grunt/grunt-gh-pages/gh-pages/first'); 21 | repo2 = path.join(fixture, '.grunt/grunt-gh-pages/gh-pages/second'); 22 | done(); 23 | }); 24 | }); 25 | 26 | after(done => { 27 | helper.afterFixture(fixture, done); 28 | }); 29 | 30 | /** 31 | * First target adds all files from `first` directory. 32 | */ 33 | it('creates .grunt/grunt-gh-pages/gh-pages/first directory', done => { 34 | fs.stat(repo1, (error, stats) => { 35 | if (error) { 36 | return done(error); 37 | } 38 | assert.isTrue(stats.isDirectory(), 'directory'); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('creates a gh-pages branch', done => { 44 | helper 45 | .git(['rev-parse', '--abbrev-ref', 'HEAD'], repo1) 46 | .then(branch => { 47 | assert.strictEqual(branch, 'gh-pages\n', 'branch created'); 48 | done(); 49 | }) 50 | .catch(done); 51 | }); 52 | 53 | it('copies source files relative to the base', () => { 54 | assert.strictEqual( 55 | fs.readFileSync(path.join(repo1, 'first.txt'), 'utf8'), 56 | 'first' 57 | ); 58 | assert.strictEqual( 59 | fs.readFileSync(path.join(repo1, 'sub', 'sub.txt'), 'utf8'), 60 | 'first' 61 | ); 62 | }); 63 | 64 | it('pushes the gh-pages branch to remote', done => { 65 | helper 66 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo1) 67 | .then(() => { 68 | done(); 69 | }) 70 | .catch(done); 71 | }); 72 | 73 | /** 74 | * Second target adds all files from `second` directory without removing those 75 | * from the `first`. 76 | */ 77 | it('creates .grunt/grunt-gh-pages/gh-pages/second directory', done => { 78 | fs.stat(repo2, (error, stats) => { 79 | if (error) { 80 | return done(error); 81 | } 82 | assert.isTrue(stats.isDirectory(), 'directory'); 83 | done(); 84 | }); 85 | }); 86 | 87 | it('creates a gh-pages branch', done => { 88 | helper 89 | .git(['rev-parse', '--abbrev-ref', 'HEAD'], repo2) 90 | .then(branch => { 91 | assert.strictEqual(branch, 'gh-pages\n', 'branch created'); 92 | done(); 93 | }) 94 | .catch(done); 95 | }); 96 | 97 | it('overwrites, but does not remove existing', () => { 98 | assert.strictEqual( 99 | fs.readFileSync(path.join(repo2, 'first.txt'), 'utf8'), 100 | 'first' 101 | ); 102 | assert.strictEqual( 103 | fs.readFileSync(path.join(repo2, 'second.txt'), 'utf8'), 104 | 'second' 105 | ); 106 | assert.strictEqual( 107 | fs.readFileSync(path.join(repo2, 'sub', 'sub.txt'), 'utf8'), 108 | 'second' 109 | ); 110 | }); 111 | 112 | it('pushes the gh-pages branch to remote', done => { 113 | helper 114 | .git(['ls-remote', '--exit-code', '.', 'origin/gh-pages'], repo2) 115 | .then(() => { 116 | done(); 117 | }) 118 | .catch(done); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const async = require('async'); 3 | const fs = require('graceful-fs'); 4 | 5 | /** 6 | * Generate a list of unique directory paths given a list of file paths. 7 | * @param {Array.} files List of file paths. 8 | * @return {Array.} List of directory paths. 9 | */ 10 | const uniqueDirs = (exports.uniqueDirs = files => { 11 | const dirs = {}; 12 | files.forEach(filepath => { 13 | const parts = path.dirname(filepath).split(path.sep); 14 | let partial = parts[0]; 15 | dirs[partial] = true; 16 | for (let i = 1, ii = parts.length; i < ii; ++i) { 17 | partial = path.join(partial, parts[i]); 18 | dirs[partial] = true; 19 | } 20 | }); 21 | return Object.keys(dirs); 22 | }); 23 | 24 | /** 25 | * Sort function for paths. Sorter paths come first. Paths of equal length are 26 | * sorted alphanumerically in path segment order. 27 | * @param {string} a First path. 28 | * @param {string} b Second path. 29 | * @return {number} Comparison. 30 | */ 31 | const byShortPath = (exports.byShortPath = (a, b) => { 32 | const aParts = a.split(path.sep); 33 | const bParts = b.split(path.sep); 34 | const aLength = aParts.length; 35 | const bLength = bParts.length; 36 | let cmp = 0; 37 | if (aLength < bLength) { 38 | cmp = -1; 39 | } else if (aLength > bLength) { 40 | cmp = 1; 41 | } else { 42 | let aPart; 43 | let 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 | 59 | /** 60 | * Generate a list of directories to create given a list of file paths. 61 | * @param {Array.} files List of file paths. 62 | * @return {Array.} List of directory paths ordered by path length. 63 | */ 64 | const dirsToCreate = (exports.dirsToCreate = files => { 65 | return uniqueDirs(files).sort(byShortPath); 66 | }); 67 | 68 | /** 69 | * Copy a file. 70 | * @param {Object} obj Object with src and dest properties. 71 | * @param {function(Error)} callback Callback 72 | */ 73 | const copyFile = (exports.copyFile = (obj, callback) => { 74 | let called = false; 75 | function done(err) { 76 | if (!called) { 77 | called = true; 78 | callback(err); 79 | } 80 | } 81 | 82 | const read = fs.createReadStream(obj.src); 83 | read.on('error', err => { 84 | done(err); 85 | }); 86 | 87 | const write = fs.createWriteStream(obj.dest); 88 | write.on('error', err => { 89 | done(err); 90 | }); 91 | write.on('close', ex => { 92 | done(); 93 | }); 94 | 95 | read.pipe(write); 96 | }); 97 | 98 | /** 99 | * Make directory, ignoring errors if directory already exists. 100 | * @param {string} path Directory path. 101 | * @param {function(Error)} callback Callback. 102 | */ 103 | function makeDir(path, callback) { 104 | fs.mkdir(path, err => { 105 | if (err) { 106 | // check if directory exists 107 | fs.stat(path, (err2, stat) => { 108 | if (err2 || !stat.isDirectory()) { 109 | callback(err); 110 | } else { 111 | callback(); 112 | } 113 | }); 114 | } else { 115 | callback(); 116 | } 117 | }); 118 | } 119 | 120 | /** 121 | * Copy a list of files. 122 | * @param {Array.} files Files to copy. 123 | * @param {string} base Base directory. 124 | * @param {string} dest Destination directory. 125 | * @return {Promise} A promise. 126 | */ 127 | exports.copy = function(files, base, dest) { 128 | const promise = new Promise((resolve, reject) => { 129 | const pairs = []; 130 | const destFiles = []; 131 | files.forEach(file => { 132 | const src = path.resolve(base, file); 133 | const relative = path.relative(base, src); 134 | const target = path.join(dest, relative); 135 | pairs.push({ 136 | src, 137 | dest: target 138 | }); 139 | destFiles.push(target); 140 | }); 141 | 142 | async.eachSeries(dirsToCreate(destFiles), makeDir, err => { 143 | if (err) { 144 | return reject(err); 145 | } 146 | async.each(pairs, copyFile, err => { 147 | if (err) { 148 | return reject(err); 149 | } 150 | return resolve(); 151 | }); 152 | }); 153 | }); 154 | 155 | return promise; 156 | }; 157 | -------------------------------------------------------------------------------- /lib/git.js: -------------------------------------------------------------------------------- 1 | const cp = require('child_process'); 2 | const path = require('path'); 3 | const util = require('util'); 4 | const fse = require('fs-extra'); 5 | 6 | let git = 'git'; 7 | 8 | /** 9 | * @constructor 10 | * @param {number} code Error code. 11 | * @param {string} message Error message. 12 | */ 13 | function ProcessError(code, message) { 14 | const callee = arguments.callee; 15 | Error.apply(this, [message]); 16 | Error.captureStackTrace(this, callee); 17 | this.code = code; 18 | this.message = message; 19 | this.name = callee.name; 20 | } 21 | util.inherits(ProcessError, Error); 22 | 23 | /** 24 | * Execute a git command. 25 | * @param {Array.} args Arguments (e.g. ['remote', 'update']). 26 | * @param {string} cwd Repository directory. 27 | * @return {Promise} A promise. The promise will be resolved with stdout as a string 28 | * or rejected with an error. 29 | */ 30 | exports = module.exports = function(args, cwd) { 31 | return spawn(git, args, cwd); 32 | }; 33 | 34 | /** 35 | * Set the Git executable to be used by exported methods (defaults to 'git'). 36 | * @param {string} exe Git executable (full path if not already on path). 37 | */ 38 | exports.exe = function(exe) { 39 | git = exe; 40 | }; 41 | 42 | /** 43 | * Util function for handling spawned processes as promises. 44 | * @param {string} exe Executable. 45 | * @param {Array.} args Arguments. 46 | * @param {string} cwd Working directory. 47 | * @return {Promise} A promise. 48 | */ 49 | function spawn(exe, args, cwd) { 50 | const promise = new Promise((resolve, reject) => { 51 | const child = cp.spawn(exe, args, {cwd: cwd || process.cwd()}); 52 | const stderrBuffer = []; 53 | const stdoutBuffer = []; 54 | child.stderr.on('data', chunk => { 55 | stderrBuffer.push(chunk.toString()); 56 | }); 57 | child.stdout.on('data', chunk => { 58 | stdoutBuffer.push(chunk.toString()); 59 | }); 60 | child.on('close', code => { 61 | if (code) { 62 | const msg = stderrBuffer.join('') || `Process failed: ${code}`; 63 | reject(new ProcessError(code, msg)); 64 | } else { 65 | resolve(stdoutBuffer.join('')); 66 | } 67 | }); 68 | }); 69 | return promise; 70 | } 71 | 72 | /** 73 | * Initialize repository. 74 | * @param {string} cwd Repository directory. 75 | * @return {ChildProcess} Child process. 76 | */ 77 | exports.init = function init(cwd) { 78 | return spawn(git, ['init'], cwd); 79 | }; 80 | 81 | /** 82 | * Clone a repo into the given dir if it doesn't already exist. 83 | * @param {string} repo Repository URL. 84 | * @param {string} dir Target directory. 85 | * @param {string} branch Branch name. 86 | * @param {options} options All options. 87 | * @return {Promise} A promise. 88 | */ 89 | exports.clone = function clone(repo, dir, branch, options) { 90 | return fse.pathExists(dir).then(exists => { 91 | if (exists) { 92 | return Promise.resolve(); 93 | } 94 | return fse.ensureDir(path.dirname(path.resolve(dir))).then(() => { 95 | const args = ['clone', repo, dir, '--branch', branch, '--single-branch']; 96 | if (options.depth) { 97 | args.push('--depth', options.depth); 98 | } 99 | return spawn(git, args).catch(err => { 100 | // try again without branch options 101 | return spawn(git, ['clone', repo, dir]); 102 | }); 103 | }); 104 | }); 105 | }; 106 | 107 | /** 108 | * Clean up unversioned files. 109 | * @param {string} cwd Repository directory. 110 | * @return {Promise} A promise. 111 | */ 112 | const clean = (exports.clean = function clean(cwd) { 113 | return spawn(git, ['clean', '-f', '-d'], cwd); 114 | }); 115 | 116 | /** 117 | * Hard reset to remote/branch 118 | * @param {string} remote Remote alias. 119 | * @param {string} branch Branch name. 120 | * @param {string} cwd Repository directory. 121 | * @return {Promise} A promise. 122 | */ 123 | const reset = (exports.reset = function reset(remote, branch, cwd) { 124 | return spawn(git, ['reset', '--hard', `${remote}/${branch}`], cwd); 125 | }); 126 | 127 | /** 128 | * Fetch from a remote. 129 | * @param {string} remote Remote alias. 130 | * @param {string} cwd Repository directory. 131 | * @return {Promise} A promise. 132 | */ 133 | exports.fetch = function fetch(remote, cwd) { 134 | return spawn(git, ['fetch', remote], cwd); 135 | }; 136 | 137 | /** 138 | * Checkout a branch (create an orphan if it doesn't exist on the remote). 139 | * @param {string} remote Remote alias. 140 | * @param {string} branch Branch name. 141 | * @param {string} cwd Repository directory. 142 | * @return {Promise} A promise. 143 | */ 144 | exports.checkout = function checkout(remote, branch, cwd) { 145 | const treeish = `${remote}/${branch}`; 146 | return spawn(git, ['ls-remote', '--exit-code', '.', treeish], cwd).then( 147 | () => { 148 | // branch exists on remote, hard reset 149 | return spawn(git, ['checkout', branch], cwd) 150 | .then(() => { 151 | return clean(cwd); 152 | }) 153 | .then(() => { 154 | return reset(remote, branch, cwd); 155 | }); 156 | }, 157 | error => { 158 | if (error instanceof ProcessError && error.code === 2) { 159 | // branch doesn't exist, create an orphan 160 | return spawn(git, ['checkout', '--orphan', branch], cwd); 161 | } 162 | // unhandled error 163 | return Promise.reject(error); 164 | } 165 | ); 166 | }; 167 | 168 | /** 169 | * Remove all unversioned files. 170 | * @param {string} files Files argument. 171 | * @param {string} cwd Repository directory. 172 | * @return {Promise} A promise. 173 | */ 174 | exports.rm = function rm(files, cwd) { 175 | return spawn(git, ['rm', '--ignore-unmatch', '-r', '-f', files], cwd); 176 | }; 177 | 178 | /** 179 | * Add files. 180 | * @param {string} files Files argument. 181 | * @param {string} cwd Repository directory. 182 | * @return {Promise} A promise. 183 | */ 184 | exports.add = function add(files, cwd) { 185 | return spawn(git, ['add', files], cwd); 186 | }; 187 | 188 | /** 189 | * Commit. 190 | * @param {string} message Commit message. 191 | * @param {string} cwd Repository directory. 192 | * @return {Promise} A promise. 193 | */ 194 | exports.commit = function commit(message, cwd) { 195 | return spawn(git, ['diff-index', '--quiet', 'HEAD', '.'], cwd) 196 | .then(() => { 197 | // nothing to commit 198 | return Promise.resolve(); 199 | }) 200 | .catch(() => { 201 | return spawn(git, ['commit', '-m', message], cwd); 202 | }); 203 | }; 204 | 205 | /** 206 | * Add tag 207 | * @param {string} tag Name of tag. 208 | * @param {string} cwd Repository directory. 209 | * @return {Promise} A promise. 210 | */ 211 | exports.tag = function tag(tag, cwd) { 212 | return spawn(git, ['tag', tag], cwd); 213 | }; 214 | 215 | /** 216 | * Push a branch. 217 | * @param {string} remote Remote alias. 218 | * @param {string} branch Branch name. 219 | * @param {string} cwd Repository directory. 220 | * @return {Promise} A promise. 221 | */ 222 | exports.push = function push(remote, branch, cwd) { 223 | return spawn(git, ['push', '--tags', remote, branch], cwd); 224 | }; 225 | -------------------------------------------------------------------------------- /tasks/gh-pages.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fse = require('fs-extra'); 3 | const urlSafe = require('url-safe'); 4 | const copy = require('../lib/util').copy; 5 | const git = require('../lib/git'); 6 | const pkg = require('../package.json'); 7 | 8 | function getCacheDir() { 9 | return path.join('.grunt', pkg.name); 10 | } 11 | 12 | function getRemoteUrl(dir, remote) { 13 | return git(['config', '--get', `remote.${remote}.url`], dir) 14 | .then(data => { 15 | const repo = data.split(/[\n\r]/).shift(); 16 | if (repo) { 17 | return Promise.resolve(repo); 18 | } 19 | return Promise.reject( 20 | new Error('Failed to get repo URL from options or current directory.') 21 | ); 22 | }) 23 | .catch(err => { 24 | return Promise.reject( 25 | new Error( 26 | 'Failed to get remote.origin.url (task must either be run in a ' + 27 | 'git repository with a configured origin remote or must be ' + 28 | 'configured with the "repo" option).' 29 | ) 30 | ); 31 | }); 32 | } 33 | 34 | function getRepo(options) { 35 | if (options.repo) { 36 | return Promise.resolve(options.repo); 37 | } 38 | return getRemoteUrl(process.cwd(), 'origin'); 39 | } 40 | 41 | /** @param {Object} grunt Grunt. */ 42 | module.exports = function(grunt) { 43 | grunt.registerMultiTask('gh-pages', 'Publish to gh-pages.', function() { 44 | let src; 45 | const data = this.data; 46 | const kind = grunt.util.kindOf(data); 47 | if (kind === 'string') { 48 | src = [data]; 49 | } else if (kind === 'array') { 50 | src = data; 51 | } else if (kind === 'object') { 52 | if (!('src' in data)) { 53 | grunt.fatal(new Error('Required "src" property missing.')); 54 | } 55 | src = data.src; 56 | } else { 57 | grunt.fatal(new Error(`Unexpected config: ${String(data)}`)); 58 | } 59 | 60 | const defaults = { 61 | add: false, 62 | git: 'git', 63 | clone: path.join(getCacheDir(), this.name, this.target), 64 | dotfiles: false, 65 | branch: 'gh-pages', 66 | remote: 'origin', 67 | base: process.cwd(), 68 | only: '.', 69 | push: true, 70 | message: 'Updates', 71 | silent: false 72 | }; 73 | 74 | // override defaults with any task options 75 | const options = this.options(defaults); 76 | 77 | // allow command line options to override 78 | let value; 79 | for (const option in defaults) { 80 | if (Object.prototype.hasOwnProperty.call(defaults, option)) { 81 | value = grunt.option(`${pkg.name}-${option}`); 82 | if (typeof value !== 'undefined') { 83 | options[option] = value; 84 | } 85 | } 86 | } 87 | 88 | if (!grunt.file.isDir(options.base)) { 89 | grunt.fatal(new Error('The "base" option must be an existing directory')); 90 | } 91 | 92 | const files = grunt.file.expand( 93 | { 94 | filter: 'isFile', 95 | cwd: options.base, 96 | dot: options.dotfiles 97 | }, 98 | src 99 | ); 100 | 101 | if (!Array.isArray(files) || files.length === 0) { 102 | grunt.fatal(new Error('Files must be provided in the "src" property.')); 103 | } 104 | 105 | const only = grunt.file.expand({cwd: options.base}, options.only); 106 | 107 | const done = this.async(); 108 | 109 | function log(message) { 110 | if (!options.silent) { 111 | grunt.log.writeln(message); 112 | } 113 | } 114 | 115 | git.exe(options.git); 116 | 117 | let repoUrl; 118 | getRepo(options) 119 | .then(repo => { 120 | repoUrl = repo; 121 | log(`Cloning ${urlSafe(repo, '[secure]')} into ${options.clone}`); 122 | return git.clone(repo, options.clone, options.branch, options); 123 | }) 124 | .then(() => { 125 | return getRemoteUrl(options.clone, options.remote).then(url => { 126 | if (url !== repoUrl) { 127 | const message = 128 | `Remote url mismatch. Got "${url}" ` + 129 | `but expected "${repoUrl}" in ${options.clone}. ` + 130 | 'If you have changed your "repo" option, try ' + 131 | 'running `grunt gh-pages-clean` first.'; 132 | return Promise.reject(new Error(message)); 133 | } 134 | return Promise.resolve(); 135 | }); 136 | }) 137 | .then(() => { 138 | // only required if someone mucks with the checkout between builds 139 | log('Cleaning'); 140 | return git.clean(options.clone); 141 | }) 142 | .then(() => { 143 | log(`Fetching ${options.remote}`); 144 | return git.fetch(options.remote, options.clone); 145 | }) 146 | .then(() => { 147 | log(`Checking out ${options.remote}/${options.branch}`); 148 | return git.checkout(options.remote, options.branch, options.clone); 149 | }) 150 | .then(() => { 151 | if (!options.add) { 152 | log('Removing files'); 153 | return git.rm(only.join(' '), options.clone); 154 | } 155 | return Promise.resolve(); 156 | }) 157 | .then(() => { 158 | log('Copying files'); 159 | return copy(files, options.base, options.clone); 160 | }) 161 | .then(() => { 162 | log('Adding all'); 163 | return git.add('.', options.clone); 164 | }) 165 | .then(() => { 166 | if (options.user) { 167 | return git( 168 | ['config', 'user.email', options.user.email], 169 | options.clone 170 | ).then(() => { 171 | return git( 172 | ['config', 'user.name', options.user.name], 173 | options.clone 174 | ); 175 | }); 176 | } 177 | return Promise.resolve(); 178 | }) 179 | .then(() => { 180 | log('Committing'); 181 | return git.commit(options.message, options.clone); 182 | }) 183 | .then(() => { 184 | if (options.tag) { 185 | log('Tagging'); 186 | const promise = new Promise((resolve, reject) => { 187 | git 188 | .tag(options.tag, options.clone) 189 | .then(() => { 190 | return resolve(); 191 | }) 192 | .catch(error => { 193 | // tagging failed probably because this tag alredy exists 194 | log('Tagging failed, continuing'); 195 | grunt.log.debug(error); 196 | return resolve(); 197 | }); 198 | }); 199 | return promise; 200 | } 201 | return Promise.resolve(); 202 | }) 203 | .then(() => { 204 | if (options.push) { 205 | log('Pushing'); 206 | return git.push(options.remote, options.branch, options.clone); 207 | } 208 | return Promise.resolve(); 209 | }) 210 | .then( 211 | () => { 212 | done(); 213 | }, 214 | error => { 215 | if (options.silent) { 216 | error = new Error( 217 | 'Unspecified error (run without silent option for detail)' 218 | ); 219 | } 220 | done(error); 221 | } 222 | ); 223 | }); 224 | 225 | grunt.registerTask('gh-pages-clean', 'Clean cache dir', () => { 226 | fse.removeSync(getCacheDir()); 227 | }); 228 | }; 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-gh-pages 2 | 3 | **Publish to GitHub Pages with Grunt** 4 | 5 | [![NPM version](https://img.shields.io/npm/v/grunt-gh-pages.svg)](https://www.npmjs.com/package/grunt-gh-pages) 6 | [![Build Status](https://github.com/tschaub/grunt-gh-pages/workflows/Tests/badge.svg)](https://github.com/tschaub/grunt-gh-pages/actions?workflow=Tests) 7 | [![Dependency Status](https://img.shields.io/david/tschaub/grunt-gh-pages.svg)](https://david-dm.org/tschaub/grunt-gh-pages) 8 | [![devDependency Status](https://img.shields.io/david/dev/tschaub/grunt-gh-pages.svg)](https://david-dm.org/tschaub/grunt-gh-pages?type=dev) 9 | 10 | Use [Grunt](https://gruntjs.com/) to push to your `gh-pages` branch hosted on GitHub or any other branch anywhere else. 11 | 12 | ## Getting Started 13 | 14 | This plugin requires Grunt `>=0.4.0` and Git `>=1.7.6`. 15 | 16 | If you haven't used [Grunt](https://gruntjs.com/) before, be sure to check out the [Getting Started](https://gruntjs.com/getting-started) guide, as it explains how to create a [`gruntfile.js`](https://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 17 | 18 | ```shell 19 | npm install grunt-gh-pages --save-dev 20 | ``` 21 | 22 | Once the plugin has been installed, it may be enabled inside your `gruntfile.js` with this line: 23 | 24 | ```js 25 | grunt.loadNpmTasks('grunt-gh-pages'); 26 | ``` 27 | 28 | ## The `gh-pages` task 29 | 30 | ### Overview 31 | 32 | In your project's Gruntfile, add a section named `gh-pages` to the data object passed into `initConfig`. 33 | 34 | ```js 35 | grunt.initConfig({ 36 | 'gh-pages': { 37 | options: { 38 | base: 'dist' 39 | }, 40 | src: ['**'] 41 | } 42 | }); 43 | ``` 44 | 45 | Running this task with `grunt gh-pages` 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 `dist` directory that match patterns from the`src` configuration, commit all changes, and push to the `origin` remote. 46 | 47 | 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. 48 | 49 | **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. 50 | 51 | The `gh-pages` task is a multi-task, so different targets can be configured with different `src` files and `options`. For example, to have the `gh-pages:gh-pages` target push to `gh-pages` and a second `gh-pages:foo` target push to a `bar` branch, the multi-task could be configured as follows: 52 | 53 | ```js 54 | grunt.initConfig({ 55 | 'gh-pages': { 56 | options: { 57 | // Options for all targets go here. 58 | }, 59 | 'gh-pages': { 60 | options: { 61 | base: 'build' 62 | }, 63 | // These files will get pushed to the `gh-pages` branch (the default). 64 | src: ['index.html'] 65 | }, 66 | 'foo': { 67 | options: { 68 | base: 'bar-build', 69 | branch: 'bar' 70 | }, 71 | // These files will get pushed to the `bar` branch. 72 | src: ['other.txt'] 73 | } 74 | } 75 | }); 76 | ``` 77 | 78 | 79 | ### Options 80 | 81 | The default task options work for simple cases. The options described below let you push to alternate branches, customize your commit messages, and more. 82 | 83 | Options for all targets can be configured on the task level. Individual tasks can also have their own options that override task level options. 84 | 85 | All options can be overriden with command line flags. The pattern to provide an option is like `--gh-pages-optname foo` where `optname` is the option name and `foo` is the option value. For example, to supply the [`tag`](#optionstag) and [`message`](#optionsmessage), the task could be run as follows: 86 | 87 | grunt gh-pages --gh-pages-tag 'v1.2.3' --gh-pages-message 'Tagging v1.2.3' 88 | 89 | #### options.base 90 | 91 | * type: `string` 92 | * default: `process.cwd()` 93 | 94 | The base directory for all source files (those listed in the `src` config property). By default, source files are assumed to be relative to the current working directory, and they will be copied to the target with this relative path. If your source files are all in a different directory (say, `build`), and you want them to be copied with a path relative to that directory, provide the directory path in the `base` option (e.g. `base: 'build'`). 95 | 96 | Example use of the `base` option: 97 | 98 | ```js 99 | /** 100 | * Given the following directory structure: 101 | * 102 | * build/ 103 | * index.html 104 | * js/ 105 | * site.js 106 | * 107 | * The task below will create a `gh-pages` branch that looks like this: 108 | * 109 | * index.html 110 | * js/ 111 | * site.js 112 | * 113 | */ 114 | grunt.initConfig({ 115 | 'gh-pages': { 116 | options: { 117 | base: 'build' 118 | }, 119 | src: '**/*' 120 | } 121 | }); 122 | ``` 123 | 124 | #### options.dotfiles 125 | 126 | * type: `boolean` 127 | * default: `false` 128 | 129 | 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. 130 | 131 | Example use of the `dotfiles` option: 132 | 133 | ```js 134 | /** 135 | * The task below will push dotfiles (directories and files) 136 | * that otherwise match the `src` pattern. 137 | */ 138 | grunt.initConfig({ 139 | 'gh-pages': { 140 | options: { 141 | base: 'dist', 142 | dotfiles: true 143 | }, 144 | src: '**/*' 145 | } 146 | }); 147 | ``` 148 | 149 | #### options.add 150 | 151 | * type: `boolean` 152 | * default: `false` 153 | 154 | 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 target options. 155 | 156 | Example use of the `add` option: 157 | 158 | ```js 159 | /** 160 | * The task below will only add files to the `gh-pages` branch, never removing 161 | * any existing files (even if they don't exist in the `src` config). 162 | */ 163 | grunt.initConfig({ 164 | 'gh-pages': { 165 | options: { 166 | base: 'build', 167 | add: true 168 | }, 169 | src: '**/*' 170 | } 171 | }); 172 | ``` 173 | 174 | #### options.only 175 | 176 | * type: `string` or `array of strings` 177 | * default: `'.'` 178 | 179 | When options.add is false, you may specify a filter to select the files to remove, instead of removing all files. 180 | 181 | Example of the `only` option: 182 | 183 | ```js 184 | /** 185 | * The task below will only remove the index.html and .js files from the 186 | * `gh-pages` branch before copying over files from the `src`. 187 | */ 188 | grunt.initConfig({ 189 | 'gh-pages': { 190 | options: { 191 | base: 'build', 192 | only: ['index.html', '**/*.js'] 193 | }, 194 | src: '**/*' 195 | } 196 | }); 197 | 198 | /** 199 | * The task below will only remove all files except the README.md from the 200 | * `gh-pages` branch before copying over files from the `src`. 201 | */ 202 | grunt.initConfig({ 203 | 'gh-pages': { 204 | options: { 205 | base: 'build', 206 | only: ['**/*', '!README.md'] 207 | }, 208 | src: '**/*' 209 | } 210 | }); 211 | ``` 212 | 213 | #### options.repo 214 | 215 | * type: `string` 216 | * default: url for the origin remote of the current dir (assumes a git repository) 217 | 218 | By default, the `gh-pages` task assumes that the current working directory is a git repository, and that you want to push changes to the `origin` remote. This is the most common case - your `gruntfile.js` builds static resources and the `gh-pages` task pushes them to a remote. 219 | 220 | If instead your `gruntfile.js` 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. 221 | 222 | Example use of the `repo` option: 223 | 224 | ```js 225 | /** 226 | * If the current directory is not a clone of the repository you want to work 227 | * with, set the URL for the repository in the `repo` option. This task will 228 | * push all files in the `src` config to the `gh-pages` branch of the `repo`. 229 | */ 230 | grunt.initConfig({ 231 | 'gh-pages': { 232 | options: { 233 | base: 'build', 234 | repo: 'https://example.com/other/repo.git' 235 | }, 236 | src: '**/*' 237 | } 238 | }); 239 | ``` 240 | 241 | 242 | #### options.branch 243 | 244 | * type: `string` 245 | * default: `'gh-pages'` 246 | 247 | The name of the branch you'll be pushing to. The default uses GitHub's `gh-pages` branch, but this same task can be used to push to any branch on any remote. 248 | 249 | Example use of the `branch` option: 250 | 251 | ```js 252 | /** 253 | * This task pushes to the `master` branch of the configured `repo`. 254 | */ 255 | grunt.initConfig({ 256 | 'gh-pages': { 257 | options: { 258 | base: 'build', 259 | branch: 'master', 260 | repo: 'https://example.com/other/repo.git' 261 | }, 262 | src: '**/*' 263 | } 264 | }); 265 | ``` 266 | 267 | 268 | #### options.tag 269 | 270 | * type: `string` 271 | * default: `''` 272 | 273 | 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. 274 | 275 | Example use of the `tag` option from the command line: 276 | 277 | ```shell 278 | grunt gh-pages --gh-pages-tag 'v3.2.1' 279 | ``` 280 | 281 | #### options.message 282 | 283 | * type: `string` 284 | * default: `'Updates'` 285 | 286 | The commit message for all commits. 287 | 288 | Example use of the `message` option: 289 | 290 | ```js 291 | /** 292 | * This adds commits with a custom message. 293 | */ 294 | grunt.initConfig({ 295 | 'gh-pages': { 296 | options: { 297 | base: 'build', 298 | message: 'Auto-generated commit' 299 | }, 300 | src: '**/*' 301 | } 302 | }); 303 | ``` 304 | 305 | Alternatively, this option can be set on the command line: 306 | 307 | ```shell 308 | grunt gh-pages --gh-pages-message 'Making commits' 309 | ``` 310 | 311 | 312 | #### options.user 313 | 314 | * type: `Object` 315 | * default: `null` 316 | 317 | 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. 318 | 319 | Example use of the `user` option: 320 | 321 | ```js 322 | grunt.initConfig({ 323 | 'gh-pages': { 324 | options: { 325 | base: 'build', 326 | user: { 327 | name: 'Joe Code', 328 | email: 'coder@example.com' 329 | } 330 | }, 331 | src: '**/*' 332 | } 333 | }); 334 | ``` 335 | 336 | #### options.clone 337 | 338 | * type: `string` 339 | * default: `'.grunt/grunt-gh-pages/gh-pages/repo'` 340 | 341 | Path to a directory where your repository will be cloned. If this directory doesn't already exist, it will be created. If it already exists, it is assumed to be a clone of your repository. If you stick with the default value (recommended), you will likely want to add `.grunt` to your `.gitignore` file. 342 | 343 | Example use of the `clone` option: 344 | 345 | ```js 346 | /** 347 | * If you already have a temp directory, and want the repository cloned there, 348 | * use the `clone` option as below. To avoid re-cloning every time the task is 349 | * run, this should be a directory that sticks around for a while. 350 | */ 351 | grunt.initConfig({ 352 | 'gh-pages': { 353 | options: { 354 | base: 'build', 355 | clone: 'path/to/tmp/dir' 356 | }, 357 | src: '**/*' 358 | } 359 | }); 360 | ``` 361 | 362 | 363 | #### options.push 364 | 365 | * type: `boolean` 366 | * default: `true` 367 | 368 | Push branch to remote. To commit only (with no push) set to `false`. 369 | 370 | Example use of the `push` option: 371 | 372 | ```js 373 | grunt.initConfig({ 374 | 'gh-pages': { 375 | options: { 376 | base: 'build', 377 | push: false 378 | }, 379 | src: '**/*' 380 | } 381 | }); 382 | ``` 383 | 384 | #### options.silent 385 | 386 | * type: `boolean` 387 | * default: `false` 388 | 389 | Suppress logging. This option should be used if the repository URL or other information passed to git commands is sensitive and should not be logged. With silent `true` log messages are suppressed and error messages are sanitized. 390 | 391 | Example use of the `silent` option: 392 | 393 | ```js 394 | /** 395 | * This configuration will suppress logging and sanitize error messages. 396 | */ 397 | grunt.initConfig({ 398 | 'gh-pages': { 399 | options: { 400 | base: 'build', 401 | repo: 'https://' + process.env.GH_TOKEN + '@github.com/user/private-repo.git', 402 | silent: true 403 | }, 404 | src: '**/*' 405 | } 406 | }); 407 | ``` 408 | 409 | 410 | #### options.git 411 | 412 | * type: `string` 413 | * default: `'git'` 414 | 415 | Your `git` executable. 416 | 417 | Example use of the `git` option: 418 | 419 | ```js 420 | /** 421 | * If `git` is not on your path, provide the path as shown below. 422 | */ 423 | grunt.initConfig({ 424 | 'gh-pages': { 425 | options: { 426 | base: 'build', 427 | git: '/path/to/git' 428 | }, 429 | src: '**/*' 430 | } 431 | }); 432 | ``` 433 | 434 | ## Dependencies 435 | 436 | Note that this plugin requires Git 1.7.6 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/grunt-gh-pages/issues). 437 | 438 | [![Current Status](https://secure.travis-ci.org/tschaub/grunt-gh-pages.png?branch=master)](https://travis-ci.org/tschaub/grunt-gh-pages) 439 | --------------------------------------------------------------------------------