├── .gitignore ├── Procfile ├── .babelrc ├── .npmrc ├── images └── screenshot.png ├── circle.yml ├── test └── A │ ├── bar.html │ ├── index.html │ └── foo.html ├── .travis.yml ├── src ├── app-name.js ├── sub-app.js ├── test │ └── app-name-spec.js ├── repo-url.js ├── config.js ├── repo-url-spec.js └── repo.js ├── app ├── partials │ └── footer.jade ├── git-pages-app.js ├── controller.js └── index.jade ├── bin └── git-pages.js ├── git-pages.config.js ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015"] } 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kensho-archive/git-pages/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | demo: 3 | branch: master 4 | heroku: 5 | appname: git-pages 6 | -------------------------------------------------------------------------------- /test/A/bar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

this is bar.html

8 | go to / 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/A/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

A

8 | go to foo.html 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/A/foo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

this is foo.html

8 | go to bar.html 9 | 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | after_success: 15 | - npm run semantic-release 16 | branches: 17 | except: 18 | - "/^v\\d+\\.\\d+\\.\\d+$/" 19 | -------------------------------------------------------------------------------- /src/app-name.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass') 2 | var check = require('check-more-types'); 3 | 4 | // url is something like http://localhost:8765/foo-bar/baz 5 | // and returns foo-bar 6 | function getAppName(url) { 7 | la(check.webUrl(url), 'invalid web url', url); 8 | var webApp = url.split('/')[3]; 9 | return webApp; 10 | } 11 | 12 | module.exports = getAppName; 13 | -------------------------------------------------------------------------------- /app/partials/footer.jade: -------------------------------------------------------------------------------- 1 | footer 2 | .clear-fix 3 | p.left.smaller © 2015 Kensho. All Rights Reserved. MIT License. 4 | ul.inline-list.smaller 5 | li 6 | a(href='https://github.com/kensho/git-pages') kensho/git-pages 7 | li 8 | a(href='https://github.com/kensho/git-pages/issues') Issues 9 | li 10 | p.faint.smaller version #{pkg.version} 11 | -------------------------------------------------------------------------------- /src/sub-app.js: -------------------------------------------------------------------------------- 1 | var quote = require('quote'); 2 | var join = require('path').join; 3 | var getAppName = require('./app-name'); 4 | 5 | function directToSubApp(req, res, next) { 6 | var from = req.headers.referer; 7 | if (from) { 8 | console.log('req url %s path %s from %s', req.url, req.path, from); 9 | var webApp = getAppName(from); 10 | if (webApp) { 11 | console.log('web app name', quote(webApp)); 12 | req.url = '/' + join(webApp, req.url); 13 | req.originalUrl = req.path = req.url; 14 | console.log('rewritten request url %s', req.url); 15 | } 16 | } 17 | next(); 18 | } 19 | 20 | module.exports = directToSubApp; 21 | -------------------------------------------------------------------------------- /src/test/app-name-spec.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass') 2 | import getAppName from '../app-name' 3 | import check from 'check-more-types' 4 | 5 | /* global describe, it */ 6 | describe('app-name', () => { 7 | it('is a function', () => { 8 | la(check.fn(getAppName)) 9 | }) 10 | 11 | it('returns expected output', () => { 12 | const url = 'http://localhost:8765/foo-bar' 13 | const name = getAppName(url) 14 | la(name === 'foo-bar') 15 | }) 16 | 17 | it('does not care about trailing slash', () => { 18 | const url = 'http://localhost:8765/foo/' 19 | const name = getAppName(url) 20 | la(name === 'foo') 21 | }) 22 | 23 | it('only returns first app name', () => { 24 | const url = 'http://localhost:8765/foo/bar' 25 | const name = getAppName(url) 26 | la(name === 'foo') 27 | }) 28 | 29 | it('throws error for invalid urls', () => { 30 | const badUrl = 'localhost:8765/foo' 31 | la(check.raises(() => { 32 | getAppName(badUrl) 33 | })) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /app/git-pages-app.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | var m = angular.module('git-pages', ['Alertify', 'http-estimate']); 3 | m.controller('pullController', function ($scope, $http, $timeout, Alertify) { 4 | $scope.pull = function pull(name) { 5 | console.log('pulling latest code for repo', name); 6 | $http.get('/pull/' + name) 7 | .then(function (response) { 8 | var commit = response.data; 9 | 10 | if (commit) { 11 | la(check.object(commit) && 12 | check.commitId(commit.hash), 'expected short commit id', commit, 13 | 'after pulling', name); 14 | console.log('pulled repo', name, commit); 15 | Alertify.success('Pulled repo', name, commit.hash.substr(0, 7), commit.committerDateRel); 16 | } else { 17 | Alertify.success('Pulled repo', name); 18 | } 19 | 20 | $timeout(function () { 21 | location.reload(true); 22 | }, 2000); 23 | }, function (err) { 24 | console.error(err); 25 | Alertify.error('Could not pull repo', name, err); 26 | }); 27 | }; 28 | }); 29 | }(window.angular)); 30 | -------------------------------------------------------------------------------- /src/repo-url.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass'); 2 | var check = require('check-more-types'); 3 | var quote = require('quote'); 4 | 5 | function userRepoPair(str) { 6 | var userRepo = /^[\w-]+\/[\w-]+$/; 7 | return check.unemptyString(str) && 8 | userRepo.test(str); 9 | } 10 | 11 | function isHttps(str) { 12 | var startsWithHttps = /^https:\/\//; 13 | return startsWithHttps.test(str); 14 | } 15 | 16 | function fullGithubSSHUrl(name) { 17 | console.log('assuming repo name on github %s via SSH', quote(name)); 18 | la(userRepoPair(name), 'expected github user/repo string', name); 19 | return 'git@github.com:' + name + '.git'; 20 | } 21 | 22 | function fullGithubHTTPSUrl(name) { 23 | console.log('assuming repo name on github %s via HTTPS', quote(name)); 24 | la(userRepoPair(name), 'expected github user/repo string', name); 25 | return 'https://github.com/' + name + '.git'; 26 | } 27 | 28 | function fullGitUrl(name, useHttps) { 29 | la(check.unemptyString(name), 'expected repo name or url', name); 30 | if (check.git(name)) { 31 | return name; 32 | } 33 | if (isHttps(name)) { 34 | return name; 35 | } 36 | return useHttps ? fullGithubHTTPSUrl(name) : fullGithubSSHUrl(name); 37 | } 38 | 39 | module.exports = fullGitUrl; 40 | -------------------------------------------------------------------------------- /bin/git-pages.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* global process, require */ 4 | /* eslint new-cap: 0 */ 5 | /* eslint no-console: 0 */ 6 | require('lazy-ass'); 7 | var check = require('check-more-types'); 8 | var R = require('ramda'); 9 | var pkg = require('../package.json'); 10 | console.log('%s@%s - %s', pkg.name, pkg.version, pkg.description); 11 | 12 | require('update-notifier')({ 13 | packageName: pkg.name, 14 | packageVersion: pkg.version 15 | }).notify(); 16 | 17 | var nopt = require('nopt'); 18 | var knownOptions = { 19 | repo: String, 20 | branch: String, 21 | index: String, 22 | help: Boolean 23 | }; 24 | var shortHands = { 25 | r: ['--repo'], 26 | git: ['--repo'], 27 | g: ['--repo'], 28 | b: ['--branch'], 29 | i: ['--index'], 30 | p: ['--index'], 31 | h: ['--help'] 32 | } 33 | var cliOptions = nopt(knownOptions, shortHands, process.argv); 34 | if (cliOptions.help) { 35 | process.exit(0); 36 | } 37 | 38 | var gitPages = require('..'); 39 | if (process.argv.length > 2) { 40 | if (check.not.unemptyString(cliOptions.repo)) { 41 | console.error('missing repo'); 42 | process.exit(1); 43 | } 44 | 45 | gitPages({ 46 | git: cliOptions.repo, 47 | branch: cliOptions.branch, 48 | index: cliOptions.index 49 | }); 50 | } else { 51 | gitPages(); 52 | } 53 | -------------------------------------------------------------------------------- /git-pages.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | List the git repos to clone and serve. 3 | For example, to serve demo.html from 'user/A' under /A 4 | 5 | module.exports = { 6 | // possible additional settings 7 | // covering hosting (local folder, port) 8 | 9 | repos: { 10 | 'A': { 11 | git: 'user/A', 12 | branch: 'master', // default: master 13 | index: 'demo.html' // default: index.html 14 | } 15 | } 16 | }; 17 | */ 18 | module.exports = { 19 | repos: { 20 | /* 21 | 'code-box': { 22 | // can use full git SSH url 23 | // git: 'git@github.com:bahmutov/code-box.git' 24 | // or the full HTTPS url 25 | git: 'https://github.com/bahmutov/code-box.git' 26 | // or just username/repo (assumes github in this case) 27 | // git: 'bahmutov/code-box' 28 | }, 29 | 'local-angular-development': { 30 | git: 'bahmutov/local-angular-development', 31 | branch: 'gh-pages' 32 | },*/ 33 | 'git-pages': { 34 | git: 'kensho/git-pages', 35 | index: 'README.md', 36 | exec: 'npm version' 37 | }, 38 | 'ndc2015-testjs': { 39 | // git: 'git@github.com:kubawalinski/ndc2015-testjs.git', 40 | git: 'https://github.com/kubawalinski/ndc2015-testjs.git', 41 | index: 'slides-testjs.html' 42 | }, 43 | 'A': { 44 | folder: 'test/A' 45 | }, 46 | 'B': { 47 | folder: 'test', 48 | index: 'A/index.html' 49 | } 50 | }, 51 | port: 8765, // serving port, optional 52 | useHttps: true // form full urls from user / repo using ssh or https 53 | }; 54 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var exists = require('fs').existsSync; 4 | var join = require('path').join; 5 | var R = require('ramda'); 6 | var check = require('check-more-types'); 7 | var os = require('os'); 8 | 9 | function firstFoundConfig(name) { 10 | var full = join(process.cwd(), name); 11 | if (exists(full)) { 12 | return full; 13 | } 14 | full = join(__dirname, '..', name); 15 | if (exists(full)) { 16 | return full; 17 | } 18 | } 19 | 20 | function mergeCliWithConfig(options) { 21 | options = options || {}; 22 | 23 | // TODO read run config using nconf 24 | var tmpFolder = os.tmpdir() 25 | var storageFolder = join(tmpFolder, 'git-pages') 26 | console.log('storage folder', storageFolder) 27 | var defaultConfig = { 28 | repos: {}, 29 | storagePath: storageFolder, 30 | port: 8765, 31 | useHttps: false, 32 | }; 33 | 34 | var foundConfigFilename = firstFoundConfig('git-pages.config.js'); 35 | if (!foundConfigFilename) { 36 | throw new Error('Cannot find the config file'); 37 | } 38 | var userConfig = R.merge(defaultConfig, require(foundConfigFilename)); 39 | 40 | var defaultRepo = { 41 | git: '', 42 | branch: 'master', 43 | index: 'index.html', 44 | exec: '' 45 | }; 46 | 47 | if (check.object(options) && 48 | check.not.empty(options)) { 49 | options = R.pickBy(check.defined, options); 50 | console.log('using command line options', options); 51 | userConfig.repos = { 52 | repo: R.merge(defaultRepo, options) 53 | }; 54 | console.log(userConfig.repos); 55 | } else { 56 | userConfig.repos = R.mapObj(R.merge(defaultRepo), userConfig.repos); 57 | } 58 | return userConfig; 59 | } 60 | 61 | module.exports = R.once(mergeCliWithConfig); 62 | -------------------------------------------------------------------------------- /app/controller.js: -------------------------------------------------------------------------------- 1 | var jade = require('jade'); 2 | var join = require('path').join; 3 | var extname = require('path').extname; 4 | var dirname = require('path').dirname; 5 | var R = require('ramda'); 6 | var fromThisFolder = R.partial(join, __dirname); 7 | var fs = require('fs'); 8 | var read = R.partialRight(fs.readFileSync, 'utf8'); 9 | 10 | // url to local path resolution 11 | var dependencies = { 12 | '/app/git-pages-app.js': './git-pages-app.js', 13 | '/app/dist/ng-alertify.js': '../node_modules/ng-alertify/dist/ng-alertify.js', 14 | '/app/dist/ng-alertify.css': '../node_modules/ng-alertify/dist/ng-alertify.css', 15 | '/app/dist/ng-http-estimate.js': '../node_modules/ng-http-estimate/dist/ng-http-estimate.js', 16 | '/app/dist/ng-http-estimate.css': '../node_modules/ng-http-estimate/dist/ng-http-estimate.css' 17 | }; 18 | 19 | // index page application 20 | function indexApp(app, repoConfig) { 21 | 22 | var pkg = require('../package.json'); 23 | 24 | Object.keys(repoConfig).forEach(function (name) { 25 | var config = repoConfig[name]; 26 | var appPath = join(name, config.index); 27 | config.path = dirname(appPath); 28 | }); 29 | 30 | app.get('/', function (req, res) { 31 | var render = jade.compileFile(fromThisFolder('./index.jade'), { pretty: true }); 32 | 33 | console.log('git repos'); 34 | console.log(repoConfig); 35 | 36 | var data = { 37 | repos: repoConfig, 38 | pkg: pkg 39 | }; 40 | var html = render(data); 41 | res.send(html); 42 | }); 43 | 44 | R.keys(dependencies).forEach(function (url) { 45 | var localPath = dependencies[url]; 46 | app.get(url, function (req, res) { 47 | var full = fromThisFolder(localPath); 48 | res.type(extname(localPath)); 49 | res.send(read(full)); 50 | }); 51 | }); 52 | } 53 | 54 | module.exports = indexApp; 55 | -------------------------------------------------------------------------------- /src/repo-url-spec.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass'); 2 | var check = require('check-more-types'); 3 | var fullUrl = require('./repo-url'); 4 | 5 | describe('join and folder path', function () { 6 | var join = require('path').join; 7 | var dirname = require('path').dirname; 8 | 9 | it('joins folder and file', function () { 10 | var joined = join('foo', 'index.html'); 11 | var dir = dirname(joined); 12 | la(dir === 'foo', dir); 13 | }); 14 | 15 | it('joins longer folder and file', function () { 16 | var joined = join('foo/bar', 'index.html'); 17 | var dir = dirname(joined); 18 | la(dir === 'foo/bar', dir); 19 | }); 20 | 21 | it('joins folder and longer file', function () { 22 | var joined = join('foo', 'bar/index.html'); 23 | var dir = dirname(joined); 24 | la(dir === 'foo/bar', dir); 25 | }); 26 | }); 27 | 28 | describe('getting full git url', function () { 29 | it('resolves to github', function () { 30 | var full = fullUrl('foo/bar'); 31 | la(full.indexOf('github.com') !== -1, 'cannot find github', full); 32 | la(full.indexOf('foo/bar') !== -1, 'cannot find foo/bar', full); 33 | la(check.git(full), full); 34 | }); 35 | 36 | it('foo21/bar3', function () { 37 | var full = fullUrl('foo21/bar3'); 38 | la(check.git(full), full); 39 | }); 40 | 41 | it('foo-bar/baz', function () { 42 | var full = fullUrl('foo-bar/baz'); 43 | la(check.git(full), full); 44 | }); 45 | 46 | it('resolves to full if git:', function () { 47 | var name = 'git@github.com:bahmutov/code-box.git'; 48 | var full = fullUrl(name); 49 | la(full === name, 'full', full, 'does not match input', name); 50 | }); 51 | 52 | it('resolves to full if https:', function () { 53 | var name = 'https://github.com/foo/bar.git'; 54 | var full = fullUrl(name); 55 | la(full === name, 'full', full, 'does not match input', name); 56 | }); 57 | 58 | it('throws if not user / repo pair', function () { 59 | la(check.raises(function () { 60 | var name = 'foo/bar/baz'; 61 | fullUrl(name); 62 | })); 63 | 64 | la(check.raises(function () { 65 | var name = 'foo'; 66 | fullUrl(name); 67 | })); 68 | 69 | la(check.raises(function () { 70 | var name = 'foo/'; 71 | fullUrl(name); 72 | })); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-pages", 3 | "description": "Run your own github-like static pages", 4 | "main": "index.js", 5 | "version": "0.0.0-semantic-release", 6 | "bugs": { 7 | "url": "https://github.com/kensho/git-pages/issues" 8 | }, 9 | "bin": { 10 | "git-pages": "./bin/git-pages.js" 11 | }, 12 | "preferGlobal": true, 13 | "files": [ 14 | "index.js", 15 | "git-pages.config.js", 16 | "bin", 17 | "images", 18 | "app", 19 | "src/**/*.js", 20 | "!src/**/*-spec.js", 21 | "Procfile" 22 | ], 23 | "homepage": "https://github.com/kensho/git-pages", 24 | "scripts": { 25 | "unit": "mocha --compilers js:babel-register src/**/*-spec.js", 26 | "test": "npm run unit", 27 | "start": "node bin/git-pages.js", 28 | "watch": "nodemon bin/git-pages.js", 29 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 30 | "issues": "git-issues", 31 | "format": "standard-format -w index.js src/**/*.js", 32 | "lint": "standard --verbose index.js src/**/*.js", 33 | "pretest": "npm run format && npm run lint", 34 | "commit": "commit-wizard", 35 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";" 36 | }, 37 | "author": "Martin Camacho ", 38 | "contributors": [ 39 | "Gleb Bahmutov " 40 | ], 41 | "license": "MIT", 42 | "dependencies": { 43 | "chdir-promise": "0.2.1", 44 | "check-more-types": "2.12.1", 45 | "express": "4.13.3", 46 | "ggit": "1.1.4", 47 | "gift": "0.6.1", 48 | "gitlog": "2.1.1", 49 | "jade": "1.11.0", 50 | "lazy-ass": "1.4.0", 51 | "marked": "0.3.5", 52 | "morgan": "1.6.1", 53 | "ncp": "2.0.0", 54 | "ng-alertify": "0.9.0", 55 | "ng-http-estimate": "0.6.0", 56 | "nopt": "3.0.4", 57 | "promised-exec": "1.0.1", 58 | "q": "2.0.3", 59 | "quote": "0.4.0", 60 | "ramda": "0.17.1", 61 | "update-notifier": "0.5.0" 62 | }, 63 | "devDependencies": { 64 | "babel-preset-es2015": "6.6.0", 65 | "babel-register": "6.7.2", 66 | "git-issues": "1.2.0", 67 | "mocha": "2.3.3", 68 | "mocha-traceur": "2.1.0", 69 | "nodemon": "1.7.0", 70 | "pre-git": "3.7.1", 71 | "semantic-release": "^4.3.5", 72 | "standard": "6.0.8", 73 | "standard-format": "2.1.1" 74 | }, 75 | "repository": { 76 | "type": "git", 77 | "url": "https://github.com/kensho/git-pages.git" 78 | }, 79 | "config": { 80 | "pre-git": { 81 | "commit-msg": "simple", 82 | "pre-commit": ["npm test"], 83 | "pre-push": ["npm run size"], 84 | "post-commit": [], 85 | "post-merge": [] 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset="utf-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1.0") 6 | meta(name="version", content="#{pkg.version}") 7 | 8 | link(rel="stylesheet", href="//cdnjs.cloudflare.com/ajax/libs/foundation/5.5.2/css/foundation.min.css") 9 | link(rel="stylesheet", href="app/dist/ng-alertify.css") 10 | link(rel="stylesheet", href="app/dist/ng-http-estimate.css") 11 | 12 | style(type="text/css"). 13 | .smaller { 14 | font-size: smaller; 15 | } 16 | footer { 17 | position: absolute; 18 | bottom: 0; 19 | } 20 | .faint { 21 | color: #aaa; 22 | } 23 | 24 | 25 | script(src="//code.jquery.com/jquery-2.1.4.min.js") 26 | script(src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js") 27 | script(src="//cdn.rawgit.com/philbooth/check-types.js/7c156cc34f43dbfdd8d4ae873b27700b311b7483/src/check-types.min.js") 28 | script(src="//cdn.rawgit.com/bahmutov/lazy-ass/gh-pages/index.js") 29 | script(src="//cdn.rawgit.com/kensho/check-more-types/master/check-more-types.min.js") 30 | script(src="app/dist/ng-alertify.js") 31 | script(src="app/dist/ng-http-estimate.js") 32 | script(src="app/git-pages-app.js") 33 | title git-pages 34 | body(ng-app="git-pages") 35 | http-estimate 36 | .row 37 | .small-12.large-centered.columns 38 | h1 git-pages 39 | table(ng-controller="pullController") 40 | thead 41 | tr 42 | td(width="200") name 43 | td is git 44 | td(width="300") git or folder 45 | td branch 46 | td link 47 | td pull 48 | td pulled commit 49 | tbody 50 | each repo, name in repos 51 | tr 52 | td= name 53 | if repo.git 54 | td yes 55 | else 56 | td no 57 | td 58 | if repo.git 59 | a(href="https://github.com/#{repo.git}/tree/#{repo.branch}", 60 | target="_blank", 61 | title="Open the git page") #{repo.git} 62 | else if repo.folder 63 | span #{repo.folder} 64 | td= repo.branch 65 | td 66 | a(href="./#{repo.path}/", target="_blank", title="Open the static page in the new tab") open 67 | td 68 | a(title="Pull latest code from the repo", ng-click="pull('#{name}')") pull 69 | td 70 | if repo.commit 71 | #{repo.commit.hash.substr(0, 7)} (#{repo.commit.committerDateRel}) 72 | .row 73 | include partials/footer 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/repo.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass'); 2 | var check = require('check-more-types'); 3 | var quote = require('quote'); 4 | var join = require('path').join; 5 | var Q = require('q'); 6 | var R = require('ramda'); 7 | var fs = require('fs'); 8 | var exists = fs.existsSync; 9 | var git = require('gift'); 10 | var gitlog = Q.denodeify(require('gitlog')); 11 | var ggit = require('ggit'); 12 | var chdir = require('chdir-promise'); 13 | var ncp = require('ncp').ncp; 14 | 15 | function copyFolder(source, destination) { 16 | la(check.unemptyString(source), 'expected source folder', source); 17 | var defer = Q.defer(); 18 | console.log('Copying %s to %s', quote(source), quote(destination)); 19 | ncp(source, destination, function (err) { 20 | if (err) { 21 | console.error('error copying %s to %s', quote(source), quote(destination)); 22 | console.error(err); 23 | defer.reject(check.array(err) ? err[0] : err); 24 | } else { 25 | defer.resolve(destination); 26 | } 27 | }); 28 | return defer.promise; 29 | } 30 | 31 | function cloneRepo(storagePath, toFullUrl, repoName, info) { 32 | la(check.unemptyString(storagePath), 'missing storage path', storagePath); 33 | la(check.unemptyString(repoName), 'missing repo name', repoName); 34 | la(check.fn(toFullUrl), 'expected full url function', toFullUrl); 35 | 36 | var repoPath = join(storagePath, repoName); 37 | var repoCloned = Q(repoPath); 38 | 39 | if (!exists(repoPath)) { 40 | 41 | if (check.has(info, 'git') && check.unemptyString(info.git)) { 42 | console.log('forming full git', info); 43 | var url = toFullUrl(info.git); 44 | console.log('cloning repo %s from %s to %s', 45 | quote(repoName), quote(url), quote(repoPath)); 46 | 47 | repoCloned = Q(git).ninvoke('clone', url, repoPath) 48 | .then(R.always(repoPath)) 49 | .catch(function (err) { 50 | console.log('Error cloning:', repoName, err); 51 | throw err; 52 | }); 53 | } else if (check.has(info, 'folder')) { 54 | // copy folder 55 | repoCloned = copyFolder(info.folder, repoPath); 56 | } else { 57 | throw new Error('Cannot determine how to clone / copy source repo ' + 58 | JSON.stringify(info)); 59 | } 60 | 61 | } 62 | return repoCloned; 63 | } 64 | 65 | function pullRepo(storagePath, repoName, branch) { 66 | la(check.unemptyString(repoName), 'missing repo name', repoName); 67 | la(check.unemptyString(branch), 'missing repo branch', repoName, branch); 68 | 69 | var repoPath = join(storagePath, repoName); 70 | var repo = git(repoPath); 71 | console.log('pulling repo %s repo path %s', quote(repoName), quote(repoPath)); 72 | console.log('working folder %s', process.cwd()); 73 | 74 | return Q.ninvoke(repo, 'remote_fetch', 'origin') 75 | .then(function () { 76 | console.log('finished pulling repo %s', quote(repoName)); 77 | }) 78 | .catch(function (err) { 79 | console.error('Error pulling repo %s\n %s', repoName, err.message); 80 | throw err; 81 | }) 82 | .then(function () { 83 | console.log('checking out branch %s in %s', branch, quote(repoName)); 84 | return Q.ninvoke(repo, 'reset', 'origin/' + branch, {hard: true}); 85 | }) 86 | .then(function () { 87 | console.log('returning full path', repoPath); 88 | return repoPath; 89 | }); 90 | } 91 | 92 | function lastCommitId(storagePath, repoName) { 93 | var repoPath = join(storagePath, repoName); 94 | return chdir.to(repoPath) 95 | .then(ggit.lastCommitId) 96 | .tap(chdir.back); 97 | } 98 | 99 | function lastCommit(storagePath, repoName) { 100 | var repoPath = join(storagePath, repoName); 101 | var logOpts = { 102 | repo: repoPath, 103 | number: 1, 104 | fields: ['hash', 'subject', 'committerDateRel'] 105 | }; 106 | return gitlog(logOpts).then(R.prop(0)); 107 | } 108 | 109 | function formExec(command, localPath) { 110 | la(check.maybe.string(command), 'invalid repo exec command', command); 111 | la(check.unemptyString(localPath), 'missing local path', localPath); 112 | 113 | if (!command) { 114 | console.log('there is no shell command for %s', localPath); 115 | return Q.when(); 116 | } 117 | var chdir = require('chdir-promise'); 118 | var exec = require('promised-exec'); 119 | 120 | if (check.unemptyString(command)) { 121 | console.log('exec %s in %s', quote(command), localPath); 122 | return chdir.to(localPath) 123 | .then(R.partial(exec, command)) 124 | .then(console.log.bind(console)) 125 | .then(chdir.back); 126 | } 127 | 128 | return Q.when(); 129 | } 130 | 131 | function shellCommand(repoConfig, localPath) { 132 | console.log('Executing shell command in %s', localPath); 133 | la(check.unemptyString(localPath), 'expected local path', localPath); 134 | var step = formExec(repoConfig, localPath); 135 | la(check.promise(step), 'expected to form a promise'); 136 | return step; 137 | }; 138 | 139 | module.exports = function init(options) { 140 | la(check.object(options), 'missing options'); 141 | 142 | var fullGitUrl = R.partialRight(require('./repo-url'), options.useHttps); 143 | 144 | return { 145 | clone: R.partial(cloneRepo, options.storagePath, fullGitUrl), 146 | pull: R.partial(pullRepo, options.storagePath), 147 | lastCommitId: R.partial(lastCommitId, options.storagePath), 148 | lastCommit: R.partial(lastCommit, options.storagePath), 149 | shell: shellCommand 150 | }; 151 | }; 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-pages 2 | 3 | > Run your own github-like static pages 4 | 5 | [![NPM][git-pages-icon] ][git-pages-url] 6 | 7 | [![Build status][git-pages-ci-image] ][git-pages-ci-url] 8 | [![dependencies][git-pages-dependencies-image] ][git-pages-dependencies-url] 9 | [![devdependencies][git-pages-devdependencies-image] ][git-pages-devdependencies-url] 10 | [![semantic-release][semantic-image] ][semantic-url] 11 | 12 | [git-pages-icon]: https://nodei.co/npm/git-pages.png?downloads=true 13 | [git-pages-url]: https://npmjs.org/package/git-pages 14 | [git-pages-ci-image]: https://travis-ci.org/kensho/git-pages.png?branch=master 15 | [git-pages-ci-url]: https://travis-ci.org/kensho/git-pages 16 | [git-pages-dependencies-image]: https://david-dm.org/kensho/git-pages.png 17 | [git-pages-dependencies-url]: https://david-dm.org/kensho/git-pages 18 | [git-pages-devdependencies-image]: https://david-dm.org/kensho/git-pages/dev-status.png 19 | [git-pages-devdependencies-url]: https://david-dm.org/kensho/git-pages#info=devDependencies 20 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 21 | [semantic-url]: https://github.com/semantic-release/semantic-release 22 | 23 | [Demo on Heroku](http://git-pages.herokuapp.com/) - might be asleep. 24 | 25 | We love [Github pages](https://pages.github.com/) - a great way to show small JS / HTML library in action. 26 | A lot of our public repos have them, but what about the private repos? There is no way (aside from 27 | buying [Github Enterprise](https://enterprise.github.com/features)) to have *privately hosted* static 28 | pages from a list of your private repos. Unless you use **git-pages**! Just install, put names of 29 | repos into a config file and start using Node. 30 | 31 | ![git-pages screenshot](images/screenshot.png) 32 | 33 | The main index page shows list of pulled repos. Click on the "open" link to open the statically hosted 34 | site pointing at the desired branch. Click "pull" to fetch the latest code. 35 | 36 | ## Install 37 | 38 | * Install globally `npm install -g git-pages` to run as a CLI in any folder 39 | * Install as a dependency `npm install --save git-pages` to start as NPM script from another project 40 | 41 | ## Serve single repo 42 | 43 | If you just need to pull a repo and statically serve a single file, use command line arguments. 44 | For example to serve the RevealJs presentation from https://github.com/kubawalinski/ndc2015-testjs 45 | you can execute this command 46 | 47 | git-pages --repo git@github.com:kubawalinski/ndc2015-testjs.git --page slides-testjs.html 48 | 49 | Run `git-pages --help` to see all options and shortcuts. 50 | 51 | ## Configure 52 | 53 | Create `git-pages.configure.js` file that exports the configuration options, for example, 54 | here are 2 repos to be hosted under `http://localhost:8765/code-box` and 55 | `http://localhost:8765/local-angular`. 56 | 57 | ```js 58 | module.exports = { 59 | repos: { 60 | 'code-box': { 61 | // you can use full git@ url 62 | git: 'git@github.com:bahmutov/code-box.git', 63 | // or the full HTTPS url 64 | // git: 'https://github.com/bahmutov/code-box.git', 65 | // pick the index page from the repo, supports HTML, Markdown, Jade 66 | index: 'demo.html' // default is index.html, 67 | exec: 'npm install' // command to execute after pulling latest code, optional 68 | }, 69 | 'local-angular': { 70 | git: 'bahmutov/local-angular-development', 71 | branch: 'gh-pages' // pick branch other than master 72 | }, 73 | 'local-folder': { 74 | // copy and serve a local folder instead of Git repo 75 | folder: '/path/to/foo' 76 | } 77 | }, 78 | storagePath: '/tmp/git-pages', // local temp folder, optional, leave it to OS tmp dir 79 | port: 8765 // serving port, optional 80 | }; 81 | ``` 82 | 83 | For our example, see [git-pages.config.js](git-pages.config.js) 84 | 85 | Note: some hosting environments, like Heroku do not support pulling repos via SSH without additional setup, 86 | thus they require HTTPS git urls. 87 | 88 | ## Run 89 | 90 | Run after installing globally `git-pages` 91 | 92 | Run after installing as a dependency (via package.json script) 93 | 94 | ```json 95 | "scripts": { 96 | "pages": "git-pages" 97 | }, 98 | "dependencies": { 99 | "git-pages": "0.2.0" 100 | } 101 | ``` 102 | 103 | Then you can start the `git-pages` server by simply `npm run pages`. 104 | 105 | Run from the cloned folder 106 | 107 | * simple start `node index.js` or `npm run start` 108 | * run with automatic restart and watching source files `npm run watch`. 109 | Uses [nodemon](http://nodemon.io/). 110 | 111 | ## Todo 112 | 113 | * [ ] webhook to allow pulling on commit 114 | * [x] execute shell commands after pulling, for example `npm install` or `bower install` 115 | * [x] form full SSH or HTTPS urls from user / repo name pair 116 | 117 | ### Small print 118 | 119 | Author: Kensho © 2015 120 | 121 | * [@kensho](https://twitter.com/kensho) 122 | * [kensho.com](http://kensho.com) 123 | 124 | Support: if you find any problems with this library, 125 | [open issue](https://github.com/kensho/git-pages/issues) on Github 126 | 127 | ## MIT License 128 | 129 | The MIT License (MIT) 130 | 131 | Copyright (c) 2015 Kensho 132 | 133 | Permission is hereby granted, free of charge, to any person obtaining a copy of 134 | this software and associated documentation files (the "Software"), to deal in 135 | the Software without restriction, including without limitation the rights to 136 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 137 | the Software, and to permit persons to whom the Software is furnished to do so, 138 | subject to the following conditions: 139 | 140 | The above copyright notice and this permission notice shall be included in all 141 | copies or substantial portions of the Software. 142 | 143 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 144 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 145 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 146 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 147 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 148 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 149 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var la = require('lazy-ass') 4 | var check = require('check-more-types') 5 | 6 | /* global process, require */ 7 | /* eslint new-cap: 0 */ 8 | /* eslint no-console: 0 */ 9 | function gitPages (options) { 10 | var userConfig = require('./src/config')(options) 11 | var repoConfig = userConfig.repos 12 | 13 | var express = require('express') 14 | var morgan = require('morgan') 15 | 16 | var fs = require('fs') 17 | var Q = require('q') 18 | var R = require('ramda') 19 | var quote = require('quote') 20 | var join = require('path').join 21 | var extname = require('path').extname 22 | var marked = require('marked') 23 | 24 | // var directToSubApp = require('./src/sub-app') 25 | 26 | var app = express() 27 | app.use(morgan('dev')) 28 | 29 | console.log('Will serve pages for repos', R.keys(repoConfig).join(', ')) 30 | 31 | require('./app/controller')(app, repoConfig) 32 | 33 | var storagePath = userConfig.storagePath 34 | if (!fs.existsSync(storagePath)) { 35 | console.log('making storage', quote(storagePath)) 36 | fs.mkdirSync(storagePath) 37 | } 38 | 39 | var repoCommands = require('./src/repo')({ 40 | storagePath: storagePath, 41 | useHttps: userConfig.useHttps 42 | }) 43 | 44 | function repoToFolder (repo) { 45 | la(check.object(repo), 'missing repo') 46 | la(check.unemptyString(repo.name), 'missing repo name', repo) 47 | 48 | // function passPath (path) { 49 | // la(check.unemptyString(path), 'copied folder should return path', path) 50 | // console.log('Local folder in %s', path) 51 | // return path 52 | // } 53 | 54 | // could be git repo or another folder 55 | var isGitRepo = check.unemptyString(repo.git) 56 | 57 | console.log('pulling repo %s, is git?', repo.name, isGitRepo) 58 | 59 | var clone = R.partial(repoCommands.clone, repo.name, repo) 60 | 61 | var pull = isGitRepo 62 | ? R.partial(repoCommands.pull, repo.name, repo.branch) 63 | : clone 64 | 65 | var shell = R.partial(repoCommands.shell, repo.exec) 66 | 67 | function noop () {} 68 | 69 | var commitId = isGitRepo 70 | ? R.partial(repoCommands.lastCommit, repo.name) 71 | : noop 72 | 73 | function rememberCommit (commit) { 74 | la(check.object(commit), 'expected commit obj for', repo.name, 'got', commit) 75 | la(check.commitId(commit.hash), 'expected commit for', repo.name, 'got', commit) 76 | repo.commit = commit 77 | console.log('remembering commit %s', commit) 78 | return commit 79 | } 80 | 81 | var setCommit = isGitRepo ? R.pipeP(commitId, rememberCommit) : noop 82 | return R.pipeP(pull, shell, setCommit) 83 | } 84 | 85 | app.get('/pull/:repo', function (req, res) { 86 | var name = req.params.repo 87 | if (!name) { 88 | console.log('cannot pull repo without name', req.params) 89 | return res.sendStatus(400) 90 | } 91 | 92 | console.log('received pull for repo %s', quote(name)) 93 | var config = repoConfig[name] 94 | if (!config) { 95 | console.log('cannot find repo %s', quote(name)) 96 | return res.status(404).send('Cannot find repo ' + name) 97 | } 98 | if (!check.has(config, 'name')) { 99 | config.name = name 100 | } 101 | 102 | // no need to clone, the repo is already there 103 | // var shell = R.partial(repoCommands.shell, config.exec) 104 | 105 | function sendOk (commit) { 106 | if (commit) { 107 | la(check.object(commit), 'expected commit obj for', name, 'got', commit) 108 | la(check.commitId(commit.hash), 'expected commit for', name, 'got', commit) 109 | return res.status(200).send(commit).end() 110 | } 111 | res.status(200).send().end() 112 | } 113 | 114 | repoToFolder(config)() 115 | .then(sendOk) 116 | .done() 117 | }) 118 | 119 | var extensionRenderers = { 120 | '.md': function renderMarkdown (res, path) { 121 | fs.readFile(path, 'utf8', function (err, data) { 122 | if (err) { 123 | throw new Error('Could not read ' + path + ' ' + err.message) 124 | } 125 | res.send(marked(data.toString())) 126 | }) 127 | } 128 | } 129 | 130 | function repoRouteFor (repoName) { 131 | // var repo = repoConfig[repoName] 132 | var repoPath = join(storagePath, repoName) 133 | return function repoRoute (req, res) { 134 | var index = repoConfig[repoName].index 135 | var full = join(repoPath, index) 136 | var fileExt = extname(full) 137 | if (R.has(fileExt, extensionRenderers)) { 138 | extensionRenderers[fileExt](res, full) 139 | } else { 140 | res.sendFile(full) 141 | } 142 | } 143 | } 144 | 145 | // TODO: process each repo in order, not all at once 146 | // to avoid multiple commands trying to execute in separate folders 147 | function fetchRepo (repoName) { 148 | la(check.unemptyString(repoName), 'missing repo name', repoName) 149 | var repo = repoConfig[repoName] 150 | if (!check.has(repo, 'name')) { 151 | repo.name = repoName 152 | } 153 | 154 | var clone = R.partial(repoCommands.clone, repoName, repo) 155 | 156 | var route = function route () { 157 | console.log('setting up route for repo', quote(repoName)) 158 | app.get('/' + repoName, repoRouteFor(repoName)) 159 | } 160 | 161 | return R.pipeP(clone, R.always(repo), repoToFolder, route)() 162 | } 163 | 164 | var repos = R.keys(repoConfig) 165 | var fetchReposOneByOne = 166 | repos 167 | .map(function (name) { 168 | return R.partial(fetchRepo, name) 169 | }) 170 | .reduce(Q.when, Q()) 171 | 172 | function start () { 173 | var PORT = process.env.PORT || userConfig.port 174 | app.listen(PORT, '0.0.0.0') 175 | console.log('Running on http://0.0.0.0:' + PORT) 176 | } 177 | 178 | function onError (err) { 179 | console.error('Caught a problem', err.message) 180 | console.error(err.stack) 181 | } 182 | 183 | fetchReposOneByOne 184 | .then(function setupSubapps () { 185 | // app.use(directToSubApp) 186 | app.use(express.static(storagePath)) 187 | }).then(start).catch(onError).done() 188 | } 189 | 190 | module.exports = gitPages 191 | 192 | if (!module.parent) { 193 | throw new Error('Please run from another module, or use bin script') 194 | } 195 | --------------------------------------------------------------------------------