Merge branch 'dzdidi:master' into master

This commit is contained in:
2024-02-18 17:48:01 +00:00
committed by GitHub
21 changed files with 650 additions and 344 deletions

View File

@@ -38,13 +38,13 @@ All data will be persisted in application directory (default `~/.gitpear`). To c
* `git pear daemon <-s, --start | -k, --stop>` - start or stop daemon * `git pear daemon <-s, --start | -k, --stop>` - start or stop daemon
* `git pear key` - print out public key. Share it with your peers so that they can do `git pull pear:<public key>/<repo name>` * `git pear key` - print out public key. Share it with your peers so that they can do `git pull pear:<public key>/<repo name>`
* `git pear init <path> [-s, --share [branch]]` - It will create [bare repository](https://git-scm.com/docs/git-init#Documentation/git-init.txt---bare) of the same name in application directory (default ~/.gitpear/<repository name>). It will add [git remote](https://git-scm.com/docs/git-remote) in current repository with name `pear`. So just like in traditional flow doing `git push orign`, here we do `git push pear`. By default repository will not be shared. To enable sharing provide `-s | --share [branch]` (default branch to share is current) or call `gitpear share <path>` later * `git pear init [-s, --share [branch]]` - It will create [bare repository](https://git-scm.com/docs/git-init#Documentation/git-init.txt---bare) of the same name in application directory (default ~/.gitpear/<repository name>). It will add [git remote](https://git-scm.com/docs/git-remote) in current repository with name `pear`. So just like in traditional flow doing `git push orign`, here we do `git push pear`. By default repository will not be shared. To enable sharing provide `-s | --share [branch]` (default branch to share is current) or call `gitpear share` later
* `git pear share [-p, --path [path (default: ".")]> [-b, --branch [branch name (default: "_current_")] [-v, --visibility <private|public> (default: "public")]` - share repository, if branch is not specified, default branch will be shared * `git pear share [-b, --branch [branch name (default: "_current_")] [-v, --visibility <private|public> (default: "public")]` - share current repository, if branch is not specified, default branch will be shared
* `git pear unshare <path>` - stop sharing repository * `git pear unshare` - stop sharing current repository
* `git pear list [-s, --shared]` - list all or (only shared) repositories * `git pear list [-s, --shared]` - list all or (only shared) repositories
* `git pear list <url>` - list repositories of a peer * `git pear list <url>` - list repositories of a peer
### ACL (for authenticated access to enable support of PUSH) ### User Access and Branch Protection Rules (for authenticated access to enable support of PUSH)
Support of `push` capabilities only enabled for authenticated users. Currently supported authentications are based on: Support of `push` capabilities only enabled for authenticated users. Currently supported authentications are based on:
* [noise](https://github.com/libp2p/specs/blob/master/noise/README.md); * [noise](https://github.com/libp2p/specs/blob/master/noise/README.md);
@@ -62,22 +62,26 @@ or
GIT_PEAR_AUTH=native git pear daemon -s GIT_PEAR_AUTH=native git pear daemon -s
``` ```
#### User Access Control
To manage access to repository use one or combination of the following commands, if `path` is not provide the command will be executed in the current directory. For `userId` use [NIP19 npub](https://github.com/nostr-protocol/nips/blob/master/19.md). To manage access to repository use one or combination of the following commands, if `path` is not provide the command will be executed in the current directory. For `userId` use [NIP19 npub](https://github.com/nostr-protocol/nips/blob/master/19.md).
* `git pear acl [command] <path>` - ACL managegement * `git pear acl -u [command] -p <repo path or url (default ".")>` - ACL managegement of for users access in local or remote repo (requires `owner` permission for remote repositories)
* `git pear acl list [userId] <path>` - list repository visitbility and user's role (or roles of all users if userId is not provided) * `git pear acl -u list [userId] -p <repo path or url (default ".")>` - list repository visitbility and user's role (or roles of all users if userId is not provided)
* `git pear acl add <userId:role> <path>` - add user as a "role" to repository, available roles are `viewer`, `contributor`, `admin`. Roles exaplained: * `git pear acl add -u <userId:role> -p <repo path or url (default ".")>` - add user as a "role" to repository available roles are `viewer`, `contributor`, `admin`, `owner`. Roles exaplained:
* `viewer` - can read all branches; * `viewer` - can read all branches;
* `contributor` - can edit all branches except protected (default master) * `contributor` - can edit all branches except protected (default master)
* `admin` - can edit protected branches * `admin` - can edit protected branches
* `git pear acl remove <userId> <path>` - revoke use access to repository * `owner` - can edit repo ack
* `git pear acl remove -u| <userId> -p <repo path or url (default ".")>` - revoke use access to repository.
### Branch protection rules ### Branch protection rules
It is possible to setup basic branch protection rules (master is proteted by default). It is possible to setup basic branch protection rules (master is proteted by default).
* `git pear branch`, same as `git pear branch list .` - list protection rules * `git pear acl -b [command] -p <repo path or url (deafult ".")`, same as `git pear branch list` - list protection rules
* `git pear branch add <branch name> <repo path>` - mark branch as protected (defatul repo path is ".") * `git pear acl -b list` - git pear branch list .` - list protection rules;
* `git pear branch remove <branch name> <repo path>` - unmark branch as protected * `git pear acl -b add <branchName> <repo path or url (default ".")>` - mark branch as protected;
* `git pear acl -b remove <branchName> <repo path or url (deafult ".")>` - unmark branch as protected;
# Examples of usage # Examples of usage
@@ -128,7 +132,7 @@ git pull
## Authenticated usage example (push) - at your own risk ## Authenticated usage example (push) - at your own risk
Collaboration is possible with the following flow between Carol and David in a peer-to-peer manner. Collaboration is possible with the following flow between Carol and Dave in a peer-to-peer manner.
Supported authentication methods are `native` and `nip98`. The `nip98` authentication, requires environment variable `GIT_PEAR_AUTH_NSEC` with nsec Supported authentication methods are `native` and `nip98`. The `nip98` authentication, requires environment variable `GIT_PEAR_AUTH_NSEC` with nsec
@@ -138,12 +142,12 @@ Supported authentication methods are `native` and `nip98`. The `nip98` authentic
2. Go to repository 2. Go to repository
* `cd repo` * `cd repo`
3. Initialize git pear repository 3. Initialize git pear repository
* `git pear init .` * `git pear init`
4. Share repository wit hviben visibility () - (default is `public`) 4. Share repository wit hviben visibility () - (default is `public`)
* `git pear share . public` * `git pear share public`
5. Add Daviv as a `contirbutor`. 5. Add Dave as a `contirbutor`.
6. List David's npub as a contributor 6. List Dave's npub as a contributor
* `git pear acl add <David pub key hex>:contributor` * `git pear acl add <Dave pub key hex>:contributor`
7. Retreive repo url and share it with Dave 7. Retreive repo url and share it with Dave
* `git pear list -s` * `git pear list -s`

View File

@@ -2,15 +2,18 @@ const home = require('./home')
const fs = require('fs') const fs = require('fs')
const ROLES = { const ROLES = {
owner: {
description: 'Read and write to all branches, and ACL management'
},
admin: { admin: {
description: 'Read and write to all branches', description: 'Read and write to all branches'
}, },
contributor: { contributor: {
description: 'Read and write to all branches except protected ones', description: 'Read and write to all branches except protected ones'
}, },
viewer: { viewer: {
description: 'Read all branches', description: 'Read all branches'
}, }
} }
const DEFAULT_ACL = { const DEFAULT_ACL = {
visibility: 'public', // public|private visibility: 'public', // public|private
@@ -23,9 +26,16 @@ function getUserRole (repoName, user) {
return acl.ACL[user] return acl.ACL[user]
} }
function getOwners (repoName) {
const acl = getACL(repoName)
return Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'owner')
}
function getAdmins (repoName) { function getAdmins (repoName) {
const acl = getACL(repoName) const acl = getACL(repoName)
return Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'admin') const admins = Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'admin')
const owners = getOwners(repoName)
return [...admins, ...owners].filter((user, i, arr) => arr.indexOf(user) === i)
} }
function getContributors (repoName) { function getContributors (repoName) {
@@ -113,8 +123,9 @@ module.exports = {
getACL, getACL,
addProtectedBranch, addProtectedBranch,
removeProtectedBranch, removeProtectedBranch,
getOwners,
getAdmins, getAdmins,
getContributors, getContributors,
getViewers, getViewers,
revokeAccessFromUser, revokeAccessFromUser
} }

View File

@@ -1,5 +1,5 @@
function getId (data) { function getId (data) {
if (!process.env.GIT_PEAR_AUTH) return payload if (!process.env.GIT_PEAR_AUTH) return data
if (process.env.GIT_PEAR_AUTH === 'nip98') { if (process.env.GIT_PEAR_AUTH === 'nip98') {
const nip98 = require('./nip98') const nip98 = require('./nip98')
return nip98.getId(data) return nip98.getId(data)
@@ -7,7 +7,7 @@ function getId(data) {
} }
async function getToken (payload) { async function getToken (payload) {
if (!process.env.GIT_PEAR_AUTH) return userId if (!process.env.GIT_PEAR_AUTH) return payload
if (process.env.GIT_PEAR_AUTH === 'nip98') { if (process.env.GIT_PEAR_AUTH === 'nip98') {
const nip98 = require('./nip98') const nip98 = require('./nip98')
return nip98.getToken(payload) return nip98.getToken(payload)

View File

@@ -1,6 +1,7 @@
const { nip98, nip19, finalizeEvent } = require('nostr-tools') const { nip98, nip19, finalizeEvent } = require('nostr-tools')
async function getToken ({ url, method, data }) { async function getToken ({ url, method, data }) {
if (!process.env.GIT_PEAR_AUTH_NSEC) throw new Error('Missing NSEC')
const { data: sK } = nip19.decode(process.env.GIT_PEAR_AUTH_NSEC) const { data: sK } = nip19.decode(process.env.GIT_PEAR_AUTH_NSEC)
return nip98.getToken( return nip98.getToken(
url, url,

156
src/cli-helpers.js Normal file
View File

@@ -0,0 +1,156 @@
const path = require('path')
const home = require('./home')
const acl = require('./acl')
const git = require('./git')
const { aclRemote } = require('./rpc-requests')
const { printACL, printACLForUser, checkIfGitRepo, logBranches } = require('./utils')
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)
}
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')
}
}
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}`)
}
}
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)
}
}
module.exports = {
remoteACL,
share,
remoteBranchProtectionRules,
localACL,
localBranchProtectionRules
}

