mirror of
https://github.com/aljazceru/gitpear.git
synced 2025-12-17 06:04:25 +01:00
77
Readme.md
77
Readme.md
@@ -32,27 +32,48 @@ npm nix
|
|||||||
|
|
||||||
See `./result` - for binaries build by nix. To make the available add to path by running `PATH="${PATH:+${PATH}:}${PWD}/result/bin/"`
|
See `./result` - for binaries build by nix. To make the available add to path by running `PATH="${PATH:+${PATH}:}${PWD}/result/bin/"`
|
||||||
|
|
||||||
##
|
## Running
|
||||||
|
|
||||||
All data will be persisted in application directory (default `~/.gitpear`). To change it. Provide environment variable `GIT_PEAR`
|
All data will be persisted in application directory (default `~/.gitpear`). To change it. Provide environment variable `GIT_PEAR`
|
||||||
|
|
||||||
* `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 [-s, --share] <path>` - 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` or call `gitpear share <path>` later
|
* `git pear init [-s, --share] <path>` - 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` or call `gitpear share <path>` later
|
||||||
|
|
||||||
* `git pear share <path>` - makes repository sharable
|
* `git pear share <path>` - makes repository sharable
|
||||||
|
|
||||||
* `git pear unshare <path>` - stop sharing repository
|
* `git pear unshare <path>` - stop sharing repository
|
||||||
|
|
||||||
* `git pear list [-s, --shared]` - list all or (only shared) repositories
|
* `git pear list [-s, --shared]` - list all or (only shared) repositories
|
||||||
|
|
||||||
## Usage example
|
### ACL (for authenticated access to enable support of PUSH)
|
||||||
|
|
||||||
Please not this is only remote helper and its intention is only to enable direct `clone|fetch|pull` of repository hosted on private computer.
|
Support of `push` capabilities only enabled for authenticated users. Currently supported authentication is based on [NIP98](https://github.com/nostr-protocol/nips/blob/master/98.md).
|
||||||
|
To start daemon with authenticated support provide environment varibales `GIT_PEAR_AUTH` with value `nip98` and `GIT_PEAR_AUTH_NSEC` with value of your [NIP19 nsec](https://github.com/nostr-protocol/nips/blob/master/19.md).
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
GIT_PEAR_AUTH=nip98 GIT_PEAR_AUTH_NSEC=nsec.... git pear daemon -s
|
||||||
|
```
|
||||||
|
|
||||||
Collaboration is possible however with the following flow between Alice and Bob in a pure peer-to-peer manner of git.
|
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 list [userId] <path>` - 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:
|
||||||
|
* `viewer` - can read all branches;
|
||||||
|
* `contributor` - can edit all branches except protected (default master)
|
||||||
|
* `admin` - can edit protected branches
|
||||||
|
* `git pear acl remove <userId> <path>` - 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 <branch name> <repo path>` - mark branch as protected (defatul repo path is ".")
|
||||||
|
* `git pear branch remove <branch name> <repo path>` - unmark branch as protected
|
||||||
|
|
||||||
|
# Examples of usage
|
||||||
|
|
||||||
|
## Un authenticated usage example (no push)
|
||||||
|
|
||||||
|
Collaboration is possible with the following flow between Alice and Bob in a peer-to-peer manner.
|
||||||
|
|
||||||
1. Both Alice and Bob have gitpear installed and Alice wants Bob to help her with repo Repo
|
1. Both Alice and Bob have gitpear installed and Alice wants Bob to help her with repo Repo
|
||||||
2. Alice steps are:
|
2. Alice steps are:
|
||||||
@@ -94,3 +115,41 @@ git checkout master
|
|||||||
git fetch origin
|
git fetch origin
|
||||||
git pull
|
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.
|
||||||
|
|
||||||
|
### Carol steps (as a server of code)
|
||||||
|
1. Start daemon
|
||||||
|
* `GIT_PEAR_AUTH_NSEC=<Carol's nsec> GIT_PEAR_AUTH='nip98' git pear daemon -s`
|
||||||
|
2. Go to repository
|
||||||
|
* `cd repo`
|
||||||
|
3. Initialize git pear repository
|
||||||
|
* `git pear init .`
|
||||||
|
4. Share repository wit hviben visibility () - (default is `public`)
|
||||||
|
* `git pear share . <private|public>`
|
||||||
|
5. Add Daviv as a `contirbutor`.
|
||||||
|
6. List David's npub as a contributor
|
||||||
|
* `git pear acl add <David npub>:contributor`
|
||||||
|
7. Retreive repo url and share it with Dave
|
||||||
|
* `git pear list -s`
|
||||||
|
|
||||||
|
### Dave side (a collaborator for code)
|
||||||
|
1. Start daemon. This will be needed later for push. Not that no auth or sec are provided which means that push to this place will not be supportedd.
|
||||||
|
* `git pear daemon -s`
|
||||||
|
2. Clone repository. Authorization data and type are necesary for server (Carol) to grant corresponding access persmissions
|
||||||
|
* `GIT_PEAR_AUTH_NSEC=<David's nsec> GIT_PEAR_AUTH='nip98' git clone pear://<Carol's url>/<repo name>`
|
||||||
|
3. Do the necessary change in separate branch
|
||||||
|
* `git checkout -b feat/david`
|
||||||
|
* do change
|
||||||
|
* `git add .`
|
||||||
|
* `git commit -s -m 'made by David'`
|
||||||
|
4. Push branch to origin
|
||||||
|
* `GIT_PEAR_AUTH_NSEC=<David's nsec> GIT_PEAR_AUTH='nip98' git push origin feat/david`
|
||||||
|
|
||||||
|
### Carol steps
|
||||||
|
1. For Carol the changes will arrive as branch `feat/david` into her `pear`
|
||||||
|
* `git fetch pear`
|
||||||
|
2. From there she can do
|
||||||
|
* `git diff pear/feat/david` or `git pull pear feat/david` ... merge to master and push to `pear`
|
||||||
|
|||||||
210
npm-shrinkwrap.json
generated
210
npm-shrinkwrap.json
generated
@@ -12,10 +12,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
"corestore": "^6.10.1",
|
"corestore": "^6.15.13",
|
||||||
"hyperdrive": "^11.5.3",
|
"hyperdrive": "^11.6.3",
|
||||||
"hyperswarm": "^4.5.1",
|
"hyperswarm": "^4.7.13",
|
||||||
"protomux-rpc": "^1.4.1",
|
"nostr-tools": "^2.1.5",
|
||||||
|
"protomux-rpc": "^1.5.1",
|
||||||
"random-access-memory": "^6.2.0"
|
"random-access-memory": "^6.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -239,6 +240,47 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/ciphers": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/curves": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/curves/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -274,6 +316,53 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@scure/base": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "~1.1.0",
|
||||||
|
"@noble/hashes": "~1.3.1",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip32/node_modules/@noble/curves": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip39": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "~1.3.0",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/istanbul-lib-coverage": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||||
@@ -3008,6 +3097,36 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nostr-tools": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.1.5.tgz",
|
||||||
|
"integrity": "sha512-Gug/j54YGQ0ewB09dZW3mS9qfXWFlcOQMlyb1MmqQsuNO/95mfNOQSBi+jZ61O++Y+jG99SzAUPFLopUsKf0MA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/ciphers": "0.2.0",
|
||||||
|
"@noble/curves": "1.2.0",
|
||||||
|
"@noble/hashes": "1.3.1",
|
||||||
|
"@scure/base": "1.1.1",
|
||||||
|
"@scure/bip32": "1.3.1",
|
||||||
|
"@scure/bip39": "1.2.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"nostr-wasm": "v0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nostr-wasm": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -4390,7 +4509,7 @@
|
|||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -4838,6 +4957,31 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@noble/ciphers": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw=="
|
||||||
|
},
|
||||||
|
"@noble/curves": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"requires": {
|
||||||
|
"@noble/hashes": "1.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@noble/hashes": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="
|
||||||
|
},
|
||||||
"@nodelib/fs.scandir": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -4864,6 +5008,40 @@
|
|||||||
"fastq": "^1.6.0"
|
"fastq": "^1.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@scure/base": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
|
||||||
|
},
|
||||||
|
"@scure/bip32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
|
||||||
|
"requires": {
|
||||||
|
"@noble/curves": "~1.1.0",
|
||||||
|
"@noble/hashes": "~1.3.1",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
|
||||||
|
"requires": {
|
||||||
|
"@noble/hashes": "1.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@scure/bip39": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
|
||||||
|
"requires": {
|
||||||
|
"@noble/hashes": "~1.3.0",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/istanbul-lib-coverage": {
|
"@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||||
@@ -6982,6 +7160,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||||
},
|
},
|
||||||
|
"nostr-tools": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.1.5.tgz",
|
||||||
|
"integrity": "sha512-Gug/j54YGQ0ewB09dZW3mS9qfXWFlcOQMlyb1MmqQsuNO/95mfNOQSBi+jZ61O++Y+jG99SzAUPFLopUsKf0MA==",
|
||||||
|
"requires": {
|
||||||
|
"@noble/ciphers": "0.2.0",
|
||||||
|
"@noble/curves": "1.2.0",
|
||||||
|
"@noble/hashes": "1.3.1",
|
||||||
|
"@scure/base": "1.1.1",
|
||||||
|
"@scure/bip32": "1.3.1",
|
||||||
|
"@scure/bip39": "1.2.1",
|
||||||
|
"nostr-wasm": "v0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nostr-wasm": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -8028,7 +8226,7 @@
|
|||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"udx-native": {
|
"udx-native": {
|
||||||
"version": "1.7.12",
|
"version": "1.7.12",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"corestore": "^6.15.13",
|
"corestore": "^6.15.13",
|
||||||
"hyperdrive": "^11.6.3",
|
"hyperdrive": "^11.6.3",
|
||||||
"hyperswarm": "^4.7.13",
|
"hyperswarm": "^4.7.13",
|
||||||
|
"nostr-tools": "^2.1.5",
|
||||||
"protomux-rpc": "^1.5.1",
|
"protomux-rpc": "^1.5.1",
|
||||||
"random-access-memory": "^6.2.0"
|
"random-access-memory": "^6.2.0"
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/acl.js
Normal file
120
src/acl.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
const home = require('./home')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
const ROLES = {
|
||||||
|
admin: {
|
||||||
|
description: 'Read and write to all branches',
|
||||||
|
},
|
||||||
|
contributor: {
|
||||||
|
description: 'Read and write to all branches except protected ones',
|
||||||
|
},
|
||||||
|
viewer: {
|
||||||
|
description: 'Read all branches',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const DEFAULT_ACL = {
|
||||||
|
visibility: 'public', // public|private
|
||||||
|
protectedBranches: ['master'],
|
||||||
|
ACL: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserRole (repoName, user) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
return acl.ACL[user]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdmins (repoName) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
return Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'admin')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContributors (repoName) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
const contributors = Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'contributor')
|
||||||
|
const admins = getAdmins(repoName)
|
||||||
|
return [...contributors, ...admins].filter((user, i, arr) => arr.indexOf(user) === i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getViewers (repoName) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
const viewers = Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'viewer')
|
||||||
|
const contributors = getContributors(repoName)
|
||||||
|
const admins = getAdmins(repoName)
|
||||||
|
|
||||||
|
return [...viewers, ...contributors, ...admins].filter((user, i, arr) => arr.indexOf(user) === i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function grantAccessToUser (repoName, user, role) {
|
||||||
|
if (!ROLES[role]) throw new Error(`Role ${role} does not exist`)
|
||||||
|
if (!Object.keys(ROLES).includes(role)) throw new Error(`Role ${role} is not allowed`)
|
||||||
|
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
acl.ACL[user] = role
|
||||||
|
setACL(repoName, acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeAccessFromUser (repoName, user) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
delete acl.ACL[user]
|
||||||
|
setACL(repoName, acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addProtectedBranch (repoName, branch) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
if (acl.protectedBranches.includes(branch)) throw new Error(`${branch} is already protected`)
|
||||||
|
acl.protectedBranches.push(branch)
|
||||||
|
setACL(repoName, acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeProtectedBranch (repoName, branch) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
acl.protectedBranches = acl.protectedBranches.filter(b => b !== branch)
|
||||||
|
setACL(repoName, acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRepoPublic (repoName) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
acl.visibility = 'public'
|
||||||
|
setACL(repoName, acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRepoVisibility (repoName) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
return acl.visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRepoPrivate (repoName) {
|
||||||
|
const acl = getACL(repoName)
|
||||||
|
acl.visibility = 'private'
|
||||||
|
setACL(repoName, acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setACL (repoName, acl = DEFAULT_ACL) {
|
||||||
|
acl = { ...DEFAULT_ACL, ...acl }
|
||||||
|
if (['public', 'private'].indexOf(acl.visibility) === -1) throw new Error('Visibility must be public or private')
|
||||||
|
|
||||||
|
const content = JSON.stringify(acl, null, 2)
|
||||||
|
fs.writeFileSync(home.getACLFilePath(repoName), content)
|
||||||
|
|
||||||
|
return acl
|
||||||
|
}
|
||||||
|
|
||||||
|
function getACL (repoName) {
|
||||||
|
return JSON.parse(fs.readFileSync(home.getACLFilePath(repoName), 'utf8') || JSON.stringify(DEFAULT_ACL))
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getUserRole,
|
||||||
|
grantAccessToUser,
|
||||||
|
makeRepoPublic,
|
||||||
|
makeRepoPrivate,
|
||||||
|
getRepoVisibility,
|
||||||
|
setACL,
|
||||||
|
getACL,
|
||||||
|
addProtectedBranch,
|
||||||
|
removeProtectedBranch,
|
||||||
|
getAdmins,
|
||||||
|
getContributors,
|
||||||
|
getViewers,
|
||||||
|
revokeAccessFromUser,
|
||||||
|
}
|
||||||
20
src/auth/index.js
Normal file
20
src/auth/index.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
function getId(data) {
|
||||||
|
if (!process.env.GIT_PEAR_AUTH) return payload
|
||||||
|
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
|
||||||
|
if (process.env.GIT_PEAR_AUTH === 'nip98') {
|
||||||
|
const nip98 = require('./nip98')
|
||||||
|
return nip98.getToken(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getId,
|
||||||
|
getToken
|
||||||
|
}
|
||||||
28
src/auth/nip98.js
Normal file
28
src/auth/nip98.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const { nip98, nip19, finalizeEvent } = require('nostr-tools')
|
||||||
|
|
||||||
|
async function getToken({ url, method, data }) {
|
||||||
|
const { data: sK } = nip19.decode(process.env.GIT_PEAR_AUTH_NSEC)
|
||||||
|
return nip98.getToken(
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
(e) => finalizeEvent(e, sK),
|
||||||
|
false,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME
|
||||||
|
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 {
|
||||||
|
...event,
|
||||||
|
userId: nip19.npubEncode(event.pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getId,
|
||||||
|
getToken
|
||||||
|
}
|
||||||
172
src/cli.js
172
src/cli.js
@@ -9,6 +9,7 @@ const path = require('path')
|
|||||||
const fs = require('fs')
|
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 pkg = require('../package.json')
|
const pkg = require('../package.json')
|
||||||
@@ -32,19 +33,28 @@ program
|
|||||||
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`)
|
||||||
|
await git.addRemote(name)
|
||||||
|
console.log(`Added git remote for "${name}" as "pear"`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
home.createAppFolder(name)
|
try {
|
||||||
console.log(`Added project "${name}" to gitpear`)
|
home.createAppFolder(name)
|
||||||
await git.createBareRepo(name)
|
console.log(`Added project "${name}" to gitpear`)
|
||||||
console.log(`Created bare repo for "${name}"`)
|
} catch (e) { }
|
||||||
await git.addRemote(name)
|
try {
|
||||||
console.log(`Added git remote for "${name}" as "pear"`)
|
await git.createBareRepo(name)
|
||||||
|
console.log(`Created bare repo for "${name}"`)
|
||||||
|
} catch (e) { }
|
||||||
|
try {
|
||||||
|
await git.addRemote(name)
|
||||||
|
console.log(`Added git remote for "${name}" as "pear"`)
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
if (options.share) {
|
if (options.share) {
|
||||||
home.shareAppFolder(name)
|
try { home.shareAppFolder(name) } catch (e) { }
|
||||||
await git.push()
|
try { acl.setACL(name) } catch (e) { }
|
||||||
|
try { await git.push() } catch (e) { }
|
||||||
console.log(`Shared "${name}" project`)
|
console.log(`Shared "${name}" project`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -53,25 +63,153 @@ program
|
|||||||
.command('share')
|
.command('share')
|
||||||
.description('share a gitpear repo')
|
.description('share a gitpear repo')
|
||||||
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
||||||
.action(async (p, options) => {
|
.addArgument(new commander.Argument('[v]', 'visibility of the repo').default('public'))
|
||||||
const name = path.resolve(p).split(path.sep).pop()
|
.action(async (p, v, options) => {
|
||||||
if ((home.isInitialized(name))) {
|
const fullPath = path.resolve(p)
|
||||||
home.shareAppFolder(name)
|
if (!fs.existsSync(path.join(fullPath, '.git'))) {
|
||||||
await git.push()
|
console.error('Not a git repo')
|
||||||
console.log(`Shared "${name}" project`)
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = fullPath.split(path.sep).pop()
|
||||||
|
if (!home.isInitialized(name)) {
|
||||||
|
console.error(`${name} is not initialized`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
try { home.shareAppFolder(name) } catch (e) { }
|
||||||
|
try { acl.setACL(name, { visibility: v }) } catch (e) { }
|
||||||
|
try { await git.push() } catch (e) { }
|
||||||
|
console.log(`Shared "${name}" project, as ${v} 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
|
||||||
|
.command('acl')
|
||||||
|
.description('set acl of a gitpear 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`${name} is not initialized`)
|
if (a === 'list') {
|
||||||
process.exit(1)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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('.'))
|
.addArgument(new commander.Argument('[p]', 'path to the repo').default('.'))
|
||||||
.action((p, options) => {
|
.action((p, options) => {
|
||||||
const name = path.resolve(p).split(path.sep).pop()
|
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))) {
|
if ((home.isInitialized(name))) {
|
||||||
home.unshareAppFolder(name)
|
home.unshareAppFolder(name)
|
||||||
console.log(`Unshared "${name}" project`)
|
console.log(`Unshared "${name}" project`)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/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')
|
||||||
@@ -9,6 +10,11 @@ const Hyperdrive = require('hyperdrive')
|
|||||||
const crypto = require('hypercore-crypto')
|
const crypto = require('hypercore-crypto')
|
||||||
|
|
||||||
const git = require('./git.js')
|
const git = require('./git.js')
|
||||||
|
const home = require('./home')
|
||||||
|
const auth = require('./auth')
|
||||||
|
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})\/(.*)/)
|
||||||
@@ -22,7 +28,12 @@ const targetKey = matches[1]
|
|||||||
const repoName = matches[2]
|
const repoName = matches[2]
|
||||||
|
|
||||||
const store = new Corestore(RAM)
|
const store = new Corestore(RAM)
|
||||||
const swarm = new Hyperswarm()
|
const swarm = new Hyperswarm({ keypair: home.getKeyPair() })
|
||||||
|
|
||||||
|
if (!home.isDaemonRunning()) {
|
||||||
|
console.error('Please start git pear daemon')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false })
|
swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false })
|
||||||
|
|
||||||
@@ -30,7 +41,12 @@ swarm.on('connection', async (socket) => {
|
|||||||
store.replicate(socket)
|
store.replicate(socket)
|
||||||
const rpc = new ProtomuxRPC(socket)
|
const rpc = new ProtomuxRPC(socket)
|
||||||
|
|
||||||
const reposRes = await rpc.request('get-repos')
|
let payload = { body: { url, method: 'get-repos' } }
|
||||||
|
if (process.env.GIT_PEAR_AUTH) {
|
||||||
|
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())
|
const repositories = JSON.parse(reposRes.toString())
|
||||||
if (!repositories) {
|
if (!repositories) {
|
||||||
console.error('Failed to retrieve repositories')
|
console.error('Failed to retrieve repositories')
|
||||||
@@ -56,27 +72,87 @@ swarm.on('connection', async (socket) => {
|
|||||||
|
|
||||||
await drive.core.update({ wait: true })
|
await drive.core.update({ wait: true })
|
||||||
|
|
||||||
const refsRes = await rpc.request('get-refs', Buffer.from(repoName))
|
payload = { body: { url, method: 'get-refs', data: repoName }}
|
||||||
|
if (process.env.GIT_PEAR_AUTH) {
|
||||||
|
payload.header = await auth.getToken(payload.body)
|
||||||
|
}
|
||||||
|
const refsRes = await rpc.request('get-refs', Buffer.from(JSON.stringify(payload)))
|
||||||
|
|
||||||
await talkToGit(JSON.parse(refsRes.toString()), drive)
|
let commit
|
||||||
|
try {
|
||||||
|
commit = await git.getCommit()
|
||||||
|
} catch (e) { }
|
||||||
|
await talkToGit(JSON.parse(refsRes.toString()), drive, repoName, rpc, commit)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function talkToGit (refs, drive) {
|
async function talkToGit (refs, drive, repoName, rpc, commit) {
|
||||||
for (const ref in refs) {
|
|
||||||
console.warn(refs[ref] + '\t' + ref)
|
|
||||||
}
|
|
||||||
process.stdin.setEncoding('utf8')
|
process.stdin.setEncoding('utf8')
|
||||||
const didFetch = false
|
const didFetch = false
|
||||||
process.stdin.on('readable', async function () {
|
process.stdin.on('readable', async function () {
|
||||||
const chunk = process.stdin.read()
|
const chunk = process.stdin.read()
|
||||||
if (chunk === 'capabilities\n') {
|
if (chunk === 'capabilities\n') {
|
||||||
|
process.stdout.write('list\n')
|
||||||
|
process.stdout.write('push\n')
|
||||||
process.stdout.write('fetch\n\n')
|
process.stdout.write('fetch\n\n')
|
||||||
} else if (chunk === 'list\n') {
|
} else if (chunk && chunk.search(/^push/) !== -1) {
|
||||||
|
const [_command, path] = chunk.split(' ')
|
||||||
|
let [src, dst] = path.split(':')
|
||||||
|
|
||||||
|
const isDelete = !src
|
||||||
|
const isForce = src.startsWith('+')
|
||||||
|
|
||||||
|
if (!home.isShared(repoName)) {
|
||||||
|
home.shareAppFolder(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
} else if (isForce) {
|
||||||
|
console.warn('To', url)
|
||||||
|
await git.push(src, isForce)
|
||||||
|
src = src.replace('+', '')
|
||||||
|
method = 'f-push'
|
||||||
|
} else {
|
||||||
|
console.warn('To', url)
|
||||||
|
await git.push(src)
|
||||||
|
method = 'push'
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = home.readPk()
|
||||||
|
let payload = { body: {
|
||||||
|
url: `pear://${publicKey}/${repoName}`,
|
||||||
|
data: `${dst}#${commit}`,
|
||||||
|
method
|
||||||
|
} }
|
||||||
|
if (process.env.GIT_PEAR_AUTH) {
|
||||||
|
payload.header = await auth.getToken(payload.body)
|
||||||
|
}
|
||||||
|
const res = await rpc.request(method, Buffer.from(JSON.stringify(payload)))
|
||||||
|
|
||||||
|
process.stdout.write('\n\n')
|
||||||
|
process.exit(0)
|
||||||
|
} else if (chunk && chunk.search(/^list/) !== -1) { // list && list for-push
|
||||||
|
for (const ref in refs) {
|
||||||
|
console.warn(refs[ref] + '\t' + ref)
|
||||||
|
}
|
||||||
Object.keys(refs).forEach(function (branch, i) {
|
Object.keys(refs).forEach(function (branch, i) {
|
||||||
process.stdout.write(refs[branch] + ' ' + branch + '\n')
|
process.stdout.write(refs[branch] + ' ' + branch + '\n')
|
||||||
})
|
})
|
||||||
process.stdout.write('\n')
|
process.stdout.write('\n')
|
||||||
} else if (chunk && chunk.search(/^fetch/) !== -1) {
|
} else if (chunk && chunk.search(/^fetch/) !== -1) {
|
||||||
|
for (const ref in refs) {
|
||||||
|
console.warn(refs[ref] + '\t' + ref)
|
||||||
|
}
|
||||||
const lines = chunk.split(/\n/).filter(l => l !== '')
|
const lines = chunk.split(/\n/).filter(l => l !== '')
|
||||||
|
|
||||||
const targets = []
|
const targets = []
|
||||||
|
|||||||
27
src/git.js
27
src/git.js
@@ -1,6 +1,25 @@
|
|||||||
const { getCodePath } = require('./home')
|
const { getCodePath } = require('./home')
|
||||||
const { spawn } = require('child_process')
|
const { spawn } = require('child_process')
|
||||||
|
|
||||||
|
async function getCommit () {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const process = spawn('git', ['rev-parse', 'HEAD'])
|
||||||
|
let outBuffer = Buffer.from('')
|
||||||
|
process.stdout.on('data', data => {
|
||||||
|
outBuffer = Buffer.concat([outBuffer, data])
|
||||||
|
})
|
||||||
|
|
||||||
|
let errBuffer = Buffer.from('')
|
||||||
|
process.stderr.on('err', data => {
|
||||||
|
errBuffer = Buffer.concat([errBuffer, data])
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('close', code => {
|
||||||
|
return code === 0 ? resolve(outBuffer.toString().replace('\n', '')) : reject(errBuffer)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function lsPromise (url) {
|
async function lsPromise (url) {
|
||||||
const ls = spawn('git', ['ls-remote', url])
|
const ls = spawn('git', ['ls-remote', url])
|
||||||
const res = {}
|
const res = {}
|
||||||
@@ -31,8 +50,10 @@ async function addRemote (name) {
|
|||||||
return await doGit(init)
|
return await doGit(init)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function push (branch = 'master') {
|
async function push (branch = 'master', force = false) {
|
||||||
const push = spawn('git', ['push', 'pear', branch])
|
const args = ['push', 'pear', branch]
|
||||||
|
if (force) args.push('-f')
|
||||||
|
const push = spawn('git', args)
|
||||||
return await doGit(push)
|
return await doGit(push)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,4 +201,4 @@ async function unpackStream (packStream) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { lsPromise, uploadPack, unpackFile, unpackStream, createBareRepo, addRemote, push }
|
module.exports = { lsPromise, uploadPack, unpackFile, unpackStream, createBareRepo, addRemote, push, getCommit }
|
||||||
|
|||||||
13
src/home.js
13
src/home.js
@@ -14,6 +14,11 @@ function shareAppFolder (name) {
|
|||||||
fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w')
|
fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getACLFilePath (name) {
|
||||||
|
if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared')
|
||||||
|
return `${APP_HOME}/${name}/.git-daemon-export-ok`
|
||||||
|
}
|
||||||
|
|
||||||
function unshareAppFolder (name) {
|
function unshareAppFolder (name) {
|
||||||
fs.unlinkSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)
|
fs.unlinkSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)
|
||||||
}
|
}
|
||||||
@@ -90,6 +95,10 @@ function getDaemonPid () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDaemonRunning () {
|
||||||
|
return fs.existsSync(`${APP_HOME}/.daemon.pid`)
|
||||||
|
}
|
||||||
|
|
||||||
function removeDaemonPid () {
|
function removeDaemonPid () {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(`${APP_HOME}/.daemon.pid`)
|
fs.unlinkSync(`${APP_HOME}/.daemon.pid`)
|
||||||
@@ -114,5 +123,7 @@ module.exports = {
|
|||||||
getErrStream,
|
getErrStream,
|
||||||
storeDaemonPid,
|
storeDaemonPid,
|
||||||
getDaemonPid,
|
getDaemonPid,
|
||||||
removeDaemonPid
|
isDaemonRunning,
|
||||||
|
removeDaemonPid,
|
||||||
|
getACLFilePath
|
||||||
}
|
}
|
||||||
|
|||||||
135
src/rpc.js
Normal file → Executable file
135
src/rpc.js
Normal file → Executable file
@@ -1,4 +1,8 @@
|
|||||||
const ProtomuxRPC = require('protomux-rpc')
|
const ProtomuxRPC = require('protomux-rpc')
|
||||||
|
const { spawn } = require('child_process')
|
||||||
|
const home = require('./home')
|
||||||
|
const auth = require('./auth')
|
||||||
|
const acl = require('./acl')
|
||||||
|
|
||||||
module.exports = class RPC {
|
module.exports = class RPC {
|
||||||
constructor (announcedRefs, repositories, drives) {
|
constructor (announcedRefs, repositories, drives) {
|
||||||
@@ -16,23 +20,138 @@ module.exports = class RPC {
|
|||||||
// for example check of peerInfo.publicKey is in a list of allowed keys
|
// for example check of peerInfo.publicKey is in a list of allowed keys
|
||||||
// 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
|
||||||
|
|
||||||
rpc.respond('get-repos', req => this.getReposHandler(req))
|
/* -- PULL HANDLERS -- */
|
||||||
rpc.respond('get-refs', async req => await this.getRefsHandler(req))
|
rpc.respond('get-repos', async req => await this.getReposHandler(peerInfo.publicKey, req))
|
||||||
|
rpc.respond('get-refs', async req => await this.getRefsHandler(peerInfo.publicKey, req))
|
||||||
|
|
||||||
|
if (process.env.GIT_PEAR_AUTH) {
|
||||||
|
/* -- PUSH HANDLERS -- */
|
||||||
|
rpc.respond('push', async req => await this.pushHandler(peerInfo.publicKey, req))
|
||||||
|
rpc.respond('f-push', async req => await this.forcePushHandler(peerInfo.publicKey, req))
|
||||||
|
rpc.respond('d-branch', async req => await this.deleteBranchHandler(peerInfo.publicKey, req))
|
||||||
|
}
|
||||||
|
|
||||||
this.connections[peerInfo.publicKey] = rpc
|
this.connections[peerInfo.publicKey] = rpc
|
||||||
}
|
}
|
||||||
|
|
||||||
getReposHandler (_req) {
|
async getReposHandler (publicKey, req) {
|
||||||
|
const { branch, url, userId } = await this.parseReq(publicKey, req)
|
||||||
|
|
||||||
const res = {}
|
const res = {}
|
||||||
for (const repo in this.repositories) {
|
for (const repoName in this.repositories) {
|
||||||
res[repo] = this.drives[repo].key.toString('hex')
|
// 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))
|
return Buffer.from(JSON.stringify(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
getRefsHandler (req) {
|
async getRefsHandler (publicKey, req) {
|
||||||
const res = this.repositories[req.toString()]
|
const { repoName, branch, url, userId } = await this.parseReq(publicKey, req)
|
||||||
|
const res = this.repositories[repoName]
|
||||||
|
|
||||||
return Buffer.from(JSON.stringify(res))
|
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) {
|
||||||
|
if (!process.env.GIT_PEAR_AUTH) return publicKey.toString('hex')
|
||||||
|
if (!request.header) throw new Error('You are not allowed to access this repo')
|
||||||
|
|
||||||
|
return (await auth.getId({ ...request.body, payload: request.header })).userId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
test/acl.test.js
Normal file
59
test/acl.test.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const test = require('brittle')
|
||||||
|
const acl = require('../src/acl')
|
||||||
|
|
||||||
|
test('acl', async t => {
|
||||||
|
const repoName = 'foo'
|
||||||
|
|
||||||
|
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(Object.keys(aclObj.ACL).length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getACL', async t => {
|
||||||
|
const aclObj = acl.setACL(repoName)
|
||||||
|
t.alike(acl.getACL(repoName), aclObj)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('makeRepoPublic', async t => {
|
||||||
|
acl.makeRepoPublic(repoName)
|
||||||
|
t.is(acl.getRepoVisibility(repoName), 'public')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('makeRepoPrivate', async t => {
|
||||||
|
acl.makeRepoPrivate(repoName)
|
||||||
|
t.is(acl.getRepoVisibility(repoName), 'private')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('grantAccessToUser', async t => {
|
||||||
|
acl.grantAccessToUser(repoName, 'user1', 'admin')
|
||||||
|
t.alike(acl.getUserRole(repoName, 'user1'), 'admin')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('addProtectedBranch', async t => {
|
||||||
|
acl.addProtectedBranch(repoName, 'branch1')
|
||||||
|
t.is(acl.getACL(repoName).protectedBranches.length, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('removeProtectedBranch', async t => {
|
||||||
|
acl.removeProtectedBranch(repoName, 'branch1')
|
||||||
|
t.is(acl.getACL(repoName).protectedBranches.length, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getAdmins', async t => {
|
||||||
|
acl.grantAccessToUser(repoName, 'user2', 'admin')
|
||||||
|
t.alike(acl.getAdmins(repoName), ['user1', 'user2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getContributors', async t => {
|
||||||
|
acl.grantAccessToUser(repoName, 'user3', 'contributor')
|
||||||
|
t.alike(acl.getContributors(repoName), ['user3', 'user1', 'user2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getViewers', async t => {
|
||||||
|
acl.grantAccessToUser(repoName, 'user4', 'viewer')
|
||||||
|
t.alike(acl.getViewers(repoName), ['user4', 'user3', 'user1', 'user2'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,33 +9,33 @@ 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')
|
home.createAppFolder('test_code')
|
||||||
|
|
||||||
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test', 'code')))
|
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code', 'code')))
|
||||||
|
|
||||||
t.absent(home.isShared('test'))
|
t.absent(home.isShared('test_code'))
|
||||||
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test', '.git-daemon-export-ok')))
|
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok')))
|
||||||
|
|
||||||
home.shareAppFolder('test')
|
home.shareAppFolder('test_code')
|
||||||
|
|
||||||
t.ok(home.isShared('test'))
|
t.ok(home.isShared('test_code'))
|
||||||
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test', '.git-daemon-export-ok')))
|
t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok')))
|
||||||
|
|
||||||
home.unshareAppFolder('test')
|
home.unshareAppFolder('test_code')
|
||||||
|
|
||||||
t.absent(home.isShared('test'))
|
t.absent(home.isShared('test_code'))
|
||||||
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test', '.git-daemon-export-ok')))
|
t.absent(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok')))
|
||||||
|
|
||||||
t.absent(home.isInitialized('test'))
|
t.absent(home.isInitialized('test_code'))
|
||||||
t.ok(home.isInitialized('foo'))
|
t.ok(home.isInitialized('foo'))
|
||||||
|
|
||||||
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')), path.resolve(path.join(home.APP_HOME, 'test', 'code')))
|
t.alike(path.resolve(home.getCodePath('test_code')), path.resolve(path.join(home.APP_HOME, 'test_code', '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 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ test('e2e', async t => {
|
|||||||
clientStore.replicate(socket)
|
clientStore.replicate(socket)
|
||||||
const rpc = new ProtomuxRPC(socket)
|
const rpc = new ProtomuxRPC(socket)
|
||||||
|
|
||||||
const reposRes = await rpc.request('get-repos')
|
const repoName = 'foo'
|
||||||
|
const url = `${serverSwarm.keyPair.publicKey.toString('hex')}/${repoName}`
|
||||||
|
let payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-repos' } }))
|
||||||
|
const reposRes = await rpc.request('get-repos', payload)
|
||||||
const reposJSON = JSON.parse(reposRes.toString())
|
const reposJSON = JSON.parse(reposRes.toString())
|
||||||
|
|
||||||
const driveKey = Buffer.from(reposJSON.foo, 'hex')
|
const driveKey = Buffer.from(reposJSON.foo, 'hex')
|
||||||
@@ -53,7 +56,8 @@ test('e2e', async t => {
|
|||||||
|
|
||||||
await drive.core.update({ wait: true })
|
await drive.core.update({ wait: true })
|
||||||
|
|
||||||
const refsRes = await rpc.request('get-refs', Buffer.from('foo'))
|
payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-refs', data: repoName }}))
|
||||||
|
const refsRes = await rpc.request('get-refs', payload)
|
||||||
t.ok(refsRes)
|
t.ok(refsRes)
|
||||||
|
|
||||||
const want = Object.values(JSON.parse(refsRes.toString()))[0]
|
const want = Object.values(JSON.parse(refsRes.toString()))[0]
|
||||||
|
|||||||
BIN
test_home.tar.gz
BIN
test_home.tar.gz
Binary file not shown.
Reference in New Issue
Block a user