mirror of
https://github.com/aljazceru/gitpear.git
synced 2025-12-17 14:14:22 +01:00
362 lines
9.6 KiB
JavaScript
Executable File
362 lines
9.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
const { spawn } = require('child_process')
|
|
|
|
const commander = require('commander')
|
|
const program = new commander.Command()
|
|
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
|
|
const home = require('./home')
|
|
const acl = require('./acl')
|
|
const git = require('./git')
|
|
|
|
const { listRemote, aclRemote } = require('./rpc-requests')
|
|
|
|
const { printACL, printACLForUser, checkIfGitRepo, logBranches } = require('./utils')
|
|
|
|
const pkg = require('../package.json')
|
|
program
|
|
.name('gitpear')
|
|
.description('CLI to gitpear')
|
|
.version(pkg.version)
|
|
|
|
program
|
|
.command('init')
|
|
.description('initialize a gitpear repo')
|
|
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
|
.option('-s, --share [branch]', 'share the repo as public, default false, default branch is current', '')
|
|
.action(async (p, options) => {
|
|
const fullPath = path.resolve(p)
|
|
checkIfGitRepo(fullPath)
|
|
|
|
const name = fullPath.split(path.sep).pop()
|
|
if ((home.isInitialized(name))) {
|
|
console.error(`${name} is already initialized`)
|
|
try {
|
|
await git.addRemote(name)
|
|
console.log(`Added git remote for "${name}" as "pear"`)
|
|
} catch (e) { }
|
|
process.exit(1)
|
|
}
|
|
|
|
try {
|
|
home.createAppFolder(name)
|
|
console.log(`Added project "${name}" to gitpear`)
|
|
} catch (e) { }
|
|
try {
|
|
await git.createBareRepo(name)
|
|
console.log(`Created bare repo for "${name}"`)
|
|
} catch (e) { }
|
|
try {
|
|
await git.addRemote(name)
|
|
console.log(`Added git remote for "${name}" as "pear"`)
|
|
} catch (e) { }
|
|
|
|
let branchToShare = await git.getCurrentBranch()
|
|
if (options.share && options.share !== true) {
|
|
branchToShare = options.share
|
|
}
|
|
|
|
if (options.share) await share(name, branchToShare)
|
|
})
|
|
|
|
program
|
|
.command('share')
|
|
.description('share a gitpear repo')
|
|
.option('-b, --branch [b]', 'branch to share, default is current branch', '')
|
|
.option('-v, --visibility [v]', 'visibility of the repo', 'public')
|
|
.option('-p, --path [p]', 'path to the repo', '.')
|
|
.action(async (options) => {
|
|
const fullPath = path.resolve(options.path)
|
|
checkIfGitRepo(fullPath)
|
|
|
|
const name = fullPath.split(path.sep).pop()
|
|
if (!home.isInitialized(name)) {
|
|
console.error(`${name} is not initialized`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const currentBranch = await git.getCurrentBranch()
|
|
const branchToShare = options.branch || currentBranch
|
|
await share(name, branchToShare, options)
|
|
})
|
|
|
|
program
|
|
.command('acl')
|
|
.description('manage acl of a gitpear repo')
|
|
.option('-u, --user', 'user to add/remove/list')
|
|
.option('-b, --branch', 'branch to add/remove/list in protected branches')
|
|
.addArgument(new commander.Argument('[a]', 'actiont to perform').choices(['add', 'remove', 'list']).default('list'))
|
|
.addArgument(new commander.Argument('[n]', 'user or branch to add/remove/list').default(''))
|
|
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
|
.action(async (a, n, p, options) => {
|
|
if (options.user && options.branch) {
|
|
throw new Error('Cannot perform both user and branch action at the same time')
|
|
}
|
|
|
|
if (!options.user && !options.branch) {
|
|
throw new Error('Either user or branch option is required')
|
|
}
|
|
|
|
if (n.startsWith('pear://')) {
|
|
let swap = n
|
|
n = p
|
|
p = swap
|
|
}
|
|
|
|
if (options.user) {
|
|
if (p.startsWith('pear://')) {
|
|
if (n === '.') n = ''
|
|
await remoteACL(a, n, p, options)
|
|
} else {
|
|
localACL(a, n, p, options)
|
|
}
|
|
} else if (options.branch) {
|
|
if (p.startsWith('pear://')) {
|
|
if (n === '.') n = ''
|
|
await remoteBranchProtectionRules(a, n, p, options)
|
|
} else {
|
|
localBranchProtectionRules(a, n, p, options)
|
|
}
|
|
}
|
|
})
|
|
|
|
|
|
program
|
|
.command('unshare')
|
|
.description('unshare a gitpear repo')
|
|
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
|
.action((p, options) => {
|
|
const fullPath = path.resolve(p)
|
|
checkIfGitRepo(fullPath)
|
|
|
|
const name = fullPath.split(path.sep).pop()
|
|
if ((home.isInitialized(name))) {
|
|
home.unshareAppFolder(name)
|
|
console.log(`Unshared "${name}" project`)
|
|
|
|
return
|
|
}
|
|
|
|
console.error(`${name} is not initialized`)
|
|
process.exit(1)
|
|
})
|
|
|
|
program
|
|
.command('list')
|
|
.description('list all gitpear repos')
|
|
.addArgument(new commander.Argument('[u]', 'url to remote pear').default(''))
|
|
.option('-s, --shared', 'list only shared repos')
|
|
.action((u, options) => {
|
|
if (u) return listRemote(u)
|
|
|
|
const k = home.readPk()
|
|
const s = options.shared
|
|
home.list(s).forEach(n => console.log(n, ...(s ? ['\t', `pear://${k}/${n}`] : [])))
|
|
})
|
|
|
|
program
|
|
.command('key')
|
|
.description('get a public key of gitpear')
|
|
.action((p, options) => {
|
|
console.log('Public key:', home.readPk())
|
|
})
|
|
|
|
program
|
|
.command('daemon')
|
|
.description('start/stop gitpear daemon')
|
|
.option('-s, --start', 'start daemon')
|
|
.option('-k, --stop', 'stop daemon')
|
|
.option('-a, --attach', 'watch daemon logs')
|
|
.action((p, options) => {
|
|
if (options.opts().start && options.opts().stop) {
|
|
console.error('Cannot start and stop daemon at the same time')
|
|
process.exit(1)
|
|
}
|
|
if (!options.opts().start && !options.opts().stop) {
|
|
console.error('Need either start or stop option')
|
|
process.exit(1)
|
|
}
|
|
|
|
if (options.opts().start) {
|
|
if (home.getDaemonPid()) {
|
|
console.error('Daemon already running with PID:', home.getDaemonPid())
|
|
process.exit(1)
|
|
}
|
|
|
|
const opts = {}
|
|
if (options.opts().attach) {
|
|
opts.stdio = 'inherit'
|
|
} else {
|
|
opts.detached = true
|
|
opts.stdio = [ 'ignore', home.getOutStream(), home.getErrStream() ]
|
|
}
|
|
|
|
const daemon = spawn('git-peard', opts)
|
|
console.log('Daemon started. Process ID:', daemon.pid)
|
|
home.storeDaemonPid(daemon.pid)
|
|
// TODO: remove in case of error or exit but allow unref
|
|
// daemon.on('error', home.removeDaemonPid)
|
|
// daemon.on('exit', home.removeDaemonPid)
|
|
daemon.unref()
|
|
} else if (options.opts().stop) {
|
|
if (!home.getDaemonPid()) {
|
|
console.error('Daemon not running')
|
|
process.exit(1)
|
|
}
|
|
|
|
const pid = home.getDaemonPid()
|
|
process.kill(pid)
|
|
|
|
home.removeDaemonPid()
|
|
console.log('Daemon stopped. Process ID:', pid)
|
|
} else {
|
|
console.error('No option provided')
|
|
process.exit(1)
|
|
}
|
|
})
|
|
|
|
function localBranchProtectionRules(a, b, p, options) {
|
|
const fullPath = path.resolve(p)
|
|
checkIfGitRepo(fullPath)
|
|
|
|
const name = fullPath.split(path.sep).pop()
|
|
if (!home.isInitialized(name)) {
|
|
console.error(`${name} is not initialized`)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (a === 'list' && !b) {
|
|
const repoACL = acl.getACL(name)
|
|
logBranches(repoACL)
|
|
}
|
|
|
|
if (a === 'add') {
|
|
acl.addProtectedBranch(name, b)
|
|
const repoACL = acl.getACL(name)
|
|
logBranches(repoACL)
|
|
}
|
|
if (a === 'remove') {
|
|
acl.removeProtectedBranch(name, b)
|
|
const repoACL = acl.getACL(name)
|
|
logBranches(repoACL)
|
|
}
|
|
}
|
|
|
|
function localACL(a, u, p, options) {
|
|
const fullPath = path.resolve(p)
|
|
checkIfGitRepo(fullPath)
|
|
|
|
const name = fullPath.split(path.sep).pop()
|
|
if (!home.isInitialized(name)) {
|
|
console.error(`${name} is not initialized`)
|
|
process.exit(1)
|
|
}
|
|
const repoACL = acl.getACL(name)
|
|
|
|
if (a === 'list' && !u) {
|
|
printACL(repoACL)
|
|
return
|
|
}
|
|
|
|
if (a === 'list') {
|
|
printACLForUser(repoACL, u)
|
|
return
|
|
}
|
|
|
|
if (a === 'add') {
|
|
if (!u) {
|
|
console.error('User not provided')
|
|
process.exit(1)
|
|
}
|
|
|
|
const [ userId, role ] = u.split(':')
|
|
if (repoACL.ACL[userId]) {
|
|
console.error(`${userId} already has access to ${name} as ${repoACL.ACL[userId]}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
acl.grantAccessToUser(name, userId, role)
|
|
console.log(`Added ${userId} to ${name} as ${role}`)
|
|
return
|
|
}
|
|
|
|
if (a === 'remove') {
|
|
if (!u) {
|
|
console.error('User not provided')
|
|
process.exit(1)
|
|
}
|
|
|
|
if (!repoACL.ACL[u]) {
|
|
console.error(`${u} does not have access to ${name}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
acl.revokeAccessFromUser(name, u)
|
|
console.log(`Removed ${u} from ${name}`)
|
|
return
|
|
}
|
|
}
|
|
|
|
async function remoteBranchProtectionRules(a, b, p, options) {
|
|
if (a === 'list') {
|
|
await aclRemote.list(p, b, { branch: true })
|
|
} else if (a === 'add') {
|
|
await aclRemote.add(p, b, { branch: true })
|
|
if (!b) {
|
|
console.error('branch is not provided')
|
|
process.exit(1)
|
|
}
|
|
} else if (a === 'remove') {
|
|
if (!b) {
|
|
console.error('branch is not provided')
|
|
process.exit(1)
|
|
}
|
|
await aclRemote.remove(p, b, { branch: true })
|
|
} else {
|
|
throw new Error('Invalid action')
|
|
}
|
|
}
|
|
|
|
async function remoteACL(a, b, p, options) {
|
|
if (a === 'list') {
|
|
await aclRemote.list(p, b)
|
|
} else if (a === 'add') {
|
|
if (!b) {
|
|
console.error('User not provided')
|
|
process.exit(1)
|
|
}
|
|
if (b.split(':').length !== 2) {
|
|
console.error('Invalid role')
|
|
process.exit(1)
|
|
}
|
|
await aclRemote.add(p, b)
|
|
} else if (a === 'remove') {
|
|
if (!b) {
|
|
console.error('User not provided')
|
|
process.exit(1)
|
|
}
|
|
await aclRemote.remove(p, b)
|
|
} else {
|
|
throw new Error('Invalid action')
|
|
}
|
|
}
|
|
|
|
async function share(name, branchToShare, options) {
|
|
let aclOptions
|
|
let message = `Shared "${name}" project, ${branchToShare} branch`
|
|
if (options?.visibility) {
|
|
aclOptions = { visibility: options.visibility }
|
|
message = `${message}, as ${options.visibility} repo`
|
|
}
|
|
|
|
try { home.shareAppFolder(name) } catch (e) { }
|
|
try { acl.setACL(name, aclOptions) } catch (e) { }
|
|
try { await git.push(branchToShare) } catch (e) { }
|
|
console.log(message)
|
|
}
|
|
|
|
program.parse()
|