View File

@@ -6,12 +6,15 @@ const commander = require('commander')
const program = new commander.Command() const program = new commander.Command()
const path = require('path') const path = require('path')
const fs = require('fs')
const home = require('./home') const home = require('./home')
const acl = require('./acl')
const git = require('./git') const git = require('./git')
const { listRemote } = require('./rpc-requests')
const { checkIfGitRepo } = require('./utils')
const { remoteACL, share, remoteBranchProtectionRules, localACL, localBranchProtectionRules } = require('./cli-helpers')
const pkg = require('../package.json') const pkg = require('../package.json')
program program
.name('gitpear') .name('gitpear')
@@ -21,20 +24,18 @@ program
program program
.command('init') .command('init')
.description('initialize a gitpear repo') .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', '') .option('-s, --share [branch]', 'share the repo as public, default false, default branch is current', '')
.action(async (p, options) => { .action(async (options) => {
const fullPath = path.resolve(p) const fullPath = path.resolve('.')
if (!fs.existsSync(path.join(fullPath, '.git'))) { checkIfGitRepo(fullPath)
console.error('Not a git repo')
process.exit(1)
}
const name = fullPath.split(path.sep).pop() const name = fullPath.split(path.sep).pop()
if ((home.isInitialized(name))) { if ((home.isInitialized(name))) {
console.error(`${name} is already initialized`) console.error(`${name} is already initialized`)
try {
await git.addRemote(name) await git.addRemote(name)
console.log(`Added git remote for "${name}" as "pear"`) console.log(`Added git remote for "${name}" as "pear"`)
} catch (e) { }
process.exit(1) process.exit(1)
} }
@@ -56,12 +57,7 @@ program
branchToShare = options.share branchToShare = options.share
} }
if (options.share) { if (options.share) await share(name, branchToShare)
try { home.shareAppFolder(name) } catch (e) { }
try { acl.setACL(name) } catch (e) { }
try { await git.push(branchToShare) } catch (e) { }
console.log(`Shared "${name}" project, ${branchToShare} branch`)
}
}) })
program program
@@ -69,13 +65,9 @@ program
.description('share a gitpear repo') .description('share a gitpear repo')
.option('-b, --branch [b]', 'branch to share, default is current branch', '') .option('-b, --branch [b]', 'branch to share, default is current branch', '')
.option('-v, --visibility [v]', 'visibility of the repo', 'public') .option('-v, --visibility [v]', 'visibility of the repo', 'public')
.option('-p, --path [p]', 'path to the repo', '.')
.action(async (options) => { .action(async (options) => {
const fullPath = path.resolve(options.path) const fullPath = path.resolve('.')
if (!fs.existsSync(path.join(fullPath, '.git'))) { checkIfGitRepo(fullPath)
console.error('Not a git repo')
process.exit(1)
}
const name = fullPath.split(path.sep).pop() const name = fullPath.split(path.sep).pop()
if (!home.isInitialized(name)) { if (!home.isInitialized(name)) {
@@ -85,137 +77,49 @@ program
const currentBranch = await git.getCurrentBranch() const currentBranch = await git.getCurrentBranch()
const branchToShare = options.branch || currentBranch const branchToShare = options.branch || currentBranch
try { home.shareAppFolder(name) } catch (e) { } await share(name, branchToShare, options)
try { acl.setACL(name, { visibility: options.visibility }) } catch (e) { }
try { await git.push(branchToShare) } catch (e) { }
console.log(`Shared "${name}" project, ${branchToShare} branch, as ${options.visibility} repo`)
return
}) })
program
.command('branch')
.description('branch protection rules')
.addArgument(new commander.Argument('[a]', 'actiont to perform').choices(['add', 'remove', 'list']).default('list'))
.addArgument(new commander.Argument('[b]', 'branch name').default(''))
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
.action(async (a, b, p, options) => {
const fullPath = path.resolve(p)
if (!fs.existsSync(path.join(fullPath, '.git'))) {
console.error('Not a git repo')
process.exit(1)
}
const name = fullPath.split(path.sep).pop()
if (!home.isInitialized(name)) {
console.error(`${name} is not initialized`)
process.exit(1)
}
if (a === 'list' && !b) { logBranches(name) }
if (a === 'add') {
acl.addProtectedBranch(name, b)
logBranches(name)
}
if (a === 'remove') {
acl.removeProtectedBranch(name, b)
logBranches(name)
}
function logBranches(name) {
const repoACL = acl.getACL(name)
console.log('Visibility:', '\t', repoACL.visibility)
console.log('Branch:')
for (const branch of repoACL.protectedBranches) { console.log(branch) }
}
return
})
program program
.command('acl') .command('acl')
.description('set acl of a gitpear repo') .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')
.option('-p, --path [path]', 'path to the repo', '.')
.addArgument(new commander.Argument('[a]', 'actiont to perform').choices(['add', 'remove', 'list']).default('list')) .addArgument(new commander.Argument('[a]', 'actiont to perform').choices(['add', 'remove', 'list']).default('list'))
.addArgument(new commander.Argument('[u]', 'user to add/remove/list').default('')) .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, options) => {
.action(async (a, u, p, options) => { if (options.user && options.branch) {
throw new Error('Cannot perform both user and branch action at the same time')
// TODO: add branch protection logic
const fullPath = path.resolve(p)
if (!fs.existsSync(path.join(fullPath, '.git'))) {
console.error('Not a git repo')
process.exit(1)
} }
const name = fullPath.split(path.sep).pop() if (!options.user && !options.branch) {
if (!home.isInitialized(name)) { throw new Error('Either user or branch option is required')
console.error(`${name} is not initialized`)
process.exit(1)
}
const repoACL = acl.getACL(name)
if (a === 'list' && !u) {
console.log('Repo Visibility:', '\t', repoACL.visibility)
console.log('User:', '\t', 'Role:')
for (const user in repoACL.ACL) {
console.log(user, '\t', repoACL.ACL[user])
}
return
} }
if (a === 'list') { if (options.user) {
console.log('Repo Visibility:', '\t', repoACL.visibility) if (options.path.startsWith('pear://')) {
console.log('User:', u, '\t', repoACL.ACL[u]) if (n === '.') n = ''
return await remoteACL(a, n, options.path, options)
} else {
localACL(a, n, options.path, options)
} }
} else if (options.branch) {
if (a === 'add') { if (options.path.startsWith('pear://')) {
if (!u) { await remoteBranchProtectionRules(a, n, options.path, options)
console.error('User not provided') } else {
process.exit(1) localBranchProtectionRules(a, n, options.path, options)
} }
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
} }
}) })
program program
.command('unshare') .command('unshare')
.description('unshare a gitpear repo') .description('unshare a gitpear repo')
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.')) .option('-p, --path [path]', 'path to the repo', '.')
.action((p, options) => { .action((options) => {
const fullPath = path.resolve(p) const fullPath = path.resolve(options.path)
if (!fs.existsSync(path.join(fullPath, '.git'))) { checkIfGitRepo(fullPath)
console.error('Not a git repo')
process.exit(1)
}
const name = fullPath.split(path.sep).pop() const name = fullPath.split(path.sep).pop()
if ((home.isInitialized(name))) { if ((home.isInitialized(name))) {
@@ -235,7 +139,7 @@ program
.addArgument(new commander.Argument('[u]', 'url to remote pear').default('')) .addArgument(new commander.Argument('[u]', 'url to remote pear').default(''))
.option('-s, --shared', 'list only shared repos') .option('-s, --shared', 'list only shared repos')
.action((u, options) => { .action((u, options) => {
if (u) return require('./list-remote')(u) if (u) return listRemote(u)
const k = home.readPk() const k = home.readPk()
const s = options.shared const s = options.shared

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
const { spawn } = require('child_process')
const ProtomuxRPC = require('protomux-rpc') const ProtomuxRPC = require('protomux-rpc')
const RAM = require('random-access-memory') const RAM = require('random-access-memory')
@@ -14,8 +13,6 @@ const home = require('./home')
const auth = require('./auth') const auth = require('./auth')
const acl = require('./acl') const acl = require('./acl')
const fs = require('fs')
const url = process.argv[3] const url = process.argv[3]
const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/) const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/)
@@ -29,7 +26,7 @@ const repoName = matches[2]
const store = new Corestore(RAM) const store = new Corestore(RAM)
const swarmOpts = {} const swarmOpts = {}
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') { if (process.env.GIT_PEAR_AUTH === 'native') {
swarmOpts.keyPair = home.getKeyPair() swarmOpts.keyPair = home.getKeyPair()
} }
const swarm = new Hyperswarm(swarmOpts) const swarm = new Hyperswarm(swarmOpts)
@@ -99,7 +96,7 @@ async function talkToGit (refs, drive, repoName, rpc, commit) {
process.stdout.write('push\n') process.stdout.write('push\n')
process.stdout.write('fetch\n\n') process.stdout.write('fetch\n\n')
} else if (chunk && chunk.search(/^push/) !== -1) { } else if (chunk && chunk.search(/^push/) !== -1) {
const [_command, path] = chunk.split(' ') const path = chunk.split(' ')[1]
let [src, dst] = path.split(':') let [src, dst] = path.split(':')
const isDelete = !src const isDelete = !src
@@ -107,13 +104,6 @@ async function talkToGit (refs, drive, repoName, rpc, commit) {
dst = dst.replace('refs/heads/', '').replace('\n\n', '') dst = dst.replace('refs/heads/', '').replace('\n\n', '')
try { home.createAppFolder(repoName) } catch (e) { }
try { await git.createBareRepo(repoName) } catch (e) { }
try { await git.addRemote(repoName) } catch (e) { }
try { await git.push(dst) } catch (e) { }
try { home.shareAppFolder(repoName) } catch (e) { }
try { acl.setACL(repoName, acl.getACL(repoName)) } catch (e) { }
let method let method
if (isDelete) { if (isDelete) {
method = 'd-branch' method = 'd-branch'
@@ -128,16 +118,25 @@ async function talkToGit (refs, drive, repoName, rpc, commit) {
method = 'push' method = 'push'
} }
try { home.createAppFolder(repoName) } catch (e) { }
try { await git.createBareRepo(repoName) } catch (e) { }
try { await git.addRemote(repoName, { quite: true }) } catch (e) { }
try { await git.push(dst, isForce) } catch (e) { }
try { home.shareAppFolder(repoName) } catch (e) { }
try { acl.setACL(repoName, acl.getACL(repoName)) } catch (e) { }
const publicKey = home.readPk() const publicKey = home.readPk()
let payload = { body: { const payload = {
body: {
url: `pear://${publicKey}/${repoName}`, url: `pear://${publicKey}/${repoName}`,
data: `${dst}#${commit}`, data: `${dst}#${commit}`,
method method
} } }
}
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') { if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
payload.header = await auth.getToken(payload.body) payload.header = await auth.getToken(payload.body)
} }
const res = await rpc.request(method, Buffer.from(JSON.stringify(payload))) await rpc.request(method, Buffer.from(JSON.stringify(payload)))
process.stdout.write('\n\n') process.stdout.write('\n\n')
process.exit(0) process.exit(0)

View File

@@ -64,9 +64,9 @@ async function createBareRepo (name) {
return await doGit(init) return await doGit(init)
} }
async function addRemote (name) { async function addRemote (name, opts = { quiet: false }) {
const init = spawn('git', ['remote', 'add', 'pear', getCodePath(name)]) const init = spawn('git', ['remote', 'add', 'pear', getCodePath(name)])
return await doGit(init) return await doGit(init, opts)
} }
async function push (branch = 'master', force = false) { async function push (branch = 'master', force = false) {
@@ -76,8 +76,8 @@ async function push (branch = 'master', force = false) {
return await doGit(push) return await doGit(push)
} }
async function doGit (child) { async function doGit (child, opts = { quiet: false }) {
child.stderr.pipe(process.stderr) if (!opts.quiet) child.stderr.pipe(process.stderr)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
child.on('close', (code) => { child.on('close', (code) => {
if (code) { if (code) {
@@ -229,5 +229,5 @@ module.exports = {
addRemote, addRemote,
push, push,
getCommit, getCommit,
getCurrentBranch, getCurrentBranch
} }

53
src/rpc-handlers/acl.js Normal file
View File

@@ -0,0 +1,53 @@
const ACL = require('../acl')
const home = require('../home')
async function getACLHandler (publicKey, req) {
const { repoName } = await parseACLRequest.bind(this)(publicKey, req)
const repoACL = ACL.getACL(repoName)
return Buffer.from(JSON.stringify(repoACL))
}
async function addACLHandler (publicKey, req) {
const { repoName, isBranch, name } = await parseACLRequest.bind(this)(publicKey, req)
isBranch ? ACL.addProtectedBranch(repoName, name) : ACL.grantAccessToUser(repoName, ...name.split(':'))
const repoACL = ACL.getACL(repoName)
return Buffer.from(JSON.stringify(repoACL))
}
async function delACLHandler (publicKey, req) {
const { repoName, isBranch, name } = await parseACLRequest.bind(this)(publicKey, req)
isBranch ? ACL.removeProtectedBranch(repoName, name) : ACL.revokeAccessFromUser(repoName, name)
const repoACL = ACL.getACL(repoName)
return Buffer.from(JSON.stringify(repoACL))
}
async function parseACLRequest (publicKey, req) {
if (!req) throw new Error('Request is empty')
const request = JSON.parse(req.toString())
const userId = await this.authenticate(publicKey, request)
const repoName = request.body.url?.split('/')?.pop()
if (!home.isInitialized(repoName)) throw new Error('Repo does not exist')
const isOwner = ACL.getOwners(repoName).includes(userId)
if (!isOwner) throw new Error('You are not allowed to access this repo')
return {
repoName,
name: request.body.name,
userId,
acl: request.body.acl,
isBranch: !!request.body.branch
}
}
module.exports = {
getACLHandler,
addACLHandler,
delACLHandler
}

122
src/rpc-handlers/git.js Normal file
View File

@@ -0,0 +1,122 @@
const ACL = require('../acl')
const home = require('../home')
const { spawn } = require('child_process')
async function getReposHandler (publicKey, req) {
const { userId } = await parseReq.bind(this)(publicKey, req)
const res = {}
for (const repoName in this.repositories) {
const isPublic = (ACL.getACL(repoName).visibility === 'public')
if (isPublic || ACL.getViewers(repoName).includes(userId)) {
res[repoName] = this.drives[repoName].key.toString('hex')
}
}
return Buffer.from(JSON.stringify(res))
}
async function getRefsHandler (publicKey, req) {
const { repoName, userId } = await parseReq.bind(this)(publicKey, req)
const res = this.repositories[repoName]
const isPublic = (ACL.getACL(repoName).visibility === 'public')
if (isPublic || ACL.getViewers(repoName).includes(userId)) {
return Buffer.from(JSON.stringify(res))
} else {
throw new Error('You are not allowed to access this repo')
}
}
async function pushHandler (publicKey, req) {
const { url, repoName, branch, userId } = await parseReq.bind(this)(publicKey, req)
const isContributor = ACL.getContributors(repoName).includes(userId)
if (!isContributor) throw new Error('You are not allowed to push to this repo')
const isProtectedBranch = ACL.getACL(repoName).protectedBranches.includes(branch)
const isAdmin = ACL.getAdmins(repoName).includes(userId)
if (isProtectedBranch && !isAdmin) throw new Error('You are not allowed to push to this branch')
return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['fetch', url, `${branch}:${branch}`], { env })
return doGit(child, resolve, reject)
})
}
async function forcePushHandler (publicKey, req) {
const { url, repoName, branch, userId } = await parseReq.bind(this)(publicKey, req)
const isContributor = ACL.getContributors(repoName).includes(userId)
if (!isContributor) throw new Error('You are not allowed to push to this repo')
const isProtectedBranch = ACL.getACL(repoName).protectedBranches.includes(branch)
const isAdmin = ACL.getAdmins(repoName).includes(userId)
if (isProtectedBranch && !isAdmin) throw new Error('You are not allowed to push to this branch')
return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env })
return doGit(child, resolve, reject)
})
}
async function deleteBranchHandler (publicKey, req) {
const { repoName, branch, userId } = await parseReq.bind(this)(publicKey, req)
const isContributor = ACL.getContributors(repoName).includes(userId)
if (!isContributor) throw new Error('You are not allowed to push to this repo')
const isProtectedBranch = ACL.getACL(repoName).protectedBranches.includes(branch)
const isAdmin = ACL.getAdmins(repoName).includes(userId)
if (isProtectedBranch && !isAdmin) throw new Error('You are not allowed to push to this branch')
return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['branch', '-D', branch], { env })
return doGit(child, resolve, reject)
})
}
async function parseReq (publicKey, req) {
if (!req) throw new Error('Request is empty')
const request = JSON.parse(req.toString())
const parsed = {
repoName: request.body.url?.split('/')?.pop(),
branch: request.body.data?.split('#')[0],
url: request.body.url,
userId: await this.authenticate(publicKey, request)
}
return parsed
}
function doGit (child, resolve, reject) {
let errBuffer = Buffer.from('')
let outBuffer = Buffer.from('')
child.stdout.on('data', data => {
outBuffer = Buffer.concat([outBuffer, data])
})
child.stderr.on('data', data => {
errBuffer = Buffer.concat([errBuffer, data])
})
child.on('close', code => {
console.error('errBuffer', errBuffer.toString())
console.log('outBuffer', outBuffer.toString())
return code === 0 ? resolve(outBuffer) : reject(errBuffer)
})
}
module.exports = {
getReposHandler,
getRefsHandler,
pushHandler,
forcePushHandler,
deleteBranchHandler
}

View File

@@ -0,0 +1,7 @@
const git = require('./git')
const acl = require('./acl')
module.exports = {
git,
acl
}

View File

@@ -0,0 +1,116 @@
const ProtomuxRPC = require('protomux-rpc')
const Hyperswarm = require('hyperswarm')
const crypto = require('hypercore-crypto')
const home = require('../home')
const auth = require('../auth')
const { printACL, printACLForUser, logBranches } = require('../utils')
async function list (url, name, rpc, opts) {
const payload = { body: { url, method: 'get-acl' } }
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
payload.header = await auth.getToken(payload.body)
}
const repoACLres = await rpc.request('get-acl', Buffer.from(JSON.stringify(payload)))
const repoACL = JSON.parse(repoACLres.toString())
opts.branch ? listACLBranch(repoACL) : listACLUser(repoACL, name)
process.exit(0)
}
function listACLUser (repoACL, u) {
u ? printACLForUser(repoACL, u) : printACL(repoACL)
}
function listACLBranch (repoACL) {
logBranches(repoACL)
}
async function add (url, name, rpc, opts) {
const payload = { body: { url, method: 'add-acl', name } }
if (opts.branch) payload.body.branch = true
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
payload.header = await auth.getToken(payload.body)
}
const repoACLres = await rpc.request('add-acl', Buffer.from(JSON.stringify(payload)))
const repoACL = JSON.parse(repoACLres.toString())
opts.branch ? listACLBranch(repoACL) : listACLUser(repoACL, name.split(':')[0])
process.exit(0)
}
async function del (url, name, rpc, opts) {
const payload = { body: { url, method: 'del-acl', name } }
if (opts.branch) payload.body.branch = true
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
payload.header = await auth.getToken(payload.body)
}
const repoACLres = await rpc.request('del-acl', Buffer.from(JSON.stringify(payload)))
const repoACL = JSON.parse(repoACLres.toString())
opts.branch ? listACLBranch(repoACL) : listACLUser(repoACL, name)
process.exit(0)
}
async function wrapper (url, name, opts = {}, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/)
if (!matches || matches.length < 3) {
console.error('Invalid URL')
process.exit(1)
}
const targetKey = matches[1]
const repoName = matches[2]
console.log('Connecting to:', targetKey)
const swarmOpts = {}
if (process.env.GIT_PEAR_AUTH === 'native') {
swarmOpts.keyPair = home.getKeyPair()
}
const swarm = new Hyperswarm(swarmOpts)
swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false })
swarm.on('connection', async (socket) => {
const rpc = new ProtomuxRPC(socket)
const payload = { body: { url, method: 'get-repos' } }
if (!process.env.GIT_PEAR_AUTH) {
console.debug('Retreiving data using un-authenticated access')
} else {
console.debug('Retreiving data using authenticated access')
}
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
payload.header = await auth.getToken(payload.body)
}
const reposRes = await rpc.request('get-repos', Buffer.from(JSON.stringify(payload)))
const repositories = JSON.parse(reposRes.toString())
if (!repositories) {
console.error('Failed to retrieve repositories')
process.exit(1)
}
if (!repositories[repoName]) {
console.error('Repository not found')
process.exit(1)
}
await cb(url, name, rpc, opts)
})
}
module.exports = {
list: (url, name, opts) => wrapper(url, name, opts, list),
add: (url, name, opts) => wrapper(url, name, opts, add),
remove: (url, name, opts) => wrapper(url, name, opts, del)
}

