├── .gitignore ├── .nodemonignore ├── README.md ├── index.js ├── lib ├── credentials.js └── fmf.js ├── package.json └── public ├── chrome-extension.crx ├── chrome-extension.pem ├── chrome-extension.zip ├── chrome-extension ├── icons │ ├── icon-128.png │ ├── icon-16.png │ └── icon-48.png ├── inpage.js ├── manifest.json └── preview.jpg ├── error.html ├── favicon.ico ├── forking.html ├── icon-white.png ├── icon.png ├── images ├── extension-preview.png └── logo.png ├── index.html ├── style.css └── userscript └── 5minfork.user.js /.gitignore: -------------------------------------------------------------------------------- 1 | forks/* 2 | .env 3 | node_modules 4 | -------------------------------------------------------------------------------- /.nodemonignore: -------------------------------------------------------------------------------- 1 | forks/* 2 | forks-loading/* 3 | public/* 4 | .git/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 5 minute fork 2 | 3 | [![Flattr this](http://api.flattr.com/button/flattr-badge-large.png)](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://.host.com/` as the cloned repo, testing locally can be challenging (unless you have a CNAME star rule - whic most people don't). So there is a debug mode that points all repos to **abc123** as the hash, and unpon restart the `forks` and `forks-loading` directories are completely reset. 33 | 34 | To enter debug mode: 35 | 36 | NODE_DEBUG=true node index.js 37 | 38 | ## How to fork for 5 minutes 39 | 40 | Simply take a public github url and swap out the https://github.com for [http://5minfork.com](http://5minfork.com), 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. 41 | 42 | ## License 43 | 44 | MIT [http://rem.mit-license.org](http://rem.mit-license.org) 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/fmf.js: -------------------------------------------------------------------------------- 1 | var debug = process.env.NODE_DEBUG || false, 2 | spawn = require('child_process').spawn, 3 | fs = require('fs'), 4 | resolve = require('path').resolve, 5 | cwd = process.cwd(), 6 | fmf = module.exports = {}, 7 | dir = { 8 | forks: 'forks', 9 | loading: 'forks-loading' 10 | }; 11 | 12 | function trycode(fn) { 13 | try { fn(); } catch (e) {} 14 | } 15 | 16 | 17 | if (debug) { 18 | // remove the fork directories 19 | trycode(function () { fs.rmdirSync(resolve(cwd, dir.forks)); }); 20 | trycode(function () { fs.rmdirSync(resolve(cwd, dir.loading)); }); 21 | } 22 | 23 | trycode(function () { fs.mkdirSync(resolve(cwd, dir.forks)); }); 24 | trycode(function () { fs.mkdirSync(resolve(cwd, dir.loading)); }); 25 | 26 | function createForkPath(fork, callback) { 27 | // cd forks 28 | var dirpath = resolve(cwd, dir.loading, fork.url[0]); 29 | 30 | // console.log('creating ' + dirpath); 31 | fs.mkdir(dirpath, function (err) { 32 | if (err && err.code !== 'EEXIST') { 33 | console.error(err); 34 | return callback(err); 35 | } 36 | 37 | fs.mkdir(resolve(cwd, dir.forks, fork.url[0]), function () {}); 38 | 39 | dirpath = resolve(cwd, dir.loading, fork.url[0], fork.url[1]); 40 | // console.log('creating ' + dirpath); 41 | 42 | fs.mkdir(dirpath, function (err) { 43 | if (err && err.code !== 'EEXIST') { 44 | // console.error(err); 45 | return callback(err); 46 | } 47 | 48 | callback(null, dirpath); 49 | }); 50 | }); 51 | 52 | } 53 | 54 | // move the fork to the live fork directory 55 | function finishFork(code, fork, path, callback) { 56 | var finalPath = fmf.getPath(fork.url); //resolve(cwd, dir.forks, fork.url[0], fork.url[1]); 57 | 58 | fs.rename(path, finalPath, function () { 59 | fork.forking = false; 60 | callback(code === 0 ? null : true, finalPath); 61 | }); 62 | } 63 | 64 | fmf.getPath = function (urlParts) { 65 | // note that resolve requires strings to be passed in, so if there's no 66 | // branch, then passing in `undefined` will bork, so we `|| ''` - bosh. 67 | return resolve(cwd, dir.forks, urlParts[0], urlParts[1] + ':' + (urlParts[3] || 'master')); 68 | }; 69 | 70 | fmf.fork = function (fork, callback) { 71 | fork.forking = true; 72 | process.nextTick(function () { 73 | createForkPath(fork, function (err, path) { 74 | var clone = spawn('git', ['clone', '--depth', '1', '--recursive', fork.repo, path]); 75 | 76 | clone.stdout.on('data', function (data) { 77 | console.log(data + ''); 78 | }); 79 | clone.stderr.on('data', function (data) { 80 | console.error(data + ''); 81 | }); 82 | clone.on('exit', function (code) { 83 | var checkout; 84 | 85 | if (code !== 0) { 86 | console.log('clone exit with code ' + code); 87 | } 88 | 89 | // most likely path 90 | if (fork.url.length < 3) { 91 | finishFork(code, fork, path, callback); 92 | } else if (fork.url.indexOf('tree') === 2) { 93 | // then checkout the specified branch 94 | checkout = spawn('git', ['checkout', fork.url[3]], {cwd: path}); 95 | 96 | checkout.stdout.on('data', function (data) { 97 | console.log(data + ''); 98 | }); 99 | 100 | checkout.stderr.on('data', function (data) { 101 | console.error(data + ''); 102 | }); 103 | 104 | checkout.on('exit', function (code) { 105 | finishFork(code, fork, path, callback); 106 | }); 107 | } else { 108 | // god knows...this really shouldn't happen. 109 | callback(true, path); 110 | } 111 | }); 112 | }); 113 | }); 114 | }; -------------------------------------------------------------------------------- /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.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/chrome-extension.crx -------------------------------------------------------------------------------- /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/chrome-extension.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/chrome-extension.zip -------------------------------------------------------------------------------- /public/chrome-extension/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/chrome-extension/icons/icon-128.png -------------------------------------------------------------------------------- /public/chrome-extension/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/chrome-extension/icons/icon-16.png -------------------------------------------------------------------------------- /public/chrome-extension/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/chrome-extension/icons/icon-48.png -------------------------------------------------------------------------------- /public/chrome-extension/inpage.js: -------------------------------------------------------------------------------- 1 | (function (d, w) { 2 | var actions = d.querySelector('.pagehead-actions'), 3 | button = '
  • ' + 4 | '' + 5 | '' + 7 | '5minfork' + 8 | '' + 9 | '
  • '; 10 | 11 | if (!(actions && d.body.classList.contains('vis-public'))) return; 12 | 13 | actions.innerHTML = button + actions.innerHTML; 14 | })(document, window); 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/chrome-extension/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/chrome-extension/preview.jpg -------------------------------------------------------------------------------- /public/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Error 6 | 7 | 8 | 9 | 10 |

    5 minute fork not available

    11 |

    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/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/favicon.ico -------------------------------------------------------------------------------- /public/forking.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Forking (actually cloning) in action... 6 | 7 | 8 | 9 | 10 |

    Loading your 5 minute fork

    11 |

    Forking — https://github.com/{{owner.login}}/{{name}}

    12 |

    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/icon-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/icon-white.png -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/icon.png -------------------------------------------------------------------------------- /public/images/extension-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/images/extension-preview.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remy/5minutefork/e80c6e1d62ad735b35e1220e432f0e18b0013ca0/public/images/logo.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 5 minute fork 6 | 7 | 8 | 9 | 10 |

    5 minute fork

    11 |
    12 | 14 | 15 | 16 |
    17 |

    As described on hackernews:

    18 |

    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.

    19 |

    Why?

    20 |

    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 |

    Want to say thanks? Flattr this

    27 | 28 |

    Super huge thanks to Jake Champion for PRs and donating hosting to 5minfork ❤️️

    29 | 30 |

    How to fork for 5 minutes

    31 |

    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.

    32 | 33 |

    Browser Tools

    34 |

    Users contributed tools that make creating a 5minfork easier. Below shows the Chrome extension and user.script in action:

    35 |

    36 | 41 | 42 |

    Fork this?

    43 |

    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 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'helvetica neue', arial, helveica; 3 | font-weight: 200; 4 | } 5 | 6 | code { 7 | font-family: courier; 8 | padding: 2px; 9 | color: #008DE2; 10 | } 11 | 12 | #spinner { 13 | font-size: 33px; 14 | float: left; 15 | padding: 2px; 16 | padding-right: 10px; 17 | font-family: courier; 18 | color: #008DE2; 19 | } 20 | 21 | body { 22 | background: white; 23 | color: #212121; 24 | margin: 0; 25 | padding: 60px 0; 26 | } 27 | 28 | p, h1, h2, h3, blockquote, ul, form { 29 | max-width: 80%; 30 | margin-left: auto; 31 | margin-right: auto; 32 | } 33 | 34 | li { 35 | margin: 10px 0; 36 | } 37 | 38 | h3 { 39 | font-weight: normal; 40 | } 41 | 42 | img { 43 | border: 3px solid #aaa; 44 | } 45 | 46 | h1 { 47 | font-size: 38px; 48 | } 49 | 50 | h2 { 51 | font-size: 28px; 52 | margin-top: 48px; 53 | } 54 | 55 | p, li { 56 | line-height: 24px; 57 | font-size: 18px; 58 | text-align: justify; 59 | } 60 | 61 | a { 62 | color: blue; 63 | } 64 | 65 | em { 66 | font-weight: 400; 67 | } 68 | 69 | .footer { 70 | padding: 40px 0; 71 | background: #efefef; 72 | margin: 0; 73 | margin-top: 40px; 74 | max-width: 100%; 75 | } 76 | 77 | .footer p { 78 | text-align: left; 79 | } 80 | 81 | blockquote { 82 | quotes: "\201C""\201D""\2018""\2019"; 83 | } 84 | 85 | blockquote p { 86 | background: #f9f9f9; 87 | border-left: 10px solid #ccc; 88 | padding: 20px; 89 | padding-left: 20px; 90 | } 91 | 92 | blockquote p:before { 93 | color:#ccc; 94 | content:open-quote; 95 | font-size:4em; 96 | line-height:.1em; 97 | margin-right:.25em; 98 | vertical-align:-.4em; 99 | } 100 | 101 | form, form input, form button { 102 | font-size: 28px; 103 | } 104 | 105 | form input { 106 | width: 50%; 107 | padding: 4px; 108 | } 109 | 110 | form button { 111 | background: #f9f9f9; 112 | border: 1px solid #ccc; 113 | padding: 4px; 114 | } 115 | 116 | form input:focus:invalid, #error { 117 | color: #900; 118 | } 119 | 120 | #error { 121 | font-size: 18px; 122 | margin-top: 18px; 123 | } 124 | 125 | body { 126 | background: url(/images/logo.png) no-repeat -77px 40px fixed; 127 | } 128 | 129 | .flattr img { 130 | border: 0; 131 | vertical-align: middle; 132 | } 133 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------