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