View File

@@ -0,0 +1,7 @@
const listRemote = require('./list-remote')
const aclRemote = require('./acl-remote')
module.exports = {
listRemote,
aclRemote
}

View File

@@ -3,8 +3,8 @@ const ProtomuxRPC = require('protomux-rpc')
const Hyperswarm = require('hyperswarm') const Hyperswarm = require('hyperswarm')
const crypto = require('hypercore-crypto') const crypto = require('hypercore-crypto')
const home = require('./home') const home = require('../home')
const auth = require('./auth') const auth = require('../auth')
module.exports = async function listRemote (url) { module.exports = async function listRemote (url) {
const matches = url.match(/pear:\/\/([a-f0-9]{64})/) const matches = url.match(/pear:\/\/([a-f0-9]{64})/)
@@ -18,7 +18,7 @@ module.exports = async function listRemote (url) {
console.log('Connecting to:', targetKey) console.log('Connecting to:', targetKey)
const swarmOpts = {} const swarmOpts = {}
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') { if (process.env.GIT_PEAR_AUTH === 'native') {
swarmOpts.keyPair = home.getKeyPair() swarmOpts.keyPair = home.getKeyPair()
} }
const swarm = new Hyperswarm(swarmOpts) const swarm = new Hyperswarm(swarmOpts)
@@ -28,14 +28,15 @@ module.exports = async function listRemote (url) {
swarm.on('connection', async (socket) => { swarm.on('connection', async (socket) => {
const rpc = new ProtomuxRPC(socket) const rpc = new ProtomuxRPC(socket)
let payload = { body: { url, method: 'get-repos' } } const payload = { body: { url, method: 'get-repos' } }
if (!process.env.GIT_PEAR_AUTH) {
console.debug('Retreiving data using un-authenticated access')
} else {
console.debug('Retreiving data using authenticated access')
}
if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') { if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') {
payload.header = await auth.getToken(payload.body) payload.header = await auth.getToken(payload.body)
console.debug('Retreiving data using authenticated access')
} else {
console.debug('Retreiving data using un-authenticated access')
} }
console.log()
const reposRes = await rpc.request('get-repos', Buffer.from(JSON.stringify(payload))) const reposRes = await rpc.request('get-repos', Buffer.from(JSON.stringify(payload)))
const repositories = JSON.parse(reposRes.toString()) const repositories = JSON.parse(reposRes.toString())

View File

@@ -1,9 +1,6 @@
const ProtomuxRPC = require('protomux-rpc') const ProtomuxRPC = require('protomux-rpc')
const SecretStream = require('@hyperswarm/secret-stream')
const { spawn } = require('child_process')
const home = require('./home')
const auth = require('./auth') const auth = require('./auth')
const acl = require('./acl') const { git, acl } = require('./rpc-handlers')
module.exports = class RPC { module.exports = class RPC {
constructor (announcedRefs, repositories, drives) { constructor (announcedRefs, repositories, drives) {
@@ -15,8 +12,9 @@ module.exports = class RPC {
async setHandlers (socket, peerInfo) { async setHandlers (socket, peerInfo) {
if (this.connections[peerInfo.publicKey]) return this.connections[peerInfo.publicKey] if (this.connections[peerInfo.publicKey]) return this.connections[peerInfo.publicKey]
const rpc = new ProtomuxRPC(socket) const rpc = new ProtomuxRPC(socket)
this.connections[peerInfo.publicKey] = rpc
rpc.on('error', err => console.error('rpc error', err)) rpc.on('error', err => console.error('rpc error', err))
rpc.on('close', () => delete this.connections[peerInfo.publicKey]) rpc.on('close', () => delete this.connections[peerInfo.publicKey])
// XXX: handshaking can be used for access and permission management // XXX: handshaking can be used for access and permission management
@@ -24,131 +22,22 @@ module.exports = class RPC {
// which can in turn be stored in a .git-daemon-export-ok file // which can in turn be stored in a .git-daemon-export-ok file
/* -- PULL HANDLERS -- */ /* -- PULL HANDLERS -- */
rpc.respond('get-repos', async req => await this.getReposHandler(socket.remotePublicKey, req)) rpc.respond('get-repos', async req => await git.getReposHandler.bind(this)(socket.remotePublicKey, req))
rpc.respond('get-refs', async req => await this.getRefsHandler(socket.remotePublicKey, req)) rpc.respond('get-refs', async req => await git.getRefsHandler.bind(this)(socket.remotePublicKey, req))
if (!process.env.GIT_PEAR_AUTH) return
if (process.env.GIT_PEAR_AUTH) {
/* -- PUSH HANDLERS -- */ /* -- PUSH HANDLERS -- */
rpc.respond('push', async req => await this.pushHandler(socket.remotePublicKey, req)) rpc.respond('push', async req => await git.pushHandler.bind(this)(socket.remotePublicKey, req))
rpc.respond('f-push', async req => await this.forcePushHandler(socket.remotePublicKey, req)) rpc.respond('f-push', async req => await git.forcePushHandler.bind(this)(socket.remotePublicKey, req))
rpc.respond('d-branch', async req => await this.deleteBranchHandler(socket.remotePublicKey, req)) rpc.respond('d-branch', async req => await git.deleteBranchHandler.bind(this)(socket.remotePublicKey, req))
}
this.connections[peerInfo.publicKey] = rpc /* -- REPO ADMINISTRATION HANDLERS -- */
}
async getReposHandler (publicKey, req) { /* -- ACL HANDLERS -- */
const { branch, url, userId } = await this.parseReq(publicKey, req) rpc.respond('get-acl', async req => await acl.getACLHandler.bind(this)(socket.remotePublicKey, req))
rpc.respond('add-acl', async req => await acl.addACLHandler.bind(this)(socket.remotePublicKey, req))
const res = {} rpc.respond('del-acl', async req => await acl.delACLHandler.bind(this)(socket.remotePublicKey, req))
for (const repoName in this.repositories) {
// TODO: add only public repos and those which are shared with the peer
// Alternatively return only requested repo
const isPublic = (acl.getACL(repoName).visibility === 'public')
if (isPublic || acl.getViewers(repoName).includes(userId)) {
res[repoName] = this.drives[repoName].key.toString('hex')
}
}
return Buffer.from(JSON.stringify(res))
}
async getRefsHandler (publicKey, req) {
const { repoName, branch, url, userId } = await this.parseReq(publicKey, req)
const res = this.repositories[repoName]
const isPublic = (acl.getACL(repoName).visibility === 'public')
if (isPublic || acl.getViewers(repoName).includes(userId)) {
return Buffer.from(JSON.stringify(res))
} else {
throw new Error('You are not allowed to access this repo')
}
}
async pushHandler (publicKey, req) {
const { url, repoName, branch, userId } = await this.parseReq(publicKey, req)
const isContributor = acl.getContributors(repoName).includes(userId)
if (!isContributor) throw new Error('You are not allowed to push to this repo')
const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch)
const isAdmin = acl.getAdmins(repoName).includes(userId)
if (isProtectedBranch && !isAdmin) throw new Error('You are not allowed to push to this branch')
return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['fetch', url, `${branch}:${branch}`], { env })
let errBuffer = Buffer.from('')
child.stderr.on('data', data => {
errBuffer = Buffer.concat([errBuffer, data])
})
child.on('close', code => {
return code === 0 ? resolve(errBuffer) : reject(errBuffer)
})
})
}
async forcePushHandler (publicKey, req) {
const { url, repoName, branch, userId } = await this.parseReq(publicKey, req)
const isContributor = acl.getContributors(repoName).includes(userId)
if (!isContributor) throw new Error('You are not allowed to push to this repo')
const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch)
const isAdmin = acl.getAdmins(repoName).includes(userId)
if (isProtectedBranch && !isAdmin) throw new Error('You are not allowed to push to this branch')
return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env })
let errBuffer = Buffer.from('')
child.stderr.on('data', data => {
errBuffer = Buffer.concat([errBuffer, data])
})
child.on('close', code => {
return code === 0 ? resolve(errBuffer) : reject(errBuffer)
})
})
}
async deleteBranchHandler (publicKey, req) {
const { url, repoName, branch, userId } = await this.parseReq(publicKey, req)
const isContributor = acl.getContributors(repoName).includes(userId)
if (!isContributor) throw new Error('You are not allowed to push to this repo')
const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch)
const isAdmin = acl.getAdmins(repoName).includes(userId)
if (isProtectedBranch && !isAdmin) throw new Error('You are not allowed to push to this branch')
return await new Promise((resolve, reject) => {
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['branch', '-D', branch], { env })
let errBuffer = Buffer.from('')
child.stderr.on('data', data => {
errBuffer = Buffer.concat([errBuffer, data])
})
child.on('close', code => {
return code === 0 ? resolve(errBuffer) : reject(errBuffer)
})
})
}
async parseReq(publicKey, req) {
if (!req) throw new Error('Request is empty')
const request = JSON.parse(req.toString())
const parsed = {
repoName: request.body.url?.split('/')?.pop(),
branch: request.body.data?.split('#')[0],
url: request.body.url,
userId: await this.authenticate(publicKey, request),
}
return parsed
} }
async authenticate (publicKey, request) { async authenticate (publicKey, request) {

36
src/utils.js Normal file
View File

@@ -0,0 +1,36 @@
const fs = require('fs')
const path = require('path')
function printACL (repoACL) {
console.log('Repo Visibility:', '\t', repoACL.visibility)
console.log('Protected Branch(s):', '\t', repoACL.protectedBranches.join(', '))
console.log('User:', '\t', 'Role:')
for (const user in repoACL.ACL) {
console.log(user, '\t', repoACL.ACL[user])
}
}
function printACLForUser (repoACL, u) {
console.log('Repo Visibility:', '\t', repoACL.visibility)
console.log('Protected Branch(s):', '\t', repoACL.protectedBranches.join(', '))
console.log('User:', u, '\t', repoACL.ACL[u])
}
function checkIfGitRepo (p) {
if (!fs.existsSync(path.join(p, '.git'))) {
console.error(` ${p} is not a git repo`)
process.exit(1)
}
}
function logBranches (repoACL) {
console.log('Repo Visibility:', '\t', repoACL.visibility)
console.log('Protected Branch(s):', '\t', repoACL.protectedBranches.join(', '))
}
module.exports = {
printACL,
printACLForUser,
checkIfGitRepo,
logBranches
}

View File

@@ -7,8 +7,8 @@ test('acl', async t => {
t.test('setACL', async t => { t.test('setACL', async t => {
const aclObj = acl.setACL(repoName) const aclObj = acl.setACL(repoName)
t.is(aclObj.visibility, 'public') t.is(aclObj.visibility, 'public')
t.is(aclObj.protectedBranches.length, 1) t.is(aclObj.protectedBranches.length, 2)
t.is(aclObj.protectedBranches[0], 'master') t.ok(aclObj.protectedBranches.includes('master') && aclObj.protectedBranches.includes('main'))
t.is(Object.keys(aclObj.ACL).length, 0) t.is(Object.keys(aclObj.ACL).length, 0)
}) })
@@ -34,12 +34,12 @@ test('acl', async t => {
test('addProtectedBranch', async t => { test('addProtectedBranch', async t => {
acl.addProtectedBranch(repoName, 'branch1') acl.addProtectedBranch(repoName, 'branch1')
t.is(acl.getACL(repoName).protectedBranches.length, 2) t.is(acl.getACL(repoName).protectedBranches.length, 3)
}) })
test('removeProtectedBranch', async t => { test('removeProtectedBranch', async t => {
acl.removeProtectedBranch(repoName, 'branch1') acl.removeProtectedBranch(repoName, 'branch1')
t.is(acl.getACL(repoName).protectedBranches.length, 1) t.is(acl.getACL(repoName).protectedBranches.length, 2)
}) })
test('getAdmins', async t => { test('getAdmins', async t => {

View File

@@ -51,13 +51,13 @@ test('git - uploadPack (w have)', { skip: true }, async t => {
}) })
test('git - createBareRepo', async t => { test('git - createBareRepo', async t => {
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'code'))) t.absent(fs.existsSync(path.join(home.APP_HOME, 'test-git')))
home.createAppFolder('test-git') home.createAppFolder('test-git')
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'code', 'HEAD'))) t.absent(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'HEAD')))
await git.createBareRepo('test-git') await git.createBareRepo('test-git')
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'code', 'HEAD'))) t.ok(fs.existsSync(path.join(home.APP_HOME, 'test-git', 'HEAD')))
t.teardown(() => { t.teardown(() => {
fs.rmSync(path.join(home.APP_HOME, 'test-git'), { recursive: true }) fs.rmSync(path.join(home.APP_HOME, 'test-git'), { recursive: true })

View File

@@ -11,7 +11,7 @@ test('getAppHome', t => {
test('createAppFolder, share, is shared, unshare, isInitialized, list, getCodePath', t => { test('createAppFolder, share, is shared, unshare, isInitialized, list, getCodePath', t => {
home.createAppFolder('test_code') home.createAppFolder('test_code')
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code', 'code'))) t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code')))
t.absent(home.isShared('test_code')) t.absent(home.isShared('test_code'))
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok'))) t.absent(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok')))
@@ -32,7 +32,7 @@ test('createAppFolder, share, is shared, unshare, isInitialized, list, getCodePa
t.alike(new Set(home.list()), new Set(['foo', 'bar', 'zar'])) t.alike(new Set(home.list()), new Set(['foo', 'bar', 'zar']))
t.alike(new Set(home.list(true)), new Set(['foo', 'bar'])) t.alike(new Set(home.list(true)), new Set(['foo', 'bar']))
t.alike(path.resolve(home.getCodePath('test_code')), path.resolve(path.join(home.APP_HOME, 'test_code', 'code'))) t.alike(path.resolve(home.getCodePath('test_code')), path.resolve(path.join(home.APP_HOME, 'test_code')))
t.teardown(() => { t.teardown(() => {
fs.rmSync(path.join(home.APP_HOME, 'test_code'), { recursive: true }) fs.rmSync(path.join(home.APP_HOME, 'test_code'), { recursive: true })

Binary file not shown.