diff --git a/Readme.md b/Readme.md index a88e2b2..61cb667 100644 --- a/Readme.md +++ b/Readme.md @@ -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 key` - print out public key. Share it with your peers so that they can do `git pull pear:/` -* `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/). 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 (default: "public")]` - share repository, if branch is not specified, default branch will be shared -* `git pear unshare ` - stop sharing repository +* `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/). 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 [-b, --branch [branch name (default: "_current_")] [-v, --visibility (default: "public")]` - share current repository, if branch is not specified, default branch will be shared +* `git pear unshare` - stop sharing current repository * `git pear list [-s, --shared]` - list all or (only shared) repositories * `git pear list ` - 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: * [noise](https://github.com/libp2p/specs/blob/master/noise/README.md); @@ -62,22 +62,26 @@ or 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). -* `git pear acl [command] ` - ACL managegement -* `git pear acl list [userId] ` - list repository visitbility and user's role (or roles of all users if userId is not provided) -* `git pear acl add ` - add user as a "role" to repository, available roles are `viewer`, `contributor`, `admin`. Roles exaplained: - * `viewer` - can read all branches; - * `contributor` - can edit all branches except protected (default master) - * `admin` - can edit protected branches -* `git pear acl remove ` - revoke use access to repository +* `git pear acl -u [command] -p ` - ACL managegement of for users access in local or remote repo (requires `owner` permission for remote repositories) + * `git pear acl -u list [userId] -p ` - list repository visitbility and user's role (or roles of all users if userId is not provided) + * `git pear acl add -u -p ` - add user as a "role" to repository available roles are `viewer`, `contributor`, `admin`, `owner`. Roles exaplained: + * `viewer` - can read all branches; + * `contributor` - can edit all branches except protected (default master) + * `admin` - can edit protected branches + * `owner` - can edit repo ack + * `git pear acl remove -u| -p ` - revoke use access to repository. ### Branch protection rules 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 branch add ` - mark branch as protected (defatul repo path is ".") -* `git pear branch remove ` - unmark branch as protected +* `git pear acl -b [command] -p ` - mark branch as protected; + * `git pear acl -b remove ` - unmark branch as protected; # Examples of usage @@ -128,7 +132,7 @@ git pull ## 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 @@ -138,12 +142,12 @@ Supported authentication methods are `native` and `nip98`. The `nip98` authentic 2. Go to repository * `cd repo` 3. Initialize git pear repository -* `git pear init .` +* `git pear init` 4. Share repository wit hviben visibility () - (default is `public`) -* `git pear share . public` -5. Add Daviv as a `contirbutor`. -6. List David's npub as a contributor -* `git pear acl add :contributor` +* `git pear share public` +5. Add Dave as a `contirbutor`. +6. List Dave's npub as a contributor +* `git pear acl add :contributor` 7. Retreive repo url and share it with Dave * `git pear list -s` @@ -176,4 +180,4 @@ git clone https://github.com/dzdidi/gitpear.git cd gitpear docker build -t gitpear . docker run -it -p 80:80 -e REPO_URL=https://github.com/dzdidi/repo.git gitpear -``` \ No newline at end of file +``` diff --git a/src/acl.js b/src/acl.js index 1e8071c..8a02c26 100644 --- a/src/acl.js +++ b/src/acl.js @@ -2,18 +2,21 @@ const home = require('./home') const fs = require('fs') const ROLES = { + owner: { + description: 'Read and write to all branches, and ACL management' + }, admin: { - description: 'Read and write to all branches', + description: 'Read and write to all branches' }, contributor: { - description: 'Read and write to all branches except protected ones', + description: 'Read and write to all branches except protected ones' }, viewer: { - description: 'Read all branches', - }, + description: 'Read all branches' + } } const DEFAULT_ACL = { - visibility: 'public', // public|private + visibility: 'public', // public|private protectedBranches: ['master', 'main'], ACL: {} } @@ -23,9 +26,16 @@ function getUserRole (repoName, 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) { 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) { @@ -113,8 +123,9 @@ module.exports = { getACL, addProtectedBranch, removeProtectedBranch, + getOwners, getAdmins, getContributors, getViewers, - revokeAccessFromUser, + revokeAccessFromUser } diff --git a/src/auth/index.js b/src/auth/index.js index 54037db..0c81c33 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -1,13 +1,13 @@ -function getId(data) { - if (!process.env.GIT_PEAR_AUTH) return payload +function getId (data) { + if (!process.env.GIT_PEAR_AUTH) return data if (process.env.GIT_PEAR_AUTH === 'nip98') { const nip98 = require('./nip98') return nip98.getId(data) } } -async function getToken(payload) { - if (!process.env.GIT_PEAR_AUTH) return userId +async function getToken (payload) { + if (!process.env.GIT_PEAR_AUTH) return payload if (process.env.GIT_PEAR_AUTH === 'nip98') { const nip98 = require('./nip98') return nip98.getToken(payload) diff --git a/src/auth/nip98.js b/src/auth/nip98.js index 42e2751..f693dc0 100644 --- a/src/auth/nip98.js +++ b/src/auth/nip98.js @@ -1,6 +1,7 @@ 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) return nip98.getToken( url, @@ -12,11 +13,11 @@ async function getToken({ url, method, data }) { } // FIXME -async function getId({ payload, url, method, data }) { +async function getId ({ payload, url, method, data }) { const event = JSON.parse(Buffer.from(payload, 'base64').toString()) const isValid = await nip98.validateEvent(event, url, method, data) if (!isValid) throw new Error('Invalid event') - return { + return { ...event, userId: nip19.npubEncode(event.pubkey) } diff --git a/src/cli-helpers.js b/src/cli-helpers.js new file mode 100644 index 0000000..1b5af91 --- /dev/null +++ b/src/cli-helpers.js @@ -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 + +} diff --git a/src/cli.js b/src/cli.js index ea963e4..2157036 100755 --- a/src/cli.js +++ b/src/cli.js @@ -6,12 +6,15 @@ 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 } = require('./rpc-requests') + +const { checkIfGitRepo } = require('./utils') +const { remoteACL, share, remoteBranchProtectionRules, localACL, localBranchProtectionRules } = require('./cli-helpers') + const pkg = require('../package.json') program .name('gitpear') @@ -21,25 +24,23 @@ program 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) - if (!fs.existsSync(path.join(fullPath, '.git'))) { - console.error('Not a git repo') - process.exit(1) - } + .action(async (options) => { + const fullPath = path.resolve('.') + checkIfGitRepo(fullPath) const name = fullPath.split(path.sep).pop() if ((home.isInitialized(name))) { console.error(`${name} is already initialized`) - await git.addRemote(name) - console.log(`Added git remote for "${name}" as "pear"`) + try { + await git.addRemote(name) + console.log(`Added git remote for "${name}" as "pear"`) + } catch (e) { } process.exit(1) } try { - home.createAppFolder(name) + home.createAppFolder(name) console.log(`Added project "${name}" to gitpear`) } catch (e) { } try { @@ -56,12 +57,7 @@ program branchToShare = options.share } - if (options.share) { - 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`) - } + if (options.share) await share(name, branchToShare) }) program @@ -69,13 +65,9 @@ program .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) - if (!fs.existsSync(path.join(fullPath, '.git'))) { - console.error('Not a git repo') - process.exit(1) - } + const fullPath = path.resolve('.') + checkIfGitRepo(fullPath) const name = fullPath.split(path.sep).pop() if (!home.isInitialized(name)) { @@ -85,137 +77,49 @@ program const currentBranch = await git.getCurrentBranch() const branchToShare = options.branch || currentBranch - try { home.shareAppFolder(name) } catch (e) { } - 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 + await share(name, branchToShare, options) }) -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 .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('[u]', 'user to add/remove/list').default('')) - .addArgument(new commander.Argument('[p]', 'path to the repo').default('.')) - .action(async (a, u, p, options) => { - - // 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) + .addArgument(new commander.Argument('[n]', 'user or branch to add/remove/list').default('')) + .action(async (a, n, options) => { + if (options.user && options.branch) { + throw new Error('Cannot perform both user and branch action at the same time') } - 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) { - 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 (!options.user && !options.branch) { + throw new Error('Either user or branch option is required') } - if (a === 'list') { - console.log('Repo Visibility:', '\t', repoACL.visibility) - console.log('User:', u, '\t', repoACL.ACL[u]) - return - } - - if (a === 'add') { - if (!u) { - console.error('User not provided') - process.exit(1) + if (options.user) { + if (options.path.startsWith('pear://')) { + if (n === '.') n = '' + await remoteACL(a, n, options.path, options) + } else { + localACL(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) + } else if (options.branch) { + if (options.path.startsWith('pear://')) { + await remoteBranchProtectionRules(a, n, options.path, options) + } else { + localBranchProtectionRules(a, n, options.path, options) } - - 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 .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) - if (!fs.existsSync(path.join(fullPath, '.git'))) { - console.error('Not a git repo') - process.exit(1) - } + .option('-p, --path [path]', 'path to the repo', '.') + .action((options) => { + const fullPath = path.resolve(options.path) + checkIfGitRepo(fullPath) const name = fullPath.split(path.sep).pop() if ((home.isInitialized(name))) { @@ -235,7 +139,7 @@ program .addArgument(new commander.Argument('[u]', 'url to remote pear').default('')) .option('-s, --shared', 'list only shared repos') .action((u, options) => { - if (u) return require('./list-remote')(u) + if (u) return listRemote(u) const k = home.readPk() const s = options.shared @@ -276,7 +180,7 @@ program opts.stdio = 'inherit' } else { opts.detached = true - opts.stdio = [ 'ignore', home.getOutStream(), home.getErrStream() ] + opts.stdio = ['ignore', home.getOutStream(), home.getErrStream()] } const daemon = spawn('git-peard', opts) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 6aac563..848fc9a 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -1,6 +1,5 @@ #!/usr/bin/env node -const { spawn } = require('child_process') const ProtomuxRPC = require('protomux-rpc') const RAM = require('random-access-memory') @@ -14,8 +13,6 @@ const home = require('./home') const auth = require('./auth') const acl = require('./acl') -const fs = require('fs') - const url = process.argv[3] const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/) @@ -29,7 +26,7 @@ const repoName = matches[2] const store = new Corestore(RAM) 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() } const swarm = new Hyperswarm(swarmOpts) @@ -76,13 +73,13 @@ swarm.on('connection', async (socket) => { await drive.core.update({ wait: true }) - payload = { body: { url, method: 'get-refs', data: repoName }} + payload = { body: { url, method: 'get-refs', data: repoName } } if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') { payload.header = await auth.getToken(payload.body) } const refsRes = await rpc.request('get-refs', Buffer.from(JSON.stringify(payload))) - let commit + let commit try { commit = await git.getCommit() } catch (e) { } @@ -99,7 +96,7 @@ async function talkToGit (refs, drive, repoName, rpc, commit) { process.stdout.write('push\n') process.stdout.write('fetch\n\n') } else if (chunk && chunk.search(/^push/) !== -1) { - const [_command, path] = chunk.split(' ') + const path = chunk.split(' ')[1] let [src, dst] = path.split(':') const isDelete = !src @@ -107,13 +104,6 @@ async function talkToGit (refs, drive, repoName, rpc, commit) { 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 if (isDelete) { method = 'd-branch' @@ -128,16 +118,25 @@ async function talkToGit (refs, drive, repoName, rpc, commit) { 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() - let payload = { body: { - url: `pear://${publicKey}/${repoName}`, - data: `${dst}#${commit}`, - method - } } + const payload = { + body: { + url: `pear://${publicKey}/${repoName}`, + data: `${dst}#${commit}`, + method + } + } if (process.env.GIT_PEAR_AUTH && process.env.GIT_PEAR_AUTH !== 'native') { 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.exit(0) diff --git a/src/git.js b/src/git.js index 789b373..ada5d08 100644 --- a/src/git.js +++ b/src/git.js @@ -64,9 +64,9 @@ async function createBareRepo (name) { return await doGit(init) } -async function addRemote (name) { +async function addRemote (name, opts = { quiet: false }) { 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) { @@ -76,8 +76,8 @@ async function push (branch = 'master', force = false) { return await doGit(push) } -async function doGit (child) { - child.stderr.pipe(process.stderr) +async function doGit (child, opts = { quiet: false }) { + if (!opts.quiet) child.stderr.pipe(process.stderr) return new Promise((resolve, reject) => { child.on('close', (code) => { if (code) { @@ -229,5 +229,5 @@ module.exports = { addRemote, push, getCommit, - getCurrentBranch, + getCurrentBranch } diff --git a/src/rpc-handlers/acl.js b/src/rpc-handlers/acl.js new file mode 100644 index 0000000..1df3019 --- /dev/null +++ b/src/rpc-handlers/acl.js @@ -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 +} diff --git a/src/rpc-handlers/git.js b/src/rpc-handlers/git.js new file mode 100644 index 0000000..5e83ce2 --- /dev/null +++ b/src/rpc-handlers/git.js @@ -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 +} diff --git a/src/rpc-handlers/index.js b/src/rpc-handlers/index.js new file mode 100644 index 0000000..7cbc02a --- /dev/null +++ b/src/rpc-handlers/index.js @@ -0,0 +1,7 @@ +const git = require('./git') +const acl = require('./acl') + +module.exports = { + git, + acl +} diff --git a/src/rpc-requests/acl-remote.js b/src/rpc-requests/acl-remote.js new file mode 100644 index 0000000..22ec067 --- /dev/null +++ b/src/rpc-requests/acl-remote.js @@ -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) +} diff --git a/src/rpc-requests/index.js b/src/rpc-requests/index.js new file mode 100644 index 0000000..e13e2f1 --- /dev/null +++ b/src/rpc-requests/index.js @@ -0,0 +1,7 @@ +const listRemote = require('./list-remote') +const aclRemote = require('./acl-remote') + +module.exports = { + listRemote, + aclRemote +} diff --git a/src/list-remote.js b/src/rpc-requests/list-remote.js similarity index 86% rename from src/list-remote.js rename to src/rpc-requests/list-remote.js index 576fadb..1ab0d37 100644 --- a/src/list-remote.js +++ b/src/rpc-requests/list-remote.js @@ -3,8 +3,8 @@ const ProtomuxRPC = require('protomux-rpc') const Hyperswarm = require('hyperswarm') const crypto = require('hypercore-crypto') -const home = require('./home') -const auth = require('./auth') +const home = require('../home') +const auth = require('../auth') module.exports = async function listRemote (url) { const matches = url.match(/pear:\/\/([a-f0-9]{64})/) @@ -18,7 +18,7 @@ module.exports = async function listRemote (url) { console.log('Connecting to:', targetKey) 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() } const swarm = new Hyperswarm(swarmOpts) @@ -28,14 +28,15 @@ module.exports = async function listRemote (url) { swarm.on('connection', async (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') { 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 repositories = JSON.parse(reposRes.toString()) diff --git a/src/rpc.js b/src/rpc.js index 90087c3..c400d45 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -1,9 +1,6 @@ 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 acl = require('./acl') +const { git, acl } = require('./rpc-handlers') module.exports = class RPC { constructor (announcedRefs, repositories, drives) { @@ -15,8 +12,9 @@ module.exports = class RPC { async setHandlers (socket, peerInfo) { if (this.connections[peerInfo.publicKey]) return this.connections[peerInfo.publicKey] - const rpc = new ProtomuxRPC(socket) + this.connections[peerInfo.publicKey] = rpc + rpc.on('error', err => console.error('rpc error', err)) rpc.on('close', () => delete this.connections[peerInfo.publicKey]) // 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 /* -- PULL HANDLERS -- */ - rpc.respond('get-repos', async req => await this.getReposHandler(socket.remotePublicKey, req)) - rpc.respond('get-refs', async req => await this.getRefsHandler(socket.remotePublicKey, req)) + rpc.respond('get-repos', async req => await git.getReposHandler.bind(this)(socket.remotePublicKey, req)) + rpc.respond('get-refs', async req => await git.getRefsHandler.bind(this)(socket.remotePublicKey, req)) - if (process.env.GIT_PEAR_AUTH) { - /* -- PUSH HANDLERS -- */ - rpc.respond('push', async req => await this.pushHandler(socket.remotePublicKey, req)) - rpc.respond('f-push', async req => await this.forcePushHandler(socket.remotePublicKey, req)) - rpc.respond('d-branch', async req => await this.deleteBranchHandler(socket.remotePublicKey, req)) - } + if (!process.env.GIT_PEAR_AUTH) return - this.connections[peerInfo.publicKey] = rpc - } + /* -- PUSH HANDLERS -- */ + rpc.respond('push', async req => await git.pushHandler.bind(this)(socket.remotePublicKey, req)) + rpc.respond('f-push', async req => await git.forcePushHandler.bind(this)(socket.remotePublicKey, req)) + rpc.respond('d-branch', async req => await git.deleteBranchHandler.bind(this)(socket.remotePublicKey, req)) - async getReposHandler (publicKey, req) { - const { branch, url, userId } = await this.parseReq(publicKey, req) + /* -- REPO ADMINISTRATION HANDLERS -- */ - const res = {} - 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 + /* -- ACL HANDLERS -- */ + 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)) + rpc.respond('del-acl', async req => await acl.delACLHandler.bind(this)(socket.remotePublicKey, req)) } async authenticate (publicKey, request) { diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..9037a57 --- /dev/null +++ b/src/utils.js @@ -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 +} diff --git a/test/acl.test.js b/test/acl.test.js index a52ef81..8d4c0d7 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -7,8 +7,8 @@ test('acl', async t => { t.test('setACL', async t => { const aclObj = acl.setACL(repoName) t.is(aclObj.visibility, 'public') - t.is(aclObj.protectedBranches.length, 1) - t.is(aclObj.protectedBranches[0], 'master') + t.is(aclObj.protectedBranches.length, 2) + t.ok(aclObj.protectedBranches.includes('master') && aclObj.protectedBranches.includes('main')) t.is(Object.keys(aclObj.ACL).length, 0) }) @@ -34,12 +34,12 @@ test('acl', async t => { test('addProtectedBranch', async t => { acl.addProtectedBranch(repoName, 'branch1') - t.is(acl.getACL(repoName).protectedBranches.length, 2) + t.is(acl.getACL(repoName).protectedBranches.length, 3) }) test('removeProtectedBranch', async t => { acl.removeProtectedBranch(repoName, 'branch1') - t.is(acl.getACL(repoName).protectedBranches.length, 1) + t.is(acl.getACL(repoName).protectedBranches.length, 2) }) test('getAdmins', async t => { diff --git a/test/git.test.js b/test/git.test.js index efed0bf..b06ab9d 100644 --- a/test/git.test.js +++ b/test/git.test.js @@ -51,13 +51,13 @@ test('git - uploadPack (w have)', { skip: true }, 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') - 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') - 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(() => { fs.rmSync(path.join(home.APP_HOME, 'test-git'), { recursive: true }) diff --git a/test/home.test.js b/test/home.test.js index 2381886..9980b06 100644 --- a/test/home.test.js +++ b/test/home.test.js @@ -11,7 +11,7 @@ test('getAppHome', t => { test('createAppFolder, share, is shared, unshare, isInitialized, list, getCodePath', t => { 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(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(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(() => { fs.rmSync(path.join(home.APP_HOME, 'test_code'), { recursive: true }) diff --git a/test/rpc.test.js b/test/rpc.test.js index fc90b99..7061b18 100644 --- a/test/rpc.test.js +++ b/test/rpc.test.js @@ -56,7 +56,7 @@ test('e2e', async t => { await drive.core.update({ wait: true }) - payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-refs', data: repoName }})) + payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-refs', data: repoName } })) const refsRes = await rpc.request('get-refs', payload) t.ok(refsRes) diff --git a/test_home.tar.gz b/test_home.tar.gz index cdbf5e4..bfe5a2c 100644 Binary files a/test_home.tar.gz and b/test_home.tar.gz differ