http{/,s} git server
git clone http://git.nthia.dev/qwgit
const path = require('path')
const { nextTick } = process
const { randomBytes } = require('crypto')
const { spawn } = require('child_process')
const EventEmitter = require('events')
const Namespaces = require('./namespaces.js')
module.exports = Repos
function Repos(opts) {
if (!(this instanceof Repos)) return new Repos(opts)
EventEmitter.call(this)
this._repodir = opts.repodir
this._fs = opts.fs
this._repos = new Map
this._pathToId = new Map
this._namespaces = new Namespaces({ data: opts.namespaces })
if (Array.isArray(opts.repos)) {
opts.repos.forEach(r => {
this._addRepo(r)
})
}
}
Repos.prototype = Object.create(EventEmitter.prototype)
Repos.prototype._addRepo = function (r) {
this._repos.set(r.id, r)
if (Array.isArray(r.paths)) {
r.paths.forEach(p => this._pathToId.set(normPath(p), r.id))
}
}
Repos.prototype.list = function () {
return Array.from(this._repos.values())
}
Repos.prototype.listAuthorized = function (ids) {
let list = []
for (let repo of this._repos.values()) {
let p = repo.paths[0]
let ok = this.isAuthorized('list', p, ids)
|| this.isAuthorized('remove', p, ids)
|| this.isAuthorized('create', p, ids)
if (ok) list.push(repo)
}
return list
}
Repos.prototype.get = function (rid) {
return this._repos.get(rid) ?? null
}
Repos.prototype.getFilePath = function (rid) {
return path.join(this._repodir, rid)
}
Repos.prototype.match = function (rurl) {
let rparts = rurl.replace(/\?.*/,'').replace(/^\//,'').split(/\/+/)
for (let i = rparts.length; i >= 0; i--) {
let p = rparts.slice(0,i).join('/') || '/'
let id = this._pathToId.get(p)
if (id === undefined) continue
let repo = this._repos.get(id)
if (repo !== null) {
return { repo, outerPath: p, innerPath: rurl.slice(p.length+1) }
}
}
return null
}
Repos.prototype.create = function (opts, cb) {
let rid = randomBytes(6).toString('hex')
let repo = {
id: rid,
paths: (opts.paths ?? []).map(p => p === '/' ? '/' : p.replace(/^\//,'')),
list: opts.list ?? [],
push: opts.push ?? [],
pull: opts.pull ?? [],
name: opts.name,
description: opts.description,
tags: opts.tags ?? [],
}
let rdir = path.join(this._repodir, rid)
this._fs.mkdir(rdir, { recursive: true }, err => {
if (err) return cb(err)
let ps = spawn('git', ['init','--bare',rdir])
ps.once('exit', code => {
if (code !== 0) {
return cb(new Error(`"git init" failed with exit code ${code}`))
}
this._addRepo(repo)
this.emit('create', repo)
cb(null, repo)
//this._updateServerInfo(rdir, repo, cb)
})
ps.stdout.resume()
ps.stderr.resume()
})
}
Repos.prototype.update = function (rid, repo, cb) {
let prev = this._repos.get(rid)
if (prev && Array.isArray(prev.paths)) {
prev.paths.forEach(p => this._pathToId.delete(p))
}
if (repo.id === undefined) {
repo = Object.assign({}, repo, { id: rid })
}
if (repo.id !== undefined && rid !== repo.id) {
this._repos.delete(rid)
}
repo = Object.assign({}, prev, repo)
Object.keys(repo).forEach(key => {
if (repo[key] === undefined) delete repo[key]
})
this._addRepo(repo)
this.emit('update', rid, repo)
nextTick(cb, null)
}
Repos.prototype.remove = function (rid, cb) {
let repo = this._repos.get(rid)
if (repo && Array.isArray(repo.paths)) {
repo.paths.forEach(p => this._pathToId.delete(p))
}
this._repos.delete(rid)
this.emit('remove', repo)
nextTick(cb, null, repo)
}
/*
Repos.prototype._updateServerInfo = function (rdir, repo, cb) {
let ps = spawn('git', ['update-server-info',rdir], { cwd: rdir })
ps.once('exit', code => {
if (code !== 0) {
return cb(new Error(`"update-server-info" failed with exit code ${code}`))
}
this._addRepo(repo)
cb(null, repo)
})
ps.stdout.resume()
ps.stderr.resume()
}
*/
Repos.prototype.getIdFromPath = function (rpath) {
return this._pathToId.get(normPath(rpath)) ?? null
}
Repos.prototype.matchPath = function (rpath, cb) {
}
Repos.prototype.getFromPath = function (rpath, cb) {
let id = this.getIdFromPath(rpath)
return this._repos.get(id) ?? null
}
Repos.prototype.isRepoAuthorized = function (action, repo, ids) {
if (!repo) return false
if (action === 'clone' || action === 'pull') {
return authCheck(repo.pull ?? [], ids)
} else if (action === 'push') {
return authCheck(repo.push ?? [], ids)
} else if (action === 'list') {
return authCheck(repo.list ?? [], ids)
}
return false
}
Repos.prototype.isAuthorized = function (action, rpath, ids) {
if (action === 'create' || action === 'remove' || action === 'rename') {
return this._namespaces.isAuthorized(action, rpath, ids)
}
let repo = this.getFromPath(rpath)
if (!repo) return false
if (action === 'clone' || action === 'pull') {
return authCheck(repo.pull ?? [], ids)
} else if (action === 'push') {
return authCheck(repo.push ?? [], ids)
} else if (action === 'list') {
return authCheck(repo.list ?? [], ids)
} else {
return false
}
}
function authCheck(allowList, ids) {
if (allowList.includes('*')) return true
return ids.some(id => allowList.includes(id))
}
function normPath(x) {
return x === '/' ? '/' : x.replace(/^\//,'')
}