http{/,s} git server
git clone http://git.nthia.dev/qwgit
const path = require('path')
let git = null
let mime = null
const entities = new Map([
['<','<'],
['>','>'],
['"','"'],
["'",'''],
])
const css = `<style>
.page {
max-width: 800px;
overflow-x: auto;
margin: auto;
}
.page .name {
font-size: 1.5em;
}
.page .desc {
margin-top: 0.5em;
}
.page .repo {
margin-bottom: 1em;
}
.file-list .dir, .file-list .file {
margin-bottom: 0.5em;
}
.page a:link, .page a:visited {
text-decoration: none;
}
.page a:hover {
text-decoration: underline;
}
.code {
font-family: monospace;
padding: 0.5em;
background-color: #eee;
}
.page .clone {
margin-top: 0.5em;
}
.page .repos-link {
text-align: right;
margin-bottom: -1em;
}
.source {
font-family: monospace;
}
</style>`
function header(req, m, pathParts) {
let proto = req.socket?.ssl ? 'https:' : 'http:'
return html`
<div class="repos-link">[<a href="/">repos</a>]</div>
<div class="name">
<a href="/${m.outerPath}">${m.outerPath}</a> / ${[pathParts]}
</div>
<div class="desc">${m.repo.description || ''}</div>
<div class="clone code">
git clone <span id="proto">${proto}</span>//<span id="host">${req.headers.host}</span>/${m.outerPath}
</div>
<script>
document.getElementById('proto').innerText = location.protocol
document.getElementById('host').innerText = location.host
</script>
<hr>`
}
module.exports = function (opts) {
let repodir = opts.repodir
let repos = opts.repos
return function (qwgit, req, res, ids) {
if (!git) git = require('nodegit')
if (!mime) mime = require('mime')
let m = null
if (req.url === '/') {
let repos = qwgit.repos.listAuthorized(ids)
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
res.end(html`
${[css]}
<div class="page">
${repos.length === 0 ? '[ no repos ]' : ''}
${repos.map(repo => {
return `<div class="repo">
<div class="name"><a href="${repo.paths[0]}">${repo.name}</a></div>
<div class="desc">${repo.description}</div>
<div class="tags">${repo.tags}</div>
</div>`
})}
</div>
`.trim().replace(/^ +/mg,'') + '\n')
} else if ((m = repos.match(req.url)) !== null) {
let ipath = m.innerPath.replace(/\?.*/,'')
let gitDir = qwgit.repos.getFilePath(m.repo.id)
if (ipath === '/' || ipath === '') {
showDir(req, res, git, gitDir, m, '/')
} else if (/^\/d(?:\/|$)/.test(ipath)) {
showDir(req, res, git, gitDir, m, ipath.slice('/d/'.length))
} else if (ipath.startsWith('/f/')) {
showFile(req, res, git, gitDir, m, ipath.slice('/f/'.length))
} else if (ipath.startsWith('/r/')) {
showRawFile(req, res, git, gitDir, m, ipath.slice('/r/'.length))
} else if (ipath.startsWith('/c/')) {
res.writeHead(404, { 'content-type': 'text/html; charset=utf-8' })
res.end('TODO: commit\n')
} else {
res.writeHead(404, { 'content-type': 'text/html; charset=utf-8' })
res.end('not found\n')
}
} else {
res.writeHead(404, { 'content-type': 'text/html; charset=utf-8' })
res.end('not found\n')
}
}
}
function error(res, code, msg) {
res.writeHead(code, { 'content-type': 'text/plain; charset=utf-8' })
res.end(msg + '\n')
}
function esc(s) {
return String(s).replace(/[<>"']/g, s => entities.get(s) ?? '')
}
function html(strings, ...values) {
return strings.flatMap((s,i) => {
if (i === strings.length-1) return [s]
let v = values[i]
return Array.isArray(v) ? [s].concat(v) : [s,esc(values[i])]
}).join('')
}
async function showDir(req, res, git, gitDir, m, dir) {
let err = null, errCode = 500
let c = null
try {
let r = await git.Repository.open(gitDir)
let bs = await r.getReferences()
c = await r.getBranchCommit(bs[0].name())
} catch (e) {
err = e
}
let t = null
if (c) {
try { t = await c.getTree() }
catch (e) { err = e }
}
if (t && !/^[\/]*$/.test(dir)) {
let te = null
try { te = await t.getEntry(dir) }
catch (e) {
if (err.errno === -3) {
err = 'directory not found'
errCode = 404
} else {
err = e
}
}
if (te && !te.isTree()) {
errCode = 400
err = 'not a directory'
}
try { t = await te.getTree() }
catch (e) { err = e }
}
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
let idir = m.innerPath.replace(/^\/d(?:\/|$)/,'/') || '/'
let updir = path.join(idir,'..')
let up = idir === '/' ? '' : html`<div class="dir">
<a href="/${updir === '/' ? m.outerPath : path.join(m.outerPath,'d',updir)}">../</a>
</div>`
let parts = dir.split('/').filter(Boolean)
let pathParts = parts.map((_,i) => parts.slice(0,i+1).join('/'))
.map((x,i) => {
return html`<a href="/${path.join(m.outerPath,'d',x)}">${path.basename(x)}</a>`
}).join(' / ')
let body = null
if (t) {
body = t.entries().map(e => {
let p = e.path()
if (e.isDirectory()) {
return html`<div class="dir">
<a href="/${path.join(m.outerPath,'d',p)}">${path.basename(p)}/</a>
</div>`
} else {
return html`<div class="file">
<a href="/${path.join(m.outerPath,'f',p)}">${path.basename(p)}</a>
</div>`
}
})
let files = t.entries().map(e => path.basename(e.path()))
let readme = files.find(x => /^readme\.(md|txt|markdown)$/i.test(x))
if (readme) {
let te = null
try { te = await t.getEntry(readme) }
catch (e) {
if (e.errno === -3) {
errCode = 404
err = 'file not found'
} else {
err = e
}
}
let b = null
if (te && !te.isFile()) {
err = 'not a file'
errCode = 400
} else if (te) {
try { b = await te.getBlob() }
catch (e) { err = e }
}
if (err) {
body.push(`<hr>\n${esc(String(err.message ?? err))}`)
} else if (b) {
body.push(`<hr>\n<pre class="source">${esc(b.content())}</div>`)
}
}
} else if (err) {
body = `[ ${esc(String(err.message ?? err))} ]`
} else {
body = '[ no commit history ]'
}
res.end(html`
${[css]}
<div class="page file-list">
${[header(req,m,pathParts)]}
${[up]}
${body}
</div>\n`.replace(/^ {4}/mg,'')
)
}
async function showRawFile(req, res, git, gitDir, m, file) {
let err = null, errCode = 500
let c = null
try {
let r = await git.Repository.open(gitDir)
let bs = await r.getReferences()
c = await r.getBranchCommit(bs[0].name())
} catch (e) {
err = e
}
let t = null, te = null
if (c) {
try { t = await c.getTree() }
catch (e) { err = e }
}
if (t) {
try { te = await t.getEntry(file) }
catch (e) {
if (e.errno === -3) {
errCode = 404
err = 'file not found'
} else {
err = e
}
}
}
let b = null
if (te && !te.isFile()) {
err = 'not a file'
errCode = 400
} else {
b = await te.getBlob()
}
let type = mime.getType(file)
if (err) {
res.writeHead(errCode, { 'content-type': 'text/plain; charset=utf-8' })
res.end(String(err.message ?? err) + '\n')
return
} else if (/^(?:text\/[^+]*|application\/(?:javascript|x?html))(?:\+|$)/.test(type)) {
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' })
} else {
res.writeHead(200, { 'content-type': type })
}
res.end(b.content())
}
async function showFile(req, res, git, gitDir, m, file) {
let err = null, errCode = 500
let c = null
try {
let r = await git.Repository.open(gitDir)
let bs = await r.getReferences()
c = await r.getBranchCommit(bs[0].name())
} catch (e) {
err = e
}
let t = null
if (c) {
try { t = await c.getTree() }
catch (e) { err = e }
}
let te = null
if (t) {
try { te = await t.getEntry(file) }
catch (e) {
if (e.errno === -3) {
errCode = 404
err = 'file not found'
} else {
err = e
}
}
}
if (te && !te.isFile()) {
err = 'not a file'
errCode = 400
}
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
let type = mime.getType(file)
let b = null
if (te) {
try { b = await te.getBlob() }
catch (e) { err = e }
}
let source = ''
if (err) {
source = String(err.message ?? err)
} else if (/^text\//.test(type) || /^application\/(?:json|javascript|x?html)\b/.test(type)) {
source = esc(b.content())
} else if (/^image\//.test(type)) {
let src = '/' + path.join(m.outerPath,'r',file)
source = html`<a href="${src}"><img src="${src}"></a>`
} else if (/^makefile$/i.test(file)) {
source = esc(b.content())
} else {
let src = '/' + path.join(m.outerPath,'r',file)
source = html`[ <a href="${src}">load raw content</a> ]`
}
let parts = file.split('/')
let pathParts = parts.map((_,i) => parts.slice(0,i+1).join('/'))
.map((x,i) => {
let df = i === parts.length-1 ? 'f' : 'd'
return html`<a href="/${path.join(m.outerPath,df,x)}">${path.basename(x)}</a>`
}).join(' / ')
res.end(`${css}\n<div class="page">`
+ header(req,m,pathParts)
+ `<pre class="source">${source}</pre>`
+ '</div>')
}