├── .gitignore ├── .nodemonignore ├── public ├── icon.png ├── favicon.ico ├── icon-white.png ├── images │ ├── logo.png │ └── extension-preview.png ├── chrome-extension.crx ├── chrome-extension.zip ├── chrome-extension │ ├── preview.jpg │ ├── icons │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ └── icon-48.png │ ├── manifest.json │ └── inpage.js ├── chrome-extension.pem ├── error.html ├── forking.html ├── userscript │ └── 5minfork.user.js ├── style.css └── index.html ├── lib ├── credentials.js └── fmf.js ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | forks/* 2 | .env 3 | node_modules 4 | -------------------------------------------------------------------------------- /.nodemonignore: -------------------------------------------------------------------------------- 1 | forks/* 2 | forks-loading/* 3 | public/* 4 | .git/* -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/icon-white.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/chrome-extension.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/chrome-extension.crx -------------------------------------------------------------------------------- /public/chrome-extension.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/chrome-extension.zip -------------------------------------------------------------------------------- /public/chrome-extension/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/chrome-extension/preview.jpg -------------------------------------------------------------------------------- /public/images/extension-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/images/extension-preview.png -------------------------------------------------------------------------------- /public/chrome-extension/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/chrome-extension/icons/icon-128.png -------------------------------------------------------------------------------- /public/chrome-extension/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/chrome-extension/icons/icon-16.png -------------------------------------------------------------------------------- /public/chrome-extension/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/HEAD/public/chrome-extension/icons/icon-48.png -------------------------------------------------------------------------------- /lib/credentials.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('dotenv').load(); 3 | 4 | if (process.env.GITHUB_TOKEN) { 5 | module.exports = { 6 | githubToken: process.env.GITHUB_TOKEN 7 | }; 8 | } else { 9 | console.error('A token is required to run 5minfork.\nDetails >> https://github.com/remy/5minutefork#running\n'); 10 | process.exit(1); 11 | } 12 | -------------------------------------------------------------------------------- /public/chrome-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "5 minute fork", 3 | "version" : "1.0.17", 4 | "manifest_version" : 2, 5 | "homepage_url": "http://5minfork.com", 6 | "description" : "Adds a button to GitHub pages so with one click you can view the files of the repo hosted on the web by 5minfork.com", 7 | "icons": { 8 | "16": "icons/icon-16.png", 9 | "48": "icons/icon-48.png", 10 | "128": "icons/icon-128.png" 11 | }, 12 | "content_scripts": [{ 13 | "js" : [ "inpage.js" ], 14 | "matches" : [ "http://*.github.com/*", "https://*.github.com/*" ], 15 | "run_at": "document_end" 16 | }] 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5minfork", 3 | "version": "0.1.0", 4 | "description": "Quickly creates a fork of a github project for experimentation, and automatically removes after 5 minutes of inactivity", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": "", 10 | "author": "Remy Sharp", 11 | "license": "MIT / http://rem.mit-license.org", 12 | "dependencies": { 13 | "connect": "~2.7.3", 14 | "dotenv": "^1.2.0", 15 | "mustache": "~0.7.2", 16 | "remove": "~0.1.5", 17 | "request": "~2.16.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/chrome-extension.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANi2o33bE3gpXd4zX 3 | TI7nzfjqrQxcCGeNOlbZ489aRkQVlyzCLvmddtKvaiDm/QS2eogcKGVxIIFUCs5vP 4 | i+YCJJPmUfltznZSnRAbB9nQP4MKq7T15QeVjcXmhw9c2cA60iUXI1dkHF9KTtp3i 5 | oK293scD84HlWi0n1npdPi1FxAgMBAAECgYACDomysheXNl1LtJUX2vUB5MlD+Iwl 6 | 5Yh/Bn0PIPgUYtFPA+v7TI6lzCnMpaMfR+aFkFVBU1iQG1jNcDjY64WiCS/8b76K2 7 | pU3yEitmhfh2BwWZCpgiTF3ooeF4PMbHtnNyE3ljWYZ8/Ql+d6cFluneok/3EPUdT 8 | CV6Yvj22FHgQJBAP5ORpfalbQlXGj07GIFE06+cAqu0Cf0mu1UESvQAwLRBlyH0K2 9 | B0ZUMwVk+v1UGRiIOdtQTwnQ9gjdMRA4zdtkCQQDaKD+RNdu6N2uoEO3FX/3V7Kwu 10 | QMf0qJ6nxdn05Y6GtUWYpXip2LfIUu1zPfAT3nAME+XvTnmNFv+p0vt6fwBZAkEA+ 11 | enI3DDWz+urbgXMS+O6/raN+yGitLFgk3z7RvgsDVeHjeV2wRyD75tSY7cTZqY8w/ 12 | k889vbTEqqLlfHxcDzuQJBALoq9Kw/sOYF22pOIAp6c0y2ruy9vaWMq+/yiKBTscB 13 | FO0Ibm5Ad8CAUnKvmpFTgUvALnwIMDvCXOtA6yv5rGOkCQFsuo4wss3X06kEkyIne 14 | SZeeeFPRL1bRkbsaQ7jVcj3AC+icF+/X05SJvegB337oAoQfVjAMkiGdGRd9h78uo 15 | +U= 16 | -----END PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /public/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |Sorry, but for some reason this repo couldn't be created or found. It might be because the repo is private, it might just be a 404 inside of the repo (pointing to a resource the repo didn't include), or it might just be something else.
12 |If you think this is a bug, please do report the error.
13 |Thanks — @rem
14 | 24 | 25 | -------------------------------------------------------------------------------- /public/forking.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |Forking — https://github.com/{{owner.login}}/{{name}}
This shouldn't take too long, we're just cloning the project which takes a series of seconds.
13 |Once cloned, this url will automatically clean itself up and remove the files after 5 minutes of idle time.
14 | 43 | 44 | -------------------------------------------------------------------------------- /public/userscript/5minfork.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 5 minute fork 3 | // @namespace http://5minfork.com/ 4 | // @version 1.0.8 5 | // @description Adds a button to GitHub pages so with one click you can view the files of the repo hosted on the web by 5minfork.com 6 | // @match http://*.github.com/* 7 | // @match https://*.github.com/* 8 | // @exclude *://github.com/organizations/* 9 | // @exclude *://github.com/orgs/* 10 | // @exclude *://gist.github.com* 11 | // @grant none 12 | // ==/UserScript== 13 | 14 | var pageHeaderMatches = document.querySelector(".pagehead-actions"); 15 | if (pageHeaderMatches) { 16 | var reResult = new RegExp("^.*?github.com[/:]([^/]+)/(.*?)(.git)?$").exec(document.location.href); 17 | 18 | var fiveMinForkButtonAnchor = document.createElement("a"); 19 | fiveMinForkButtonAnchor.className = "btn btn-sm"; 20 | fiveMinForkButtonAnchor.href = "http://5minfork.com/" + reResult[1] + "/" + reResult[2].split('/')[0]; 21 | fiveMinForkButtonAnchor.target = "_blank"; 22 | 23 | var fiveMinForkButtonIcon = document.createElement("img"); 24 | fiveMinForkButtonIcon.className = "octicon"; 25 | fiveMinForkButtonIcon.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAA9ElEQVQYGQXBPSiEYQAA4Ocok+vEnSgxKKJMko7CEaPdKpPFIJLVYJPdYrOeXRkomaToE4vyd537+2S5y93reQCAeU/erQMAAHBv0YCiTgAA6EBkWlpBFwBAm0PnWFFUs4VZV25kAfYFJf3Ii5DwatWSF2BUw48RkBehXSwloyQBx4JtQF6kz4WmNx82gUdBGiwoiZUVLevWC/DrGxCZ0qNsDAyalIKqGuBOzpCCJDgTZOFWMA5mPPu0ARJeNaVhT3AKAGBNcAkkFQQbAGBCRcscQE5Dy4kxkLErFhwAQM6XIKiq+BPU7QAAJO24Fqt7cGQY4B/0wlLnM/C+BgAAAABJRU5ErkJggg=="; 26 | 27 | fiveMinForkButtonAnchor.appendChild(fiveMinForkButtonIcon); 28 | fiveMinForkButtonAnchor.appendChild(document.createTextNode("5 min fork")); 29 | 30 | var fiveMinForkButtonListItem = document.createElement("li"); 31 | fiveMinForkButtonListItem.appendChild(fiveMinForkButtonAnchor); 32 | 33 | var pageHeader = pageHeaderMatches; 34 | pageHeader.insertBefore(fiveMinForkButtonListItem, pageHeader.childNodes[0]); 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 5 minute fork 2 | 3 | [](http://flattr.com/thing/1463468/remy5minutefork-on-GitHub) 4 | 5 | Heard of 10 minute email? Well this is the same thing, except for github repos. 6 | 7 | The number of times I've come across a cool demo, only to be faced with a github repo and no live link - it just bums me out. 8 | 9 | So I made this (quickly - literally about 90 minutes), which reads the url, and forks the project. If the url is idle for 5 minutes, then it's automatically swept under the carpet. 10 | 11 | The code is pretty gnarly, but don't judge me - I wanted quick and dirty. 12 | 13 | ## Running 14 | 15 | The project requires a github OAuth token to place API requests. If you are not sure how to generate an OAuth token [Github have an article to help](https://help.github.com/articles/creating-an-access-token-for-command-line-use/) 16 | 17 | To run: 18 | 19 | GITHUB_TOKEN=token node index.js 20 | 21 | Alternatively, you can put the token in a file in the root of this project called `.env`: 22 | 23 | ```BASH 24 | GITHUB_TOKEN=token 25 | NODE_DEBUG=true #Set to false for production 26 | ``` 27 | 28 | `.gitignore` should ensure the file isn't sent up to github. 29 | 30 | ## Development & debug mode 31 | 32 | Since 5minfork uses the format `http://As described on hackernews:
18 |19 |If a github repo has an index.html file and you click on it, github will show you the source instead of the webpage.
5minfork shows you the webpage so that you can have an idea of what the repo is about.
Heard of 10 minute email? Well this is the same thing, except for github repos.
21 | 22 |The number of times I've come across a cool demo, only to be faced with a github repo and no live link - it just bums me out.
23 | 24 |So I made this (quickly - literally about 90 minutes), which reads the url, and forks the project. If the url is idle for 5 minutes, then it's automatically discarded and swept under the carpet. I wrote up more detail about 5minfork on my blog if you're interested.
25 | 26 | 27 | 28 |Super huge thanks to Jake Champion for PRs and donating hosting to 5minfork ❤️️
29 | 30 |Simply take a public github url and swap out the https://github.com for http://5minfork.com, add /tree/branch-name if you want a specific branch, wait a moment for the project to be cloned, and then you'll be redirected to a unique url which will serve up static content from the project.
Users contributed tools that make creating a 5minfork easier. Below shows the Chrome extension and user.script in action:
35 |
If you're interested in the code, have found a bug, want to fix a bug, have written a browser extension (go on - you know you want to) - the code is freely available on github: https://github.com/remy/5minutefork
44 |Enjoy — @rem
45 | 69 | 70 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var connect = require('connect'), 3 | fs = require('fs'), 4 | remove = require('remove'), 5 | fmf = require('./lib/fmf'), // FiveMinFork - fmf.js 6 | crypto = require('crypto'), 7 | request = require('request'), 8 | http = require('http'), 9 | mustache = require('mustache'), 10 | credentials = require('./lib/credentials'), 11 | forks = {}, 12 | template = mustache.compile(fs.readFileSync(__dirname + '/public/forking.html', 'utf8')), 13 | error = fs.readFileSync(__dirname + '/public/error.html', 'utf8'), 14 | timeout = 5 * 60 * 1000, 15 | debug = process.env.NODE_DEBUG || false; 16 | 17 | if (debug) { 18 | console.log('>>> in debug mode'); 19 | } 20 | 21 | function createRoute(dir) { 22 | return connect() 23 | .use(connect.static(dir)) 24 | .use(connect.directory(dir)) 25 | .use(function (req, res) { 26 | // if we hit this point, then we have a 404 27 | res.writeHead(404, { 'content-type': 'text/html' }); 28 | res.end(error); 29 | }); 30 | } 31 | 32 | var app = connect().use(connect.logger('dev')).use(connect.favicon(__dirname + '/public/favicon.ico')).use(function subdomains(req, res, next) { 33 | req.subdomains = req.headers.host 34 | .split('.') 35 | .slice(0, -2); 36 | 37 | next(); 38 | }).use(function xhr(req, res, next) { 39 | req.xhr = req.headers['x-requested-with'] === 'XMLHttpRequest'; 40 | next(); 41 | }).use(function (req, res, next) { 42 | var hash = req.subdomains[0]; 43 | var fork = forks[hash]; 44 | 45 | if (fork) { 46 | var url = fork.url; 47 | var dir = fmf.getPath(url); //'./forks/' + url.join('/') + '/'; 48 | 49 | fs.exists(dir, function (exists) { 50 | if (exists) { 51 | // route static router through this directory 52 | if (!fork.error) { 53 | // reset timeout on this path 54 | fork.accessed = Date.now(); 55 | 56 | if (!fork.router) { 57 | fork.router = createRoute(dir); 58 | } 59 | 60 | return fork.router(req, res, next); 61 | } else { 62 | res.writeHead(404, { 'content-type': 'text/html' }); 63 | res.end(error); 64 | } 65 | } else if (fork.forking) { 66 | if (req.xhr) { 67 | var timer = setInterval(function () { 68 | if (fork.forking === false) { 69 | clearInterval(timer); 70 | res.writeHead(200, { 'content-type': 'text/plain' }); 71 | res.end('true'); 72 | } 73 | }, 500); 74 | } else { 75 | res.writeHead(200, { 'content-type': 'text/html' }); 76 | res.end(template(fork.gitdata)); 77 | } 78 | } else { 79 | // render a holding page, and place xhr request 80 | if (req.xhr) { 81 | fmf.fork(fork, function (err, dir) { 82 | fork = forks[hash] = { 83 | error: err, 84 | repo: fork.repo, 85 | // FIXME: for some reason I have to re-add these in :( 86 | url: fork.url, 87 | urlWithoutBranch: fork.urlWithoutBranch, 88 | gitdata: fork.gitdata, 89 | router: createRoute(dir), 90 | accessed: Date.now(), 91 | clear: function () { 92 | clearInterval(fork.timer); 93 | delete forks[hash]; 94 | console.log('deleting path: ' + dir); 95 | remove(dir, function () {}); 96 | }, 97 | timer: setInterval(function () { 98 | var now = Date.now(); 99 | if (now - fork.accessed > timeout) { 100 | fork.clear(); 101 | } 102 | }, 1000 * 10) 103 | }; 104 | res.writeHead(200, { 'content-type': 'text/plain' }); 105 | res.end('true'); 106 | }); 107 | } else { 108 | request('https://api.github.com/repos/' + fork.urlWithoutBranch.join('/'), { 109 | 'headers': { 110 | 'user-agent': '5minfork - http://5minfork.com', 111 | 'Authorization': 'token ' + credentials.githubToken 112 | } 113 | }, function (e, r, body) { 114 | fork.gitdata = JSON.parse(body); 115 | fork.repo = fork.gitdata.git_url; 116 | res.writeHead(200, { 'content-type': 'text/html' }); 117 | res.end(template(fork.gitdata)); 118 | }).end(); 119 | } 120 | } 121 | }); 122 | } else { 123 | next(); 124 | } 125 | }).use(connect.static('./public')) 126 | .use(function (req, res, next) { 127 | if (req.subdomain) { 128 | return next(); 129 | } 130 | 131 | // means no subdomain, and no real file found, 132 | // and ignore the leading slash, and only return 133 | // 2 parts 134 | var split = req.url.replace(/\/$/, '').split('/'), 135 | url = split.slice(1), 136 | urlWithoutBranch = split.slice(1, 3); 137 | 138 | if (url.length === 2) { 139 | url.push('tree'); 140 | url.push('master'); 141 | } 142 | 143 | if (urlWithoutBranch.length === 2) { 144 | var sha1 = crypto.createHash('sha1'); 145 | sha1.update(url.join('.')); 146 | var hash = debug ? 'abc123' : sha1.digest('hex').substr(0, 7); 147 | if (!forks[hash]) { 148 | forks[hash] = { url: url, urlWithoutBranch: urlWithoutBranch }; 149 | } 150 | res.writeHead(302, { 'location': 'http://' + hash + '.' + req.headers.host }); 151 | res.end('Redirect to ' + 'http://' + hash + '.' + req.headers.host); 152 | } else { 153 | res.writeHead(404, { 'content-type': 'text/html' }); 154 | res.end('404'); 155 | } 156 | }); 157 | 158 | http.createServer(app).listen(process.env.PORT || 8000); 159 | -------------------------------------------------------------------------------- /public/chrome-extension/inpage.js: -------------------------------------------------------------------------------- 1 | (function (d, w) { 2 | var actions = d.querySelector('.pagehead-actions'), 3 | button = '