├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── bin └── git-directory-deploy.sh ├── cliArgs.js ├── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 5 5 | - 6 6 | script: npm run lint && npm run validate && npm test 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Luke Karrys 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-directory-deploy 2 | 3 | Deploy a subdirectory from a git repo to a different branch. Useful to deploy to [GitHub Pages](https://pages.github.com/). 4 | 5 | [![Build Status](https://img.shields.io/travis/lukekarrys/git-directory-deploy/master.svg)](https://travis-ci.org/lukekarrys/git-directory-deploy) 6 | [![NPM](https://nodei.co/npm/git-directory-deploy.png)](https://nodei.co/npm/git-directory-deploy/) 7 | [![Greenkeeper badge](https://badges.greenkeeper.io/lukekarrys/git-directory-deploy.svg)](https://greenkeeper.io/) 8 | 9 | 10 | # Why 11 | 12 | The shell script portion of this code is from [X1011/git-directory-deploy](https://github.com/X1011/git-directory-deploy), 13 | and it's best explained in that README: 14 | 15 | > Unlike the git-subtree approach, it does not require the generated files be committed to the source branch. It keeps a linear history on the deploy branch and does not make superfluous commits or deploys when the generated files do not change. 16 | 17 | This repo accomplishes a few other things: 18 | 19 | - Named cli args 20 | - Publish it to [`npm`](https://www.npmjs.com/) so I can use it as a devDep in projects 21 | 22 | 23 | # Example 24 | 25 | I normally use it like this: 26 | ```sh 27 | cd project/ # Assumes a git directory with a package.json 28 | echo _build >> .gitignore 29 | npm install git-directory-deploy --save-dev 30 | touch build.js # For however you want to build your static files 31 | ``` 32 | 33 | Then add these `scripts` to `package.json`: 34 | ```js 35 | "scripts": { 36 | "build": "node build.js", // should write files to _build/ 37 | "deploy": "npm run build && git-directory-deploy --directory _build/" 38 | } 39 | ``` 40 | 41 | Then: `npm run deploy`! 42 | 43 | Check out [npm-v0-finder](https://github.com/lukekarrys/npm-v0-finder/blob/1fc7f243378ed40cfa22fe04d2a9925c18989738/package.json#L36-L37) for an example. 44 | 45 | 46 | # Install 47 | 48 | **For global use:** 49 | ```sh 50 | npm install -g git-directory-deploy 51 | 52 | cd projects/i-want-to-deploy/ 53 | 54 | git-directory-deploy --directory _dist --branch gh-pages 55 | ``` 56 | 57 | **Or for use in via [`npm run-script`](https://docs.npmjs.com/cli/run-script):** 58 | ```sh 59 | npm install git-directory-deploy --save-dev 60 | ``` 61 | and then use it in your `package.json` likes this: 62 | ```js 63 | "scripts": { 64 | "deploy": "git-directory-deploy --directory _dist --branch gh-pages" 65 | } 66 | ``` 67 | 68 | 69 | # Usage 70 | 71 | ### `git-directory-deploy [args]` 72 | 73 | #### `--directory [_site]` 74 | The subdirectory to deploy. Defaults to `_site/`. 75 | 76 | #### `--branch [gh-pages]` 77 | The branch that will receive the deploy. Defaults to `gh-pages`. 78 | 79 | #### `--repo [origin]` 80 | The repo to push the deploy to. Defaults to `origin`. 81 | 82 | #### `--username [git config user.name]` 83 | The username that will be associated with the deploy commit. This will always be set to the current `user.name` from `git config`, but if that is not set, then it can be set via this flag. 84 | 85 | #### `--email [git config user.email]` 86 | The email that will be associated with the deploy commit. This will always be set to the current `user.email` from `git config`, but if that is not set, then it can be set via this flag. 87 | 88 | #### `--message` 89 | Append something to the commit message. The message will look like this: 90 | ```sh 91 | publish: $COMMIT_MESSAGE $MESSAGE 92 | 93 | generated from commit $COMMIT_HASH 94 | ``` 95 | 96 | #### `--verbose` 97 | Be louder. 98 | 99 | #### `--allow_empty` 100 | Allow the `--directory` to be empty. 101 | 102 | #### `--ignore_removal` 103 | Deploy will not override files that are in the remote repo but not in the deploy directory. 104 | 105 | 106 | ### LICENSE 107 | 108 | MIT 109 | 110 | The script at `bin/git-directory-deploy.sh` is Copyright [Daniel Smith](https://github.com/X1011/git-directory-deploy). 111 | See the file for terms. 112 | -------------------------------------------------------------------------------- /bin/git-directory-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # git-directory-deploy.sh 3 | # https://github.com/X1011/git-directory-deploy 4 | 5 | # BSD 3-Clause License: 6 | 7 | # Copyright Daniel Smith 8 | # All rights reserved. 9 | 10 | # Redistribution and use in source and binary forms, with or without modification, 11 | # are permitted provided that the following conditions are met: 12 | 13 | # Redistributions of source code must retain the above copyright notice, this 14 | # list of conditions and the following disclaimer. 15 | 16 | # Redistributions in binary form must reproduce the above copyright notice, this 17 | # list of conditions and the following disclaimer in the documentation and/or 18 | # other materials provided with the distribution. 19 | 20 | # The names of the contributors may not be used to endorse or promote products 21 | # derived from this software without specific prior written permission. 22 | 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 27 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 28 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 30 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | 35 | set -o errexit #abort if any command fails 36 | 37 | 38 | # Start modifications 39 | # -------------------- 40 | while getopts "d:b:u:e:v:r:a:m:i:" opt; do 41 | declare "opt_$opt=${OPTARG:-0}" 42 | done 43 | 44 | #set directory and branch 45 | deploy_directory=$opt_d 46 | deploy_branch=$opt_b 47 | 48 | #if no user identity is already set in the current git environment, use this: 49 | default_username=$opt_u 50 | default_email=$opt_e 51 | 52 | #Parse arg flags 53 | verbose=$opt_v 54 | allow_empty=$opt_a 55 | ignore_removal=$opt_i 56 | 57 | # possibly append commit message 58 | append_message=${opt_m:-""} 59 | if [ -n "$append_message" ]; then 60 | append_message=" $append_message" 61 | fi 62 | 63 | #repository to deploy to. must be readable and writable. 64 | repo=$opt_r 65 | # -------------------- 66 | # End modifications 67 | 68 | 69 | #echo expanded commands as they are executed (for debugging) 70 | function enable_expanded_output { 71 | if [ $verbose ]; then 72 | set -o xtrace 73 | set +o verbose 74 | fi 75 | } 76 | 77 | #this is used to avoid outputting the repo URL, which may contain a secret token 78 | function disable_expanded_output { 79 | if [ $verbose ]; then 80 | set +o xtrace 81 | set -o verbose 82 | fi 83 | } 84 | 85 | enable_expanded_output 86 | 87 | function set_user_id { 88 | if [[ -z `git config user.name` ]]; then 89 | git config user.name "$default_username" 90 | fi 91 | if [[ -z `git config user.email` ]]; then 92 | git config user.email "$default_email" 93 | fi 94 | } 95 | 96 | function restore_head { 97 | if [[ $previous_branch = "HEAD" ]]; then 98 | #we weren't on any branch before, so just set HEAD back to the commit it was on 99 | git update-ref --no-deref HEAD $commit_hash $deploy_branch 100 | else 101 | git symbolic-ref HEAD refs/heads/$previous_branch 102 | fi 103 | 104 | git reset --mixed 105 | } 106 | 107 | if ! git diff --exit-code --quiet --cached; then 108 | echo Aborting due to uncommitted changes in the index >&2 109 | exit 1 110 | fi 111 | 112 | commit_title=`git log -n 1 --format="%s" HEAD` 113 | commit_hash=`git log -n 1 --format="%H" HEAD` 114 | previous_branch=`git rev-parse --abbrev-ref HEAD` 115 | 116 | initial_deploy() { 117 | git --work-tree "$deploy_directory" checkout --orphan $deploy_branch 118 | git --work-tree "$deploy_directory" add --all 119 | commit+push 120 | } 121 | 122 | incremental_deploy() { 123 | #make deploy_branch the current branch 124 | git symbolic-ref HEAD refs/heads/$deploy_branch 125 | 126 | #put the previously committed contents of deploy_branch branch into the index 127 | git --work-tree "$deploy_directory" reset --mixed --quiet 128 | 129 | if [ $ignore_removal ]; then 130 | git --work-tree "$deploy_directory" add . --ignore-removal 131 | git --work-tree "$deploy_directory" checkout -- . 132 | else 133 | git --work-tree "$deploy_directory" add --all 134 | fi 135 | 136 | set +o errexit 137 | diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD)$? 138 | set -o errexit 139 | case $diff in 140 | 0) echo No changes to files in $deploy_directory. Skipping commit.;; 141 | 1) commit+push;; 142 | *) 143 | echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to master, use: git symbolic-ref HEAD refs/heads/master && git reset --mixed >&2 144 | exit $diff 145 | ;; 146 | esac 147 | } 148 | 149 | commit+push() { 150 | set_user_id 151 | git --work-tree "$deploy_directory" commit -nm \ 152 | "publish: $commit_title$append_message"$'\n\n'"generated from commit $commit_hash" 153 | 154 | disable_expanded_output 155 | #--quiet is important here to avoid outputting the repo URL, which may contain a secret token 156 | git push --quiet $repo $deploy_branch 157 | enable_expanded_output 158 | } 159 | 160 | 161 | if [ ! -d "$deploy_directory" ]; then 162 | echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2 163 | exit 1 164 | fi 165 | 166 | if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then 167 | echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the -e flag." >&2 168 | exit 1 169 | fi 170 | 171 | if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then 172 | # deploy_branch exists in $repo; make sure we have the latest version 173 | disable_expanded_output 174 | git fetch --force $repo $deploy_branch:$deploy_branch 175 | enable_expanded_output 176 | fi 177 | 178 | if git show-ref --verify --quiet "refs/heads/$deploy_branch" 179 | then incremental_deploy 180 | else initial_deploy 181 | fi 182 | 183 | restore_head 184 | -------------------------------------------------------------------------------- /cliArgs.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var minimist = require('minimist') 3 | 4 | module.exports = function cliArgs (args) { 5 | // Alias all the cli arg names to the names 6 | // used by getopts in the bash script 7 | // Also make verbose, allow_empty all booleans 8 | var argv = minimist(args || [], { 9 | boolean: ['v', 'a', 'i'], 10 | alias: { 11 | d: 'directory', 12 | b: 'branch', 13 | u: 'username', 14 | e: 'email', 15 | v: 'verbose', 16 | r: ['repo', 'remote'], 17 | a: 'allow_empty', 18 | m: 'message', 19 | i: ['ignore-removal', 'ignore_removal'] 20 | }, 21 | default: { 22 | d: '_site', 23 | b: 'gh-pages', 24 | u: 'git-directory-deploy', 25 | r: 'origin' 26 | } 27 | }) 28 | 29 | // Map any non-false, single letter opt names 30 | // to an array for spawn, will end up looking like 31 | // ['-d', '_build', '-b', 'branch-name', '-e', 'you@email.com'] 32 | return _.chain(argv).map(function (value, key) { 33 | if (key === '_' || value === false || key.length > 1) { 34 | return 35 | } 36 | return ['-' + key, value] 37 | }).compact().flatten().value() 38 | } 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | var spawn = require('child_process').spawn 5 | var cliArgs = require('./cliArgs') 6 | 7 | // Get path to our bin script 8 | var scriptPath = path.join(__dirname, 'bin', 'git-directory-deploy.sh') 9 | var command = spawn(scriptPath, cliArgs(process.argv.slice(2))) 10 | 11 | // Nothing special here, just bind stdout, stderr, and exit to the process 12 | command.stdout.on('data', process.stdout.write.bind(process.stdout)) 13 | command.stderr.on('data', process.stderr.write.bind(process.stderr)) 14 | command.on('exit', process.exit.bind(process.exit)) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-directory-deploy", 3 | "description": "Deploy a git directory to a branch.", 4 | "version": "1.5.1", 5 | "author": "Luke Karrys", 6 | "bin": { 7 | "git-directory-deploy": "index.js" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/lukekarrys/git-directory-deploy/issues" 11 | }, 12 | "dependencies": { 13 | "lodash": "^4.14.2", 14 | "minimist": "^1.1.0" 15 | }, 16 | "devDependencies": { 17 | "git-validate": "^2.1.4", 18 | "standard": "^10.0.0", 19 | "tap-spec": "^4.1.1", 20 | "tape": "^4.6.0" 21 | }, 22 | "homepage": "https://github.com/lukekarrys/git-directory-deploy", 23 | "keywords": [ 24 | "deploy", 25 | "git", 26 | "github", 27 | "pages" 28 | ], 29 | "license": "MIT", 30 | "main": "index.js", 31 | "pre-commit": [ 32 | "test", 33 | "lint", 34 | "validate" 35 | ], 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/lukekarrys/git-directory-deploy" 39 | }, 40 | "scripts": { 41 | "lint": "standard", 42 | "test": "tape test/*.js | tap-spec", 43 | "validate": "npm ls >/dev/null" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const cliArgs = require('../cliArgs') 3 | 4 | const args = (a) => cliArgs(a).join(' ') 5 | 6 | const D = '_site' 7 | const B = 'gh-pages' 8 | const U = 'git-directory-deploy' 9 | const R = 'origin' 10 | 11 | test('cli args', function (t) { 12 | t.equal(args(), `-d ${D} -b ${B} -u ${U} -r ${R}`, 'Defaults args') 13 | 14 | t.equal(args(['-d', 'test']), `-d test -b ${B} -u ${U} -r ${R}`, 'Directory short') 15 | t.equal(args(['--directory', 'test']), `-d test -b ${B} -u ${U} -r ${R}`, 'Directory long') 16 | 17 | t.equal(args(['-b', 'test']), `-b test -d ${D} -u ${U} -r ${R}`, 'Branch short') 18 | t.equal(args(['--branch', 'test']), `-b test -d ${D} -u ${U} -r ${R}`, 'Branch long') 19 | 20 | t.equal(args(['-u', 'test']), `-u test -d ${D} -b ${B} -r ${R}`, 'User short') 21 | t.equal(args(['--username', 'test']), `-u test -d ${D} -b ${B} -r ${R}`, 'User long') 22 | 23 | t.equal(args(['-e', 'test']), `-e test -d ${D} -b ${B} -u ${U} -r ${R}`, 'Email short') 24 | t.equal(args(['--email', 'test']), `-e test -d ${D} -b ${B} -u ${U} -r ${R}`, 'Email long') 25 | 26 | t.equal(args(['-v']), `-v true -d ${D} -b ${B} -u ${U} -r ${R}`, 'Verbose short') 27 | t.equal(args(['-v=false']), `-v false -d ${D} -b ${B} -u ${U} -r ${R}`, 'Verbose false short') 28 | t.equal(args(['--verbose']), `-v true -d ${D} -b ${B} -u ${U} -r ${R}`, 'Verbose long') 29 | t.equal(args(['--verbose=false']), `-v false -d ${D} -b ${B} -u ${U} -r ${R}`, 'Verbose fa;se long') 30 | 31 | t.equal(args(['-r', 'test']), `-r test -d ${D} -b ${B} -u ${U}`, 'Repo short') 32 | t.equal(args(['--repo', 'test']), `-r test -d ${D} -b ${B} -u ${U}`, 'Repo long') 33 | t.equal(args(['--remote', 'test']), `-r test -d ${D} -b ${B} -u ${U}`, 'Remote long') 34 | 35 | t.equal(args(['-a']), `-a true -d ${D} -b ${B} -u ${U} -r ${R}`, 'Empty short') 36 | t.equal(args(['-a=false']), `-a false -d ${D} -b ${B} -u ${U} -r ${R}`, 'Empty false short') 37 | t.equal(args(['--allow_empty']), `-a true -d ${D} -b ${B} -u ${U} -r ${R}`, 'Empty long') 38 | t.equal(args(['--allow_empty=false']), `-a false -d ${D} -b ${B} -u ${U} -r ${R}`, 'Empty false long') 39 | 40 | t.equal(args(['-m', 'test']), `-m test -d ${D} -b ${B} -u ${U} -r ${R}`, 'Message short') 41 | t.equal(args(['--message', 'test']), `-m test -d ${D} -b ${B} -u ${U} -r ${R}`, 'Message long') 42 | 43 | t.equal(args(['-i']), `-i true -d ${D} -b ${B} -u ${U} -r ${R}`, 'Ignore removal short') 44 | t.equal(args(['-i=false']), `-i false -d ${D} -b ${B} -u ${U} -r ${R}`, 'Ignore removal false short') 45 | t.equal(args(['--ignore-removal']), `-i true -d ${D} -b ${B} -u ${U} -r ${R}`, 'Ignore removal long') 46 | t.equal(args(['--ignore-removal=false']), `-i false -d ${D} -b ${B} -u ${U} -r ${R}`, 'Ignore removal false long') 47 | t.equal(args(['--ignore_removal']), `-i true -d ${D} -b ${B} -u ${U} -r ${R}`, 'Ignore removal long 2') 48 | t.equal(args(['--ignore_removal=false']), `-i false -d ${D} -b ${B} -u ${U} -r ${R}`, 'Ignore removal false long 2') 49 | 50 | t.end() 51 | }) 52 | --------------------------------------------------------------------------------