mirror of
https://github.com/markqvist/Reticulum.git
synced 2026-06-23 04:16:12 -07:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9885a70a88 | |||
| e4a85de089 | |||
| ca0d2dffbe | |||
| 511d169c77 | |||
| 19bc8ef85c | |||
| 1e33d3eebb | |||
| d18f434583 | |||
| 64749b4d18 | |||
| 875d8ef7eb | |||
| 20283f1536 | |||
| c4af328802 | |||
| a2193b9ffd | |||
| d2542fd49b | |||
| 0333884877 | |||
| 63947ed69a | |||
| d6d18ce29c | |||
| 2ef58d8b59 | |||
| 15f2d1635e | |||
| c83b71f49a | |||
| f0824fd71e | |||
| 8dde60658f | |||
| 9437648ae5 | |||
| 71b19aca2c | |||
| 7d320f8cd5 | |||
| 340d0883a7 | |||
| 66096acc29 | |||
| e35100d865 | |||
| 128455ef01 | |||
| 10156cc90e | |||
| 4f5482f2ae | |||
| 2b9fdae74b | |||
| 7506caa0da | |||
| b1f522277c | |||
| af6e0c9ecf | |||
| 176567e3f1 | |||
| 15cd4268ac | |||
| 9307db16c4 | |||
| 0f29ab629a | |||
| b2a4ceb853 | |||
| 6c7f1d068b | |||
| b76beb602d | |||
| 0c68f6491a | |||
| 038981474a | |||
| df0b4a5165 | |||
| db7359f56d | |||
| 12e45b6483 | |||
| ba8fca6f87 | |||
| 9b99b72f61 | |||
| 03cfbc2eb6 | |||
| c92872a81b | |||
| f3f4d9bca3 | |||
| e7a317f0a0 | |||
| d5b64a4af3 | |||
| 5667a0bbac | |||
| 7e46422c16 | |||
| 869a803149 | |||
| f744e4d9a3 | |||
| 1a7607cba3 | |||
| d881c111f6 | |||
| bdac57ec0b | |||
| c15f566cfa | |||
| bdc79b9097 | |||
| 102eccb77d | |||
| e8b236c7d8 | |||
| d69491eb80 | |||
| 256a4d0b92 | |||
| c5add012c1 | |||
| 6ecc8933b4 | |||
| 42b5661979 | |||
| 6333fb39bf | |||
| ea27a8b8a7 | |||
| 358f9c3b0c | |||
| cb3ef69072 | |||
| eee9354657 | |||
| ff86a1d7e6 | |||
| e49f31322c |
@@ -1,3 +1,58 @@
|
||||
### 2026-05-18: RNS 1.2.8
|
||||
|
||||
This release improves the `rngit` system with signed release manifest generation and automatic artifact signing. It also includes several additions to `rnid` and various minor fixes and improvements to the `rngit` system.
|
||||
|
||||
**Changes**
|
||||
- Added signed release manifest generation to `rngit release`
|
||||
- Added verified release fetching to `rngit release`
|
||||
- Added automatic artifact signing to `rngit release`
|
||||
- Added signed message creation from file to `rnid`
|
||||
- Added signed message metadata output option to `rnid`
|
||||
- Added `rsm` metadata embedding and spec validation to `rnid`
|
||||
- Added identity and destination aliases to `rngit`
|
||||
- Added blocked identities option to `rngit`
|
||||
- Added ability to render raw micron in markdown files to `rngit`
|
||||
- Added fork and mirror last sync time to repository page in `rngit`
|
||||
- Better handling of silly links in `rngit`
|
||||
- Fixed markdown table cell truncation not closing micron tags
|
||||
- Fixed various minor bugs and inconsistencies in `rngit`
|
||||
- Dropped `note` metadata field requirement from `rsg` structure
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V manifest.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-05-17: RNS 1.2.7
|
||||
|
||||
This release significantly improves the `rngit` system with fork, mirroring and empty repository creation functionality, a new work document proposals feature, improvements to the transport core reliability and efficiency and various other tweaks and improvements.
|
||||
|
||||
**Changes**
|
||||
- Added work document proposals functionality to `rngit`
|
||||
- Added fork and mirroring support to `rngit`
|
||||
- Added ability to create new repositories remotely to `rngit`
|
||||
- Added latest release management to `rngit`
|
||||
- Added download stats to `rngit`
|
||||
- Improved shared instance RPC error handling
|
||||
- Improved announce cache cleaning
|
||||
- Improved `rngit` page node link handling
|
||||
- Improved stats pages `rngit`
|
||||
- Improved transfer completed feedback in `rncp`, thanks to **neutral**
|
||||
- Improved interface transport insertion and removal
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns*.whl
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether it is valid. If the signature cannot be verified, the file has been tampered with and should be thrown very far away in a jiffy.
|
||||
|
||||
### 2026-05-14: RNS 1.2.6
|
||||
|
||||
This release adds further improvements to the `rnid` and `rngit` utilities, and includes several bugfixes and other improvements.
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
>> Reticulum Network Stack
|
||||
|
||||
To understand the foundational philosophy and goals of this system, read the `!`[Zen of Reticulum`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=Zen+of+Reticulum.md]`!.
|
||||
To understand the foundational philosophy and goals of this system, read the `_`!`[Zen of Reticulum`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=Zen+of+Reticulum.md]`!`_.
|
||||
|
||||
Reticulum is the cryptography-based networking stack for building local and wide-area networks with readily available hardware. It can operate even with very high latency and extremely low bandwidth. Reticulum allows you to build wide-area networks with off-the-shelf tools, and offers end-to-end encryption and connectivity, initiator anonymity, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable delivery acknowledgements and more.
|
||||
|
||||
The vision of Reticulum is to allow anyone to be their own network operator, and to make it cheap and easy to cover vast areas with a myriad of independent, inter-connectable and autonomous networks. Reticulum **is not** *one* network. It is **a tool** for building *thousands of networks*. Networks without kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate with each other, and require no central oversight. Networks for human beings. *Networks for the people*.
|
||||
The vision of Reticulum is to allow anyone to be their own network operator, and to make it cheap and easy to cover vast areas with a myriad of independent, inter-connectable and autonomous networks. Reticulum `!is not`! `*one`* network. It is `!a tool`! for building `*thousands of networks`*. Networks without kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate with each other, and require no central oversight. Networks for human beings. `*Networks for the people`*.
|
||||
|
||||
Reticulum is a complete networking stack, and does not rely on IP or higher layers, but it is possible to use IP as the underlying carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the Internet or private IP networks.
|
||||
|
||||
Having no dependencies on traditional networking stacks frees up overhead that has been used to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality, even in open and trustless networks.
|
||||
|
||||
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3.
|
||||
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python.
|
||||
|
||||
>> Read The Manual
|
||||
|
||||
The full documentation for Reticulum is available on `!`[this node`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!.
|
||||
The full documentation for Reticulum is available on `_`!`[this node`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
|
||||
|
||||
You can also download the `!`[Reticulum manual as a PDF`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`! or `!`[as an e-book in EPUB format`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`!.
|
||||
You can also download the `_`!`[Reticulum manual as a PDF`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`!`_ or `_`!`[as an e-book in EPUB format`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`!`_.
|
||||
|
||||
>> Notable Features
|
||||
|
||||
@@ -66,33 +66,31 @@ The Reticulum Protocol was dedicated to the Public Domain in 2016.
|
||||
|
||||
>> Examples of Reticulum Applications
|
||||
|
||||
If you want to quickly get an idea of what Reticulum can do, take a look at the [Programs Using Reticulum](https://reticulum.network/manual/software.html) section of the manual, or the following resources:
|
||||
If you want to quickly get an idea of what Reticulum can do, take a look at the `_`!`[Programs Using Reticulum`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/software.md]`!`_ section of the manual, or the following resources:
|
||||
|
||||
• [LXMF](https://github.com/markqvist/lxmf) is a distributed, delay and disruption tolerant message transfer
|
||||
protocol built on Reticulum
|
||||
• `_`!`[LXMF`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=lxmf]`!`_ is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum
|
||||
|
||||
• The [LXST](https://github.com/markqvist/lxst) protocol and framework provides real-time audio and signals
|
||||
transport over Reticulum. It includes primitives and utilities for building voice-based applications and
|
||||
hardware devices, such as the `B333rnphone`b program, that can be used to build hardware telephones.
|
||||
• The `_`!`[LXST`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=lxst]`!`_ protocol and framework provides real-time audio and signals transport over Reticulum. It
|
||||
includes primitives and utilities for building voice-based applications and hardware devices,
|
||||
such as the `B333rnphone`b program, that can be used to build hardware telephones.
|
||||
|
||||
• For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet)
|
||||
• For an off-grid, encrypted and resilient mesh communications platform, see `_`!`[Nomad Network`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=nomadnet]`!`_.
|
||||
|
||||
• The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical
|
||||
interface and many advanced features, such as file transfers, image and voice messages, real-time voice calls,
|
||||
a distributed telemetry system, mapping capabilities and full plugin extensibility.
|
||||
• The Android, Linux, macOS and Windows app `_`!`[Sideband`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=sideband]`!`_ has a graphical interface and many advanced
|
||||
features, such as file transfers, image and voice messages, real-time voice calls, a distributed
|
||||
telemetry system, mapping capabilities and full plugin extensibility.
|
||||
|
||||
• [MeshChatX](https://git.quad4.io/RNS-Things/MeshChatX) is a full-featured LXMF client with many built-in tools
|
||||
and functionalities, that also supports image and voice messages, file transfers and voice calls. It also
|
||||
includes a built-in page browser for browsing Nomad Network nodes.
|
||||
• `_`!`[MeshChatX`c10d80b1a42fa958c37a6cc30dc04f53]`!`_ (`_`!`[source`5399f5a0212477618821e91e88ce053b:/page/repo.mu`g=quad4|r=MeshChatX]`_`!) is a full-featured LXMF client with many built-in tools and functionalities,
|
||||
that also supports image and voice messages, file transfers and voice calls. It also includes a
|
||||
built-in page browser for browsing Nomad Network nodes.
|
||||
|
||||
• You can use the included [rnsh](https://reticulum.network/manual/using.html#the-rnsh-utility) program to
|
||||
establish remote shell sessions over Reticulum.
|
||||
• You can use the included `_`!`[rnsh`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/using.md|anchor=the-rnsh-utility]`!`_ program to establish remote shell sessions over Reticulum.
|
||||
|
||||
>> Where can Reticulum be used?
|
||||
|
||||
Over practically any medium that can support at least a half-duplex channel with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, WiFi and Ethernet devices, free-space optical links, and similar systems are all examples of the types of physical devices Reticulum can use.
|
||||
|
||||
An open-source LoRa-based interface called [RNode](https://markqvist.github.io/Reticulum/manual/hardware.html#rnode) has been designed specifically for use with Reticulum. It is possible to build yourself, or it can be purchased as a complete transceiver that just needs a USB connection to the host.
|
||||
An open-source LoRa-based interface called `_`!`[RNode`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/hardware.md|anchor=rnode]`!`_ has been designed specifically for use with Reticulum. It is possible to build yourself, or it can be purchased as a complete transceiver that just needs a USB connection to the host.
|
||||
|
||||
Reticulum can also be encapsulated over existing IP networks, so there's nothing stopping you from using it over wired Ethernet, your local WiFi network or the Internet, where it'll work just as well. In fact, one of the strengths of Reticulum is how easily it allows you to connect different mediums into a self-configuring, resilient and encrypted mesh, using any available mixture of available infrastructure.
|
||||
|
||||
@@ -100,9 +98,9 @@ As an example, it's possible to set up a Raspberry Pi connected to both a LoRa r
|
||||
|
||||
>> How do I get started?
|
||||
|
||||
The best way to get started with the Reticulum Network Stack depends on what you want to do. For full details and examples, have a look at the [Getting Started Fast](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
The best way to get started with the Reticulum Network Stack depends on what you want to do. For full details and examples, have a look at the `_`!`[Getting Started Fast`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/gettingstartedfast.md]`!`_ section of the `_`!`[Reticulum Manual`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
|
||||
|
||||
To simply install Reticulum and related utilities on your system, the easiest way is via `B333pip`b. You can then start any program that uses Reticulum, or start Reticulum as a system service with [the rnsd utility](https://markqvist.github.io/Reticulum/manual/using.html#the-rnsd-utility).
|
||||
To simply install Reticulum and related utilities on your system, the easiest way is via `B333pip`b. You can then start any program that uses Reticulum, or start Reticulum as a system service with `_`!`[the rnsd utility`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/using.md|anchor=the-rnsd-utility]`!`_.
|
||||
|
||||
`B333
|
||||
`=
|
||||
@@ -131,10 +129,11 @@ When first started, Reticulum will create a default configuration file, providin
|
||||
|
||||
If you have an old version of `B333pip`b on your system, you may need to upgrade it first with `B333pip install pip --upgrade`b. If you no not already have `B333pip`b installed, you can install it using the package manager of your system with `B333sudo apt install python3-pip`b or similar.
|
||||
|
||||
For more detailed examples on how to expand communication over many mediums such as packet radio or LoRa, serial ports, or over fast IP links and the Internet using the UDP and TCP interfaces, take a look at the [Supported Interfaces](https://markqvist.github.io/Reticulum/manual/interfaces.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
For more detailed examples on how to expand communication over many mediums such as packet radio or LoRa, serial ports, or over fast IP links and the Internet using the UDP and TCP interfaces, take a look at the `_`!`[Supported Interfaces`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/interfaces.md]`!`_ section of the `_`!`[Reticulum Manual`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
|
||||
|
||||
>> Included Utilities
|
||||
Reticulum includes a range of useful utilities for managing your networks, viewing status and information, and other tasks. You can read more about these programs in the [Included Utility Programs](https://markqvist.github.io/Reticulum/manual/using.html#included-utility-programs) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
Reticulum includes a range of useful utilities for managing your networks, viewing status and information, and other tasks. You can read more about these programs in the `_`!`[Included Utility Programs`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/using.md|anchor=included-utility-programs]`!`_ section of the `_`!`[Reticulum Manual`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
|
||||
|
||||
• The system daemon `B333rnsd`b for running Reticulum as an always-available service
|
||||
• An interface status utility called `B333rnstatus`b, that displays information about interfaces
|
||||
@@ -147,16 +146,14 @@ Reticulum includes a range of useful utilities for managing your networks, viewi
|
||||
• The `B333rngit`b program provides a full multi-repository Git node for serving repositories over Reticulum
|
||||
• The included `B333git-remote-rns`b helper allows you to interact with Git repositories over Reticulum
|
||||
|
||||
All tools, including `B333rnx`b and `B333rncp`b, work reliably and well even over very low-bandwidth links like LoRa or Packet Radio. For full-featured remote shells over Reticulum, also have a look at the [rnsh](https://github.com/acehoss/rnsh) program.
|
||||
|
||||
>> Supported interface types and devices
|
||||
|
||||
Reticulum implements a range of generalised interface types that covers most of the communications hardware that Reticulum can run over. If your hardware is not supported, it's [simple to implement a custom interface module](https://markqvist.github.io/Reticulum/manual/interfaces.html#custom-interfaces).
|
||||
Reticulum implements a range of generalised interface types that covers most of the communications hardware that Reticulum can run over. If your hardware is not supported, it's `_`!`[simple to implement a custom interface module`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/interfaces.md|anchor=custom-interfaces]`!`_.
|
||||
|
||||
Currently, the following built-in interfaces are supported:
|
||||
|
||||
• Any Ethernet device
|
||||
• LoRa using [RNode](https://unsigned.io/rnode/)
|
||||
• LoRa using `_`!`[RNode`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=rnode_firmware]`!`_
|
||||
• Packet Radio TNCs (with or without AX.25)
|
||||
• KISS-compatible hardware and software modems
|
||||
• Any device with a serial port
|
||||
@@ -166,64 +163,72 @@ Currently, the following built-in interfaces are supported:
|
||||
• Custom hardware via stdio or pipes
|
||||
|
||||
>> Performance
|
||||
Reticulum targets a *very* wide usable performance envelope, but prioritises functionality and performance on low-bandwidth mediums. The goal is to provide a dynamic performance envelope from 250 bits per second, to 1 gigabit per second on normal hardware.
|
||||
|
||||
Reticulum targets a `*very`* wide usable performance envelope, but prioritises functionality and performance on low-bandwidth mediums. The goal is to provide a dynamic performance envelope from 250 bits per second, to 1 gigabit per second on normal hardware.
|
||||
|
||||
Currently, the usable performance envelope is approximately 150 bits per second to 500 megabits per second, with physical mediums faster than that not being saturated. Performance beyond the current level is intended for future upgrades, but not highly prioritised at this point in time.
|
||||
|
||||
>> Current Status
|
||||
|
||||
All core protocol features are implemented and functioning, but additions will probably occur as real-world use is explored and understood. The API and wire-format can be considered stable.
|
||||
|
||||
>> Dependencies
|
||||
|
||||
The installation of the default `B333rns`b package requires only two external dependencies, listed below. Almost all systems and distributions have readily available packages for these dependencies, and when the `B333rns`b package is installed with `B333pip`b, they will be downloaded and installed as well.
|
||||
|
||||
• [PyCA/cryptography](https://github.com/pyca/cryptography)
|
||||
• [pyserial](https://github.com/pyserial/pyserial)
|
||||
• PyCA/cryptography
|
||||
• pyserial
|
||||
|
||||
On more unusual systems, and in some rare cases, it might not be possible to install or even compile one or more of the above modules. In such situations, you can use the `B333rnspure`b package instead, which require no external dependencies for installation. Please note that the contents of the `B333rns`b and `B333rnspure`b packages are *identical*. The only difference is that the `B333rnspure`b package lists no dependencies required for installation.
|
||||
On more unusual systems, and in some rare cases, it might not be possible to install or even compile one or more of the above modules. In such situations, you can use the `B333rnspure`b package instead, which require no external dependencies for installation. Please note that the contents of the `B333rns`b and `B333rnspure`b packages are `*identical`*. The only difference is that the `B333rnspure`b package lists no dependencies required for installation.
|
||||
|
||||
No matter how Reticulum is installed and started, it will load external dependencies only if they are *needed* and *available*. If for example you want to use Reticulum on a system that cannot support [pyserial](https://github.com/pyserial/pyserial), it is perfectly possible to do so using the `B333rnspure`b package, but Reticulum will not be able to use serial-based interfaces. All other available modules will still be loaded when needed.
|
||||
No matter how Reticulum is installed and started, it will load external dependencies only if they are `*needed`* and `*available`*. If for example you want to use Reticulum on a system that cannot support `B333pyserial`b, it is perfectly possible to do so using the `B333rnspure`b package, but Reticulum will not be able to use serial-based interfaces. All other available modules will still be loaded when needed.
|
||||
|
||||
**Please Note!** If you use the `B333rnspure`b package to run Reticulum on systems that do not support [PyCA/cryptography](https://github.com/pyca/cryptography), it is important that you read and understand the [Cryptographic Primitives](#cryptographic-primitives) section of this document.
|
||||
`!Please Note!`! If you use the `B333rnspure`b package to run Reticulum on systems that do not support PyCA/cryptography, it is important that you read and understand the `!Cryptographic Primitives`! section of this document.
|
||||
|
||||
>> Bootstrapping Connectivity
|
||||
|
||||
Reticulum is not a service you subscribe to, nor is it a single global network you "join". Reticulum provides functionality for discovering available public interfaces over the network itself, and the broader community has provided various directories of publicly available entrypoints to bootstrap connectivity.
|
||||
|
||||
To learn how to establish initial connectivity over Reticulum, read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual.
|
||||
To learn how to establish initial connectivity over Reticulum, read the `_`!`[Bootstrapping Connectivity`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/gettingstartedfast.md|anchor=bootstrapping-connectivity]`!`_ section of the manual.
|
||||
|
||||
If you already have a general idea of how this works, you can use community-run sites such as [directory.rns.recipes](https://directory.rns.recipes/) and [rmap.world](https://rmap.world) to find interface definitions for initial connectivity to the global distributed Reticulum backbone.
|
||||
If you already have a general idea of how this works, you can use community-run sites such as `_`!`[rns.recipes`9ce92808be498e9e05590ff27cbfdfe4]`!`_ and `_`!`[rmap.world`a4a5e861626ce97c9aa544d9ecdf6d22]`!`_ to find interface definitions for initial connectivity to the global distributed Reticulum backbone.
|
||||
|
||||
>> Public Testnet
|
||||
***Important!** Historically, a developer-targeted testnet was made available by the Reticulum project itself. As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, this public testnet, including the Amsterdam Testnet entrypoint, has now been decommissioned. If your still have instances that relied on this entrypoint for connectivity, transition to using the distributed backbone instead. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual for pointers.*
|
||||
|
||||
`!`*Important!`! Historically, a developer-targeted testnet was made available by the Reticulum project itself. As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, this public testnet, including the Amsterdam Testnet entrypoint, has now been decommissioned. If you still have instances that relied on this entrypoint for connectivity, transition to using the distributed backbone instead. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the `_`[Bootstrapping Connectivity`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/gettingstartedfast.md|anchor=bootstrapping-connectivity]`_ section of the manual for pointers.`*
|
||||
|
||||
>> Support Reticulum
|
||||
|
||||
For this to be possible, I need your help. Please support the continued development of open, free and private communications systems by donating via one of the following channels:
|
||||
|
||||
• Monero:
|
||||
• `!Monero`!
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
|
||||
• Bitcoin
|
||||
• `!Bitcoin`!
|
||||
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
|
||||
|
||||
• Ethereum
|
||||
• `!Ethereum`!
|
||||
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
|
||||
|
||||
• Liberapay: https://liberapay.com/Reticulum/
|
||||
• `!Liberapay`!
|
||||
`[https://liberapay.com/Reticulum/]
|
||||
|
||||
• Ko-Fi: https://ko-fi.com/markqvist
|
||||
• `!Ko-Fi`!
|
||||
`[https://ko-fi.com/markqvist]
|
||||
|
||||
>> Cryptographic Primitives
|
||||
|
||||
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic primitives, with widely available implementations that can be used both on general-purpose CPUs and on microcontrollers.
|
||||
|
||||
One of the primary considerations for choosing this particular set of primitives is that they can be implemented *safely* with relatively few pitfalls, on practically all current computing platforms.
|
||||
One of the primary considerations for choosing this particular set of primitives is that they can be implemented `*safely`* with relatively few pitfalls, on practically all current computing platforms.
|
||||
|
||||
The primitives listed here **are authoritative**. Anything claiming to be Reticulum, but not using these exact primitives **is not** Reticulum, and possibly an intentionally compromised or weakened clone. The utilised primitives are:
|
||||
The primitives listed here `!are authoritative`!. Anything `*claiming`* to be Reticulum, but not using these exact primitives `FTA35050`!is not`!`f Reticulum, and possibly an intentionally compromised or weakened clone. The utilised primitives are:
|
||||
|
||||
• Reticulum Identity Keys are 512-bit Curve25519 keysets
|
||||
• A 256-bit Ed25519 key for signatures
|
||||
• A 256-bit X22519 key for ECDH key exchanges
|
||||
• HKDF for key derivation
|
||||
• Encrypted tokens are based on the [Fernet spec](https://github.com/fernet/spec/)
|
||||
• Encrypted tokens are based on the `_`!`[Fernet spec`https://github.com/fernet/spec/]`!`_
|
||||
• Ephemeral keys derived from an ECDH key exchange on Curve25519
|
||||
• HMAC using SHA256 for message authentication
|
||||
• IVs must be generated through `B333os.urandom()`b or better
|
||||
@@ -232,36 +237,37 @@ The primitives listed here **are authoritative**. Anything claiming to be Reticu
|
||||
• SHA-256
|
||||
• SHA-512
|
||||
|
||||
In the default installation configuration, the `B333X25519`b, `B333Ed25519`b, and `B333AES-256-CBC`b primitives are provided by [OpenSSL](https://www.openssl.org/) (via the [PyCA/cryptography](https://github.com/pyca/cryptography) package). The hashing functions `B333SHA-256`b and `B333SHA-512`b are provided by the standard Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `B333HKDF`b, `B333HMAC`b, `B333Token`b primitives, and the `B333PKCS7`b padding function are always provided by the following internal implementations:
|
||||
In the default installation configuration, the `B333X25519`b, `B333Ed25519`b, and `B333AES-256-CBC`b primitives are provided by `_`!`[OpenSSL`https://www.openssl.org/]`!`_ (via the `_`!`[PyCA/cryptography`https://github.com/pyca/cryptography]`!`_ package). The hashing functions `B333SHA-256`b and `B333SHA-512`b are provided by the standard Python `_`!`[hashlib`https://docs.python.org/3/library/hashlib.html]`!`_. The `B333HKDF`b, `B333HMAC`b, `B333Token`b primitives, and the `B333PKCS7`b padding function are always provided by the following internal implementations:
|
||||
|
||||
• [HKDF.py](RNS/Cryptography/HKDF.py)
|
||||
• [HMAC.py](RNS/Cryptography/HMAC.py)
|
||||
• [Token.py](RNS/Cryptography/Token.py)
|
||||
• [PKCS7.py](RNS/Cryptography/PKCS7.py)
|
||||
• `_`!`[HKDF.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/HKDF.py]`!`_
|
||||
• `_`!`[HMAC.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/HMAC.py]`!`_
|
||||
• `_`!`[Token.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/Token.py]`!`_
|
||||
• `_`!`[PKCS7.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/PKCS7.py]`!`_
|
||||
|
||||
Reticulum also includes a complete implementation of all necessary primitives in pure Python. If OpenSSL and PyCA are not available on the system when Reticulum is started, Reticulum will instead use the internal pure-python primitives. A trivial consequence of this is performance, with the OpenSSL backend being *much* faster. The most important consequence however, is the potential loss of security by using primitives that has not seen the same amount of scrutiny, testing and review as those from OpenSSL.
|
||||
Reticulum also includes a complete implementation of all necessary primitives in pure Python. If OpenSSL and PyCA are not available on the system when Reticulum is started, Reticulum will instead use the internal pure-python primitives. A trivial consequence of this is performance, with the OpenSSL backend being `*much`* faster. The most important consequence however, is the potential loss of security by using primitives that has not seen the same amount of scrutiny, testing and review as those from OpenSSL.
|
||||
|
||||
Please note that by default, installing Reticulum will **require** OpenSSL and PyCA to also be automatically installed if not already available. It is only possible to use the pure-python primitives if this requirement is specifically overridden by the user, for example by installing the `B333rnspure`b package instead of the normal `B333rns`b package, or by running directly from local source-code.
|
||||
Please note that by default, installing Reticulum will `!require`! OpenSSL and PyCA to also be automatically installed if not already available. It is only possible to use the pure-python primitives if this requirement is specifically overridden by the user, for example by installing the `B333rnspure`b package instead of the normal `B333rns`b package, or by running directly from local source-code.
|
||||
|
||||
If you want to use the internal pure-python primitives, it is **highly advisable** that you have a good understanding of the risks that this pose, and make an informed decision on whether those risks are acceptable to you.
|
||||
If you want to use the internal pure-python primitives, it is `!highly advisable`! that you have a good understanding of the risks that this pose, and make an informed decision on whether those risks are acceptable to you.
|
||||
|
||||
Reticulum is relatively young software, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy or security breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.
|
||||
|
||||
>> Acknowledgements & Credits
|
||||
|
||||
Reticulum can only exist because of the mountain of Open Source work it was built on top of, the contributions of everyone involved, and everyone that has supported the project through the years. To everyone who has helped, thank you so much.
|
||||
|
||||
A number of other modules and projects are either part of, or used by Reticulum. Sincere thanks to the authors and contributors of the following projects:
|
||||
|
||||
• [PyCA/cryptography](https://github.com/pyca/cryptography), *BSD License*
|
||||
• [Pure-25519](https://github.com/warner/python-pure25519) by [Brian Warner](https://github.com/warner), *MIT License*
|
||||
• [Pysha2](https://github.com/thomdixon/pysha2) by [Thom Dixon](https://github.com/thomdixon), *MIT License*
|
||||
• [Python AES-128](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License*
|
||||
• [Python AES-256](https://github.com/boppreh/aes) by [BoppreH](https://github.com/boppreh), *MIT License*
|
||||
• [Curve25519.py](https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3#file-curve25519-py) by [Nicko van Someren](https://gist.github.com/nickovs), *Public Domain*
|
||||
• [I2Plib](https://github.com/l-n-s/i2plib) by [Viktor Villainov](https://github.com/l-n-s)
|
||||
• [PySerial](https://github.com/pyserial/pyserial) by Chris Liechti, *BSD License*
|
||||
• [Configobj](https://github.com/DiffSK/configobj) by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, *BSD License*
|
||||
• [ifaddr](https://github.com/pydron/ifaddr) by Stefan C. Mueller, *MIT License*
|
||||
• [Umsgpack.py](https://github.com/vsergeev/u-msgpack-python) by [Ivan A. Sergeev](https://github.com/vsergeev)
|
||||
• [rnsh](https://github.com/acehoss/rnsh) by [Aaron Heise](https://github.com/acehoss)
|
||||
• [Python](https://www.python.org)
|
||||
• `_`!`[PyCA/cryptography`https://github.com/pyca/cryptography]`!`_, `*BSD License`*
|
||||
• `_`!`[Pure-25519`https://github.com/warner/python-pure25519]`!`_, by `_`!`[Brian Warner`https://github.com/warner]`!`_, `*MIT License`*
|
||||
• `_`!`[Pysha2`https://github.com/thomdixon/pysha2]`!`_ by `_`!`[Thom Dixon`https://github.com/thomdixon]`!`_, `*MIT License`*
|
||||
• `_`!`[Python AES-128`https://github.com/orgurar/python-aes]`!`_ by `_`!`[Or Gur Arie`https://github.com/orgurar]`!`_, `*MIT License`*
|
||||
• `_`!`[Python AES-256`https://github.com/boppreh/aes]`!`_ by `_`!`[BoppreH`https://github.com/boppreh]`!`_, `*MIT License`*
|
||||
• `_`!`[Curve25519.py`https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3]`!`_ by `_`!`[Nicko van Someren`https://gist.github.com/nickovs]`!`_, `*Public Domain`*
|
||||
• `_`!`[I2Plib`https://github.com/l-n-s/i2plib]`!`_ by `_`!`[Viktor Villainov`https://github.com/l-n-s]`!`_
|
||||
• `_`!`[PySerial`https://github.com/pyserial/pyserial]`!`_ by Chris Liechti, `*BSD License`*
|
||||
• `_`!`[Configobj`https://github.com/DiffSK/configobj]`!`_ by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, `*BSD License`*
|
||||
• `_`!`[ifaddr`https://github.com/pydron/ifaddr]`!`_ by Stefan C. Mueller, `*MIT License`*
|
||||
• `_`!`[Umsgpack.py`https://github.com/vsergeev/u-msgpack-python]`!`_ by `_`!`[Ivan A. Sergeev`https://github.com/vsergeev]`!`_
|
||||
• `_`!`[rnsh`https://github.com/acehoss/rnsh]`!`_ by `_`!`[Aaron Heise`https://github.com/acehoss]`!`_
|
||||
• `_`!`[Python`https://www.python.org]`!`_
|
||||
|
||||
+1
-1
@@ -576,7 +576,7 @@ class InterfaceDiscovery():
|
||||
|
||||
def teardown_interface(self, interface):
|
||||
interface.detach()
|
||||
if interface in RNS.Transport.interfaces: RNS.Transport.interfaces.remove(interface)
|
||||
RNS.Transport.remove_interface(interface)
|
||||
if interface in self.monitored_interfaces: self.monitored_interfaces.remove(interface)
|
||||
|
||||
def autoconnect_count(self):
|
||||
|
||||
@@ -574,7 +574,7 @@ class AutoInterface(Interface):
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
if addr in self.spawned_interfaces:
|
||||
self.spawned_interfaces[addr].detach()
|
||||
self.spawned_interfaces[addr].teardown()
|
||||
@@ -666,7 +666,7 @@ class AutoInterfacePeer(Interface):
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not remove {self} from parent interface on detach. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
if self in RNS.Transport.interfaces: RNS.Transport.interfaces.remove(self)
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
class AutoInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
|
||||
@@ -258,7 +258,7 @@ class BackboneInterface(Interface):
|
||||
@staticmethod
|
||||
def deregister_fileno(fileno):
|
||||
if fileno < 0:
|
||||
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_WARNING)
|
||||
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
try: BackboneInterface.epoll.unregister(fileno)
|
||||
@@ -454,7 +454,7 @@ class BackboneInterface(Interface):
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
BackboneInterface.add_client_socket(socket, spawned_interface)
|
||||
@@ -769,9 +769,8 @@ class BackboneClientInterface(Interface):
|
||||
while self in self.parent_interface.spawned_interfaces:
|
||||
self.parent_interface.spawned_interfaces.remove(self)
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
if not self.initiator:
|
||||
RNS.Transport.interfaces.remove(self)
|
||||
if not self.initiator:
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -826,9 +826,8 @@ class I2PInterfacePeer(Interface):
|
||||
while self in self.parent_interface.spawned_interfaces:
|
||||
self.parent_interface.spawned_interfaces.remove(self)
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
if not self.initiator:
|
||||
RNS.Transport.interfaces.remove(self)
|
||||
if not self.initiator:
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
@@ -940,7 +939,7 @@ class I2PInterface(Interface):
|
||||
peer_interface.IN = True
|
||||
peer_interface.parent_interface = self
|
||||
peer_interface.parent_count = False
|
||||
RNS.Transport.interfaces.append(peer_interface)
|
||||
RNS.Transport.add_interface(peer_interface)
|
||||
|
||||
def incoming_connection(self, handler):
|
||||
RNS.log("Accepting incoming I2P connection", RNS.LOG_VERBOSE)
|
||||
@@ -993,7 +992,7 @@ class I2PInterface(Interface):
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
RNS.log("Spawned new I2PInterface Peer: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
while spawned_interface in self.spawned_interfaces:
|
||||
self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
|
||||
@@ -347,8 +347,7 @@ class LocalClientInterface(Interface):
|
||||
self.OUT = False
|
||||
self.IN = False
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
RNS.Transport.interfaces.remove(self)
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
if self in RNS.Transport.local_client_interfaces:
|
||||
RNS.Transport.local_client_interfaces.remove(self)
|
||||
@@ -458,7 +457,7 @@ class LocalServerInterface(Interface):
|
||||
spawned_interface.socket_path = self.socket_path
|
||||
|
||||
if hasattr(self, "_force_bitrate"): spawned_interface._force_bitrate = self._force_bitrate
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
RNS.Transport.local_client_interfaces.append(spawned_interface)
|
||||
BackboneInterface.add_client_socket(client_socket, spawned_interface)
|
||||
self.clients += 1
|
||||
@@ -474,7 +473,7 @@ class LocalServerInterface(Interface):
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
if hasattr(self, "_force_bitrate"): spawned_interface._force_bitrate = self._force_bitrate
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
RNS.Transport.local_client_interfaces.append(spawned_interface)
|
||||
self.clients += 1
|
||||
spawned_interface.read_loop()
|
||||
|
||||
@@ -375,7 +375,7 @@ class RNodeMultiInterface(Interface):
|
||||
interface.mode = self.mode
|
||||
interface.HW_MTU = self.HW_MTU
|
||||
interface.detected = True
|
||||
RNS.Transport.interfaces.append(interface)
|
||||
RNS.Transport.add_interface(interface)
|
||||
RNS.log("Spawned new RNode subinterface: "+str(interface), RNS.LOG_VERBOSE)
|
||||
|
||||
self.clients += 1
|
||||
@@ -909,8 +909,7 @@ class RNodeMultiInterface(Interface):
|
||||
def teardown_subinterfaces(self):
|
||||
for interface in self.subinterfaces:
|
||||
if interface != 0:
|
||||
if interface in RNS.Transport.interfaces:
|
||||
RNS.Transport.interfaces.remove(interface)
|
||||
RNS.Transport.remove_interface(interface)
|
||||
self.subinterfaces[interface.index] = 0
|
||||
|
||||
def should_ingress_limit(self):
|
||||
|
||||
@@ -436,9 +436,8 @@ class TCPClientInterface(Interface):
|
||||
while self in self.parent_interface.spawned_interfaces:
|
||||
self.parent_interface.spawned_interfaces.remove(self)
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
if not self.initiator:
|
||||
RNS.Transport.interfaces.remove(self)
|
||||
if not self.initiator:
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
@@ -627,7 +626,7 @@ class TCPServerInterface(Interface):
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.log("Spawned new TCPClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
while spawned_interface in self.spawned_interfaces:
|
||||
self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
|
||||
@@ -981,7 +981,7 @@ class WeaveInterface(Interface):
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface._online = True
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
if endpoint_addr in self.spawned_interfaces:
|
||||
self.spawned_interfaces[endpoint_addr].detach()
|
||||
self.spawned_interfaces[endpoint_addr].teardown()
|
||||
@@ -1097,5 +1097,4 @@ class WeaveInterfacePeer(Interface):
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not remove {self} from parent interface on detach. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
RNS.Transport.interfaces.remove(self)
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
+40
-20
@@ -402,7 +402,7 @@ class Reticulum:
|
||||
RNS.log("Existing shared instance required, but this instance started as shared instance. Aborting startup.", RNS.LOG_VERBOSE)
|
||||
|
||||
else:
|
||||
RNS.Transport.interfaces.append(interface)
|
||||
RNS.Transport.add_interface(interface)
|
||||
self.shared_instance_interface = interface
|
||||
self.is_shared_instance = True
|
||||
RNS.log("Started shared instance interface: "+str(interface), RNS.LOG_DEBUG)
|
||||
@@ -422,7 +422,7 @@ class Reticulum:
|
||||
interface._force_bitrate = True
|
||||
RNS.log(f"Forcing shared instance bitrate of {RNS.prettyspeed(interface.bitrate)}", RNS.LOG_WARNING)
|
||||
interface.optimise_mtu()
|
||||
RNS.Transport.interfaces.append(interface)
|
||||
RNS.Transport.add_interface(interface)
|
||||
self.is_shared_instance = False
|
||||
self.is_standalone_instance = False
|
||||
self.is_connected_to_shared_instance = True
|
||||
@@ -915,7 +915,7 @@ class Reticulum:
|
||||
interface.ifac_identity = RNS.Identity.from_bytes(interface.ifac_key)
|
||||
interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key))
|
||||
|
||||
RNS.Transport.interfaces.append(interface)
|
||||
RNS.Transport.add_interface(interface)
|
||||
interface.final_init()
|
||||
|
||||
interface = None
|
||||
@@ -1077,7 +1077,7 @@ class Reticulum:
|
||||
interface.ifac_identity = RNS.Identity.from_bytes(interface.ifac_key)
|
||||
interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key))
|
||||
|
||||
RNS.Transport.interfaces.append(interface)
|
||||
RNS.Transport.add_interface(interface)
|
||||
interface.final_init()
|
||||
|
||||
def _default_ar_target(self):
|
||||
@@ -1234,28 +1234,43 @@ class Reticulum:
|
||||
|
||||
def _used_destination_data(self, destination_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "used", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
try:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "used", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Shared instance RPC failed while setting destination data use: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
else: return RNS.Identity._used_destination_data(destination_hash)
|
||||
|
||||
def _retain_destination_data(self, destination_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "retain", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
try:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "retain", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Shared instance RPC failed while retaining destination data: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
else: return RNS.Identity._retain_destination_data(destination_hash)
|
||||
|
||||
def _unretain_destination_data(self, destination_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "unretain", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
try:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "unretain", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Shared instance RPC failed while unretaining destination data: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
else: return RNS.Identity._unretain_destination_data(destination_hash)
|
||||
|
||||
@@ -1264,10 +1279,15 @@ class Reticulum:
|
||||
raise TypeError("Cannot retain identity, not a valid identity hash")
|
||||
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"identity_data": "retain", "identity_hash": identity_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
try:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"identity_data": "retain", "identity_hash": identity_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Shared instance RPC failed while retaining identity: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
else: return RNS.Identity._retain_identity(identity_hash)
|
||||
|
||||
|
||||
+23
-15
@@ -417,6 +417,18 @@ class Transport:
|
||||
|
||||
gc.collect()
|
||||
|
||||
@staticmethod
|
||||
def add_interface(interface):
|
||||
with Transport.interfaces_lock:
|
||||
if not interface in Transport.interfaces:
|
||||
Transport.interfaces.append(interface)
|
||||
|
||||
@staticmethod
|
||||
def remove_interface(interface):
|
||||
with Transport.interfaces_lock:
|
||||
if interface in Transport.interfaces:
|
||||
Transport.interfaces.remove(interface)
|
||||
|
||||
@staticmethod
|
||||
def set_network_identity(identity):
|
||||
if not Transport.network_identity:
|
||||
@@ -1594,7 +1606,7 @@ class Transport:
|
||||
RNS.log(f"Clamping link MTU to {RNS.prettysize(path_mtu)}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu
|
||||
except Exception as e:
|
||||
RNS.log(f"Dropping link request packet. The contained exception was: {e}", RNS.LOG_WARNING)
|
||||
RNS.log(f"Dropping link request packet. The contained exception was: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return
|
||||
|
||||
# Entry format is
|
||||
@@ -2187,12 +2199,9 @@ class Transport:
|
||||
else:
|
||||
RNS.log("Invalid link request proof in transport for link "+RNS.prettyhexrep(packet.destination_hash)+", dropping proof.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not transport link request proof. The contained exception was: "+str(e), RNS.LOG_WARNING)
|
||||
else:
|
||||
RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
else:
|
||||
RNS.log("Received link request proof with hop mismatch, not transporting it", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
except Exception as e: RNS.log("Could not transport link request proof. The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(LOG_DEBUG) else None
|
||||
else: RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
else: RNS.log("Received link request proof with hop mismatch, not transporting it", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
else:
|
||||
# Check if we can deliver it to a local
|
||||
@@ -2521,16 +2530,17 @@ class Transport:
|
||||
def clean_announce_cache():
|
||||
st = time.time()
|
||||
target_path = os.path.join(RNS.Reticulum.cachepath, "announces")
|
||||
with Transport.path_table_lock: active_paths = [Transport.path_table[dst_hash][6] for dst_hash in Transport.path_table]
|
||||
with Transport.tunnels_lock: tunnel_paths = list(set([path_dict[dst_hash][6] for path_dict in [Transport.tunnels[tunnel_id][2] for tunnel_id in Transport.tunnels] for dst_hash in path_dict]))
|
||||
cached_announce_hashes = os.listdir(target_path)
|
||||
with Transport.path_table_lock: active_path_hashes = list(set([Transport.path_table[dst_hash][IDX_PT_PACKET] for dst_hash in Transport.path_table]))
|
||||
with Transport.tunnels_lock: tunnel_path_hashes = list(set([path_dict[dst_hash][IDX_PT_PACKET] for path_dict in [Transport.tunnels[tunnel_id][IDX_TT_PATHS] for tunnel_id in Transport.tunnels] for dst_hash in path_dict]))
|
||||
removed = 0; total = 0
|
||||
for packet_hash in os.listdir(target_path):
|
||||
for packet_hash in cached_announce_hashes:
|
||||
remove = False
|
||||
full_path = os.path.join(target_path, packet_hash)
|
||||
if os.path.isfile(full_path):
|
||||
try: target_hash = bytes.fromhex(packet_hash)
|
||||
except: remove = True
|
||||
if (not target_hash in active_paths) and (not target_hash in tunnel_paths): remove = True
|
||||
if (not target_hash in active_path_hashes) and (not target_hash in tunnel_path_hashes): remove = True
|
||||
if remove: os.unlink(full_path); removed += 1
|
||||
total += 1
|
||||
|
||||
@@ -2892,10 +2902,8 @@ class Transport:
|
||||
tag=tag_bytes)
|
||||
|
||||
else: RNS.log("Ignoring duplicate path request for "+RNS.prettyhexrep(destination_hash)+" with tag "+RNS.prettyhexrep(unique_tag), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
|
||||
else: RNS.log("Ignoring tagless path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
except Exception as e: RNS.log("Error while handling path request. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"Error while handling path request. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
@staticmethod
|
||||
def path_request(destination_hash, is_from_local_client, attached_interface, requestor_transport_id=None, tag=None):
|
||||
@@ -2936,7 +2944,7 @@ class Transport:
|
||||
received_from = Transport.path_table[destination_hash][IDX_PT_RVCD_IF]
|
||||
|
||||
if packet == None:
|
||||
RNS.log("Could not retrieve announce packet from cache while answering path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_WARNING)
|
||||
RNS.log(f"Could not retrieve announce packet from cache while answering path request for {RNS.prettyhexrep(destination_hash)}, ignoring path request", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
elif attached_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ROAMING and attached_interface == received_from:
|
||||
RNS.log("Not answering path request on roaming-mode interface, since next hop is on same roaming-mode interface", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
@@ -163,11 +163,11 @@ def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
RNS.log(f"Could not apply allowed identity: {e}", RNS.LOG_ERROR)
|
||||
RNS.exit(1)
|
||||
|
||||
if len(allowed_identity_hashes) < 1 and not disable_auth:
|
||||
print("Warning: No allowed identities configured, rncp will not accept any files!")
|
||||
RNS.log("No allowed identities configured, rncp will not accept any files!", RNS.LOG_WARNING)
|
||||
|
||||
def fetch_request(path, data, request_id, link_id, remote_identity, requested_at):
|
||||
global allow_fetch, fetch_jail, fetch_auto_compress
|
||||
@@ -217,7 +217,7 @@ def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed
|
||||
else:
|
||||
destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_LIST, allowed_list=allowed_identity_hashes)
|
||||
|
||||
print("rncp listening on "+RNS.prettyhexrep(destination.hash))
|
||||
RNS.log("rncp listening on "+RNS.prettyhexrep(destination.hash), RNS.LOG_INFO)
|
||||
|
||||
if announce >= 0:
|
||||
def job():
|
||||
@@ -271,15 +271,15 @@ def receive_resource_started(resource):
|
||||
else:
|
||||
id_str = ""
|
||||
|
||||
print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str)
|
||||
RNS.log("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str, RNS.LOG_INFO)
|
||||
|
||||
def receive_resource_concluded(resource):
|
||||
global save_path, allow_overwrite_on_receive
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
print(str(resource)+" completed")
|
||||
RNS.log(f"Incoming resource {resource} completed", RNS.LOG_INFO)
|
||||
|
||||
if resource.metadata == None:
|
||||
print("Invalid data received, ignoring resource")
|
||||
RNS.log("Invalid data received, ignoring resource", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
else:
|
||||
@@ -306,13 +306,14 @@ def receive_resource_concluded(resource):
|
||||
full_save_path = saved_filename+"."+str(counter)
|
||||
|
||||
shutil.move(resource.data.name, full_save_path)
|
||||
RNS.log("Saved received file to "+full_save_path, RNS.LOG_NOTICE)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while saving received resource: {e}", RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
else:
|
||||
print("Resource failed")
|
||||
RNS.log("Resource failed", RNS.LOG_INFO)
|
||||
|
||||
resource_done = False
|
||||
current_resource = None
|
||||
|
||||
@@ -101,6 +101,7 @@ class ReticulumGitClient():
|
||||
self.config = None
|
||||
self.ready = False
|
||||
|
||||
self.destination_aliases = {}
|
||||
self.remote_identity = None
|
||||
self.destination = None
|
||||
self.link = None
|
||||
@@ -170,6 +171,18 @@ class ReticulumGitClient():
|
||||
section = self.config["client"]
|
||||
if "ref_batch_size" in section: self.ref_batch_size = max(0, min(1024, section.as_int("ref_batch_size")))
|
||||
|
||||
if "aliases" in self.config:
|
||||
section = self.config["aliases"]
|
||||
for alias in section:
|
||||
alias_hexhash = section[alias]
|
||||
len_ok = len(alias_hexhash) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2
|
||||
try: alias_hash = bytes.fromhex(alias_hexhash)
|
||||
except: alias_hash = None
|
||||
alias_exists = alias in self.destination_aliases
|
||||
if not len_ok or not alias_hash: continue
|
||||
if alias_exists: continue
|
||||
self.destination_aliases[alias] = RNS.hexrep(alias_hash, delimit=False)
|
||||
|
||||
if not os.path.isfile(self.identitypath):
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(self.identitypath)
|
||||
@@ -185,6 +198,19 @@ class ReticulumGitClient():
|
||||
|
||||
else: self.identity = identity
|
||||
|
||||
self.destination_hexhash = self.__resolve_destination_alias(self.destination_hexhash)
|
||||
|
||||
def __resolve_destination_alias(self, alias):
|
||||
def resolve(alias):
|
||||
len_match = len(alias) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2
|
||||
try: hash_bytes = bytes.fromhex(alias)
|
||||
except: hash_bytes = None
|
||||
if len_match and hash_bytes: return alias
|
||||
else: return self.destination_aliases[alias] if alias in self.destination_aliases else alias
|
||||
|
||||
resolved = resolve(alias)
|
||||
return resolved
|
||||
|
||||
def abort(self, reason=None, code=255):
|
||||
if not reason: reason = "Unknown reason"
|
||||
print(f"git-remote-rns failed: {reason}", file=sys.stderr)
|
||||
@@ -656,6 +682,21 @@ __default_rngit_config__ = '''# This is the default rngit client config file.
|
||||
|
||||
ref_batch_size = 25
|
||||
|
||||
|
||||
[aliases]
|
||||
|
||||
# You can define aliases for commonly used destination
|
||||
# hashes in this section. Each line must be in the format
|
||||
# aliased_name = DESTINATION_HASH
|
||||
#
|
||||
# These hashes are used for resolving remote destinations.
|
||||
# For rngit node permissions and identity resolution,
|
||||
# aliases must be defined in ~/.rngit/config.
|
||||
|
||||
# my_node = 063d38912bffc850af4a1b8a270a9d85
|
||||
# bobs_node = 714981d03e41deda0e4468cb274414cc
|
||||
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# 0: Log only critical information
|
||||
|
||||
+326
-102
@@ -98,6 +98,19 @@ class NomadNetworkNode():
|
||||
CLR_FILE = "`F66d"
|
||||
CLR_DIM = "`F666"
|
||||
CLR_DIM_H = "`F444"
|
||||
CLR_OK_DIM = "`FT537855"
|
||||
CLR_DIFF_A = "`F0a0"
|
||||
CLR_DIFF_R = "`F900"
|
||||
CLR_DIFF_P = "`F0aa"
|
||||
|
||||
RCLR_PUSH = "B9A810"
|
||||
RCLR_PUSH_G = "791212"
|
||||
RCLR_FETCH = "10b981"
|
||||
RCLR_FETCH_G = "1c5e71"
|
||||
RCLR_VIEW = "3b82f6"
|
||||
RCLR_VIEW_G = "13428A"
|
||||
RCLR_DOWNLOAD = "7831E0"
|
||||
RCLR_DOWNLOAD_G = "c5754d"
|
||||
|
||||
# Yes, I'm being intentionally weird here. If you
|
||||
# want to use tabs, three spaces is all you get.
|
||||
@@ -208,6 +221,13 @@ class NomadNetworkNode():
|
||||
if not remote_identity: remote_identity = self.null_ident
|
||||
return self.owner.resolve_permission(remote_identity, group_name, repository_name, permission)
|
||||
|
||||
def resolve_doc_permission(self, remote_identity, group_name, repository_name, doc_id, permission):
|
||||
# Since the nomadnet page protocol doesn't *require* authentication,
|
||||
# we use null_ident in case the remote hasn't identified.
|
||||
if not remote_identity: remote_identity = self.null_ident
|
||||
|
||||
return self.owner.resolve_doc_permission(remote_identity, group_name, repository_name, doc_id, permission)
|
||||
|
||||
def register_request_handlers(self):
|
||||
self.destination.register_request_handler(self.PATH_INDEX, response_generator=self.serve_front_page, allow=RNS.Destination.ALLOW_ALL)
|
||||
self.destination.register_request_handler(self.PATH_GROUP, response_generator=self.serve_group_page, allow=RNS.Destination.ALLOW_ALL)
|
||||
@@ -294,6 +314,16 @@ class NomadNetworkNode():
|
||||
field_str = "`" + "|".join(field_parts)
|
||||
return f"`[{sanitize_label(_label)}`:{_path}{field_str}]"
|
||||
|
||||
def m_link_e(self, _label, remote, _path, **fields):
|
||||
def sanitize_v(value): return urllib.parse.quote_plus(str(value).encode("utf-8"))
|
||||
def sanitize_label(value): return value.replace("[", "").replace("]", "").replace("`", "")
|
||||
field_str = ""
|
||||
if fields:
|
||||
field_parts = []
|
||||
for k, v in fields.items(): field_parts.append(f"{k}={sanitize_v(v)}")
|
||||
field_str = "`" + "|".join(field_parts)
|
||||
return f"`!`[{sanitize_label(_label)}`{remote}:{_path}{field_str}]`!"
|
||||
|
||||
def m_link(self, _label, _path, **fields):
|
||||
def sanitize_v(value): return urllib.parse.quote_plus(str(value).encode("utf-8"))
|
||||
def sanitize_label(value): return value.replace("[", "").replace("]", "").replace("`", "")
|
||||
@@ -410,7 +440,35 @@ class NomadNetworkNode():
|
||||
if not repo:
|
||||
content = self.m_heading("Not Found", 1) + "\nThe requested repository was not found.\n"
|
||||
return self.render_template(content, nav_content="".join(nav_parts), st=st)
|
||||
|
||||
|
||||
repo_source = ""; source_link = None
|
||||
if repo["fork"] or repo["mirror"]:
|
||||
if repo["fork"]: source_type = "fork"; source_url = repo["fork"]
|
||||
elif repo["mirror"]: source_type = "mirror"; source_url = repo["mirror"]
|
||||
else: source_type = "retriev"; source_url = "unknown source"
|
||||
if not source_url.lower().startswith("rns://"): source_link = ""
|
||||
else:
|
||||
try:
|
||||
url_components = source_url.split("/")
|
||||
if len(url_components) == 5 and len(url_components[2]) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2:
|
||||
source_repo_dest = bytes.fromhex(url_components[2])
|
||||
source_group_name = url_components[3]
|
||||
source_repo_name = url_components[4]
|
||||
source_identity = RNS.Identity.recall(source_repo_dest)
|
||||
if source_identity:
|
||||
source_page_dest = RNS.Destination.hash_from_name_and_identity("nomadnetwork.node", source_identity)
|
||||
mu_link = self.m_link_e(source_url, RNS.hexrep(source_page_dest, delimit=False), self.PATH_REPO, g=source_group_name, r=source_repo_name)
|
||||
source_link = f"{mu_link}"
|
||||
except Exception as e: source_link = ""
|
||||
|
||||
synced_ago = max(0, time.time()-self.owner.last_upstream_sync(repo["path"]))
|
||||
sync_time = RNS.prettytime(synced_ago, compact=True).split(" ")[0]
|
||||
sync_str = f" `*{self.CLR_DIM_H}synced {sync_time} ago`f`*\n"
|
||||
source_desc = f"{source_type}ed from"
|
||||
source_indent = " "*(len(f"Node / {group_name} / {repo_name}")-len(source_desc))
|
||||
if source_link: source_url = source_link
|
||||
nav_parts.append(f"{self.CLR_DIM}{source_desc.capitalize()}{source_indent} {source_url}`f{sync_str}\n")
|
||||
|
||||
description = self.get_repository_description(repo["path"])
|
||||
if description: description = f"{description}\n\n"
|
||||
else: description = ""
|
||||
@@ -434,7 +492,7 @@ class NomadNetworkNode():
|
||||
# Get releases count
|
||||
releases_path = f"{repo['path']}.releases"
|
||||
releases_count = 0
|
||||
releases = self.owner.releases_list_data(releases_path)
|
||||
releases, latest_release = self.owner.releases_list_data(releases_path)
|
||||
if releases: releases_count = len([r for r in releases if r.get("status") == "published"])
|
||||
|
||||
sep = self.icon("sep")
|
||||
@@ -461,7 +519,7 @@ class NomadNetworkNode():
|
||||
converted = mdc.format_block(readme_content)
|
||||
content_parts.append(converted)
|
||||
|
||||
else: content_parts.append(f"\n{readme_content}")
|
||||
else: content_parts.append(f"\n{readme_content.rstrip()}\n")
|
||||
|
||||
else:
|
||||
content_parts.append(self.m_divider())
|
||||
@@ -647,6 +705,7 @@ class NomadNetworkNode():
|
||||
content = self.m_heading("Invalid Path", 1) + "\n\nNo file path specified.\n"
|
||||
return self.render_template(content, st=st)
|
||||
|
||||
file_path = file_path.lstrip("./").replace("/./", "/")
|
||||
file_ext = os.path.splitext(file_path)[1].lower()
|
||||
renderable = file_ext in self.RENDERABLE_EXTS
|
||||
if not renderable: raw = True; render = False
|
||||
@@ -689,6 +748,10 @@ class NomadNetworkNode():
|
||||
|
||||
if blob_info is None: content_parts.append("File not found at this ref.\n")
|
||||
else:
|
||||
# Redirect to tree page if this is a tree
|
||||
if blob_info.get("is_tree", None) == True:
|
||||
return self.serve_tree_page(path, data, request_id, link_id, remote_identity, requested_at)
|
||||
|
||||
size = blob_info.get("size", 0)
|
||||
is_binary = blob_info.get("is_binary", False)
|
||||
is_symlink = blob_info.get("is_symlink", False)
|
||||
@@ -1103,40 +1166,56 @@ class NomadNetworkNode():
|
||||
content_parts.append(self.m_heading(f"Stats for {repo_name}", 2))
|
||||
|
||||
v_total = stats["views"]["total"]
|
||||
v_peak = stats["views"]["peak"]
|
||||
v_peak = stats["views"]["peak"]
|
||||
v_tday = stats["views"]["daily"][-1] if len(stats["views"]["daily"]) else 0
|
||||
|
||||
f_total = stats["fetches"]["total"]
|
||||
f_peak = stats["fetches"]["peak"]
|
||||
p_total = stats["pushes"]["total"]
|
||||
p_peak = stats["pushes"]["peak"]
|
||||
f_peak = stats["fetches"]["peak"]
|
||||
f_tday = stats["fetches"]["daily"][-1] if len(stats["fetches"]["daily"]) else 0
|
||||
|
||||
content_parts.append(f"\n`F66dViews`f : {v_total:>5} total {self.CLR_DIM}(peak: {v_peak:>3})`f\n")
|
||||
content_parts.append(f"`F0a0Fetches`f : {f_total:>5} total {self.CLR_DIM}(peak: {f_peak:>3})\n`f")
|
||||
content_parts.append(f"`Faa0Pushes`f : {p_total:>5} total {self.CLR_DIM}(peak: {p_peak:>3})\n`f")
|
||||
content_parts.append(f"`F0aaActivity`f : {stats['activity_score']:>5} points\n\n")
|
||||
p_total = stats["pushes"]["total"]
|
||||
p_peak = stats["pushes"]["peak"]
|
||||
p_tday = stats["pushes"]["daily"][-1] if len(stats["pushes"]["daily"]) else 0
|
||||
|
||||
d_total = stats["downloads_combined"]["total"]
|
||||
d_peak = stats["downloads_combined"]["peak"]
|
||||
d_tday = stats["downloads_combined"]["daily"][-1] if len(stats["downloads_combined"]["daily"]) else 0
|
||||
|
||||
content_parts.append( f"\n`FT{self.RCLR_FETCH}Fetches`f : {f_total:>5} total {self.CLR_DIM} today: {f_tday:>3} peak: {f_peak:>3} \n`f")
|
||||
content_parts.append( f"`FT{self.RCLR_PUSH}Pushes`f : {p_total:>5} total {self.CLR_DIM} today: {p_tday:>3} peak: {p_peak:>3} \n`f")
|
||||
content_parts.append( f"`FT{self.RCLR_VIEW}Views`f : {v_total:>5} total {self.CLR_DIM} today: {v_tday:>3} peak: {v_peak:>3} `f\n")
|
||||
content_parts.append(f"`FT{self.RCLR_DOWNLOAD}Downloads`f : {d_total:>5} total {self.CLR_DIM} today: {d_tday:>3} peak: {d_peak:>3} `f\n")
|
||||
content_parts.append( f"`F0aaActivity`f : {stats['activity_score']:>5} points\n\n")
|
||||
content_parts.append(f"{act_color}{act_label}`f over the last {stats['actual_days']} days ({stats['date_range']})\n\n")
|
||||
|
||||
if v_total > 0:
|
||||
content_parts.append(self.m_heading(f"Views", 2))
|
||||
content_parts.append("\n")
|
||||
content_parts.append(self.render_chart(stats["views"]["daily"], stats["timeline_labels"], color="66d"))
|
||||
content_parts.append("\n")
|
||||
|
||||
|
||||
if f_total > 0:
|
||||
content_parts.append(self.m_heading(f"Fetches", 2))
|
||||
content_parts.append("\n")
|
||||
content_parts.append(self.render_chart(stats["fetches"]["daily"], stats["timeline_labels"], color="0a0"))
|
||||
content_parts.append(self.render_chart(stats["fetches"]["daily"], stats["timeline_labels"], color=self.RCLR_FETCH, secondary_color=self.RCLR_FETCH_G))
|
||||
content_parts.append("\n")
|
||||
|
||||
if p_total > 0:
|
||||
content_parts.append(self.m_heading(f"Pushes", 2))
|
||||
content_parts.append("\n")
|
||||
content_parts.append(self.render_chart(stats["pushes"]["daily"], stats["timeline_labels"], color="aa0"))
|
||||
content_parts.append(self.render_chart(stats["pushes"]["daily"], stats["timeline_labels"], color=self.RCLR_PUSH, secondary_color=self.RCLR_PUSH_G))
|
||||
content_parts.append("\n")
|
||||
|
||||
if v_total > 0:
|
||||
content_parts.append(self.m_heading(f"Views", 2))
|
||||
content_parts.append("\n")
|
||||
content_parts.append(self.render_chart(stats["views"]["daily"], stats["timeline_labels"], color=self.RCLR_VIEW, secondary_color=self.RCLR_VIEW_G))
|
||||
content_parts.append("\n")
|
||||
|
||||
if d_total > 0:
|
||||
content_parts.append(self.m_heading(f"Downloads", 2))
|
||||
content_parts.append("\n")
|
||||
content_parts.append(self.render_chart(stats["downloads_combined"]["daily"], stats["timeline_labels"], color=self.RCLR_DOWNLOAD, secondary_color=self.RCLR_DOWNLOAD_G, gradient_factor=1.7))
|
||||
content_parts.append("\n")
|
||||
|
||||
if stats["activity_score"] > 0:
|
||||
content_parts.append(self.m_heading("Combined Activity", 2))
|
||||
content_parts.append("\n")
|
||||
content_parts.append(self.render_combined_chart(stats["views"]["daily"], stats["fetches"]["daily"], stats["pushes"]["daily"], stats["timeline_labels"]))
|
||||
content_parts.append(self.render_combined_chart(stats["views"]["daily"], stats["fetches"]["daily"], stats["pushes"]["daily"], stats["downloads_combined"]["daily"], stats["timeline_labels"]))
|
||||
|
||||
else: content_parts.append(self.m_italic("\nNo development activity recorded for this repository in the selected time period.\n\n"))
|
||||
|
||||
@@ -1170,7 +1249,7 @@ class NomadNetworkNode():
|
||||
nav_content = "".join(nav_parts)
|
||||
|
||||
releases_path = f"{repo['path']}.releases"
|
||||
releases = self.owner.releases_list_data(releases_path)
|
||||
releases, latest_release = self.owner.releases_list_data(releases_path)
|
||||
if not releases:
|
||||
content_parts.append(self.m_heading("Releases", 2))
|
||||
content_parts.append("\nNo releases available for this repository.\n")
|
||||
@@ -1194,8 +1273,9 @@ class NomadNetworkNode():
|
||||
link = self.m_link(tag, self.PATH_RELEASE, g=group_name, r=repo_name, t=tag)
|
||||
|
||||
sep = self.icon("sep")
|
||||
latest_str = f" {sep} {self.CLR_OK_DIM}`*Latest`*`f" if tag == latest_release else ""
|
||||
artifacts_str = f"`*{artifacts} artifact{'s' if artifacts != 1 else ''}`*"
|
||||
content_parts.append(f"{link} {self.CLR_DIM}{date_str} {sep} {artifacts_str}`f\n")
|
||||
content_parts.append(f"{link} {self.CLR_DIM}{date_str} {sep} {artifacts_str}{latest_str}`f\n")
|
||||
if preview:
|
||||
if rel_format == "markdown": content_parts.append(f"{self.mdc.format_block(preview)}\n")
|
||||
elif rel_format == "micron": content_parts.append(f"{preview}\n")
|
||||
@@ -1226,13 +1306,16 @@ class NomadNetworkNode():
|
||||
|
||||
releases_path = f"{repo['path']}.releases"
|
||||
if tag == "latest":
|
||||
releases = self.owner.releases_list_data(releases_path)
|
||||
releases, latest_release = self.owner.releases_list_data(releases_path)
|
||||
if not releases:
|
||||
content = self.m_heading("Release Not Found", 2) + f"\nNo latest release exist.\n"
|
||||
content = self.m_heading("Release Not Found", 2) + f"\nNo releases exist.\n"
|
||||
return self.render_template(content, nav_content=nav_content, st=st)
|
||||
|
||||
recent_releases = sorted(releases, key=lambda x: x['created'], reverse=True)
|
||||
tag = recent_releases[0]["tag"]
|
||||
if not latest_release:
|
||||
recent_releases = sorted(releases, key=lambda x: x['created'], reverse=True)
|
||||
tag = recent_releases[0]["tag"]
|
||||
|
||||
else: tag = latest_release
|
||||
|
||||
content_parts = []
|
||||
nav_parts = []
|
||||
@@ -1312,7 +1395,7 @@ class NomadNetworkNode():
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
scope = data.get("var_scope", "active") if data else "active"
|
||||
if scope not in ["active", "completed", "all"]: scope = "active"
|
||||
if scope not in ["active", "completed", "proposed", "all"]: scope = "active"
|
||||
|
||||
if not group_name or not repo_name:
|
||||
content = self.m_heading("Error", 2) + "\nInvalid request\n"
|
||||
@@ -1335,16 +1418,18 @@ class NomadNetworkNode():
|
||||
sep = self.icon("sep")
|
||||
active_s = "`_" if scope == "active" else ""
|
||||
cmplt_s = "`_" if scope == "completed" else ""
|
||||
prpsd_s = "`_" if scope == "proposed" else ""
|
||||
all_s = "`_" if scope == "all" else ""
|
||||
filter_links = []
|
||||
filter_links.append(active_s+self.m_link("Active", self.PATH_WORK, g=group_name, r=repo_name, scope="active")+active_s)
|
||||
filter_links.append(cmplt_s+self.m_link("Completed", self.PATH_WORK, g=group_name, r=repo_name, scope="completed")+cmplt_s)
|
||||
filter_links.append(prpsd_s+self.m_link("Proposed", self.PATH_WORK, g=group_name, r=repo_name, scope="proposed")+prpsd_s)
|
||||
filter_links.append(all_s+self.m_link("All", self.PATH_WORK, g=group_name, r=repo_name, scope="all")+all_s)
|
||||
content_parts.append(f" {sep} ".join(filter_links) + "\n\n")
|
||||
|
||||
# Load work documents
|
||||
work_path = f"{repo['path']}.work"
|
||||
scopes_to_show = ["active", "completed"] if scope == "all" else [scope]
|
||||
scopes_to_show = ["active", "completed", "proposed"] if scope == "all" else [scope]
|
||||
|
||||
for s in scopes_to_show:
|
||||
folder_path = os.path.join(work_path, s)
|
||||
@@ -1356,6 +1441,9 @@ class NomadNetworkNode():
|
||||
if not os.path.isdir(doc_dir): continue
|
||||
try:
|
||||
doc_id = int(entry)
|
||||
read_access = self.resolve_doc_permission(remote_identity, group_name, repo_name, doc_id, self.owner.PERM_READ)
|
||||
if not read_access: continue
|
||||
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
if not os.path.isfile(root_path): continue
|
||||
|
||||
@@ -1407,7 +1495,7 @@ class NomadNetworkNode():
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
doc_id = data.get("var_id", "") if data else ""
|
||||
scope = data.get("var_scope", "all") if data else "all"
|
||||
if scope not in ["active", "completed", "all"]: scope = "active"
|
||||
if scope not in ["active", "completed", "proposed", "all"]: scope = "active"
|
||||
|
||||
if not group_name or not repo_name or not doc_id:
|
||||
content = self.m_heading("Error", 2) + "\nInvalid request\n"
|
||||
@@ -1423,20 +1511,31 @@ class NomadNetworkNode():
|
||||
content = self.m_heading("Error", 2) + "\nThe requested repository was not found\n"
|
||||
return self.render_template(content, st=st)
|
||||
|
||||
work_path = f"{repo['path']}.work"
|
||||
active_dir = os.path.join(work_path, "active", str(doc_id))
|
||||
read_access = self.resolve_doc_permission(remote_identity, group_name, repo_name, doc_id, self.owner.PERM_READ)
|
||||
if not read_access:
|
||||
content = self.m_heading("Error", 2) + "\nThe requested work document was not found\n"
|
||||
return self.render_template(content, st=st)
|
||||
|
||||
work_path = f"{repo['path']}.work"
|
||||
active_dir = os.path.join(work_path, "active", str(doc_id))
|
||||
completed_dir = os.path.join(work_path, "completed", str(doc_id))
|
||||
proposed_dir = os.path.join(work_path, "proposed", str(doc_id))
|
||||
if scope == "active": doc_dir = active_dir
|
||||
elif scope == "completed": doc_dir = completed_dir
|
||||
elif scope == "proposed": doc_dir = proposed_dir
|
||||
elif scope == "all":
|
||||
if os.path.isdir(active_dir):
|
||||
doc_dir = active_dir
|
||||
scope = "active"
|
||||
|
||||
else:
|
||||
elif os.path.isdir(completed_dir):
|
||||
doc_dir = completed_dir
|
||||
scope = "completed"
|
||||
|
||||
else:
|
||||
doc_dir = proposed_dir
|
||||
scope = "proposed"
|
||||
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
|
||||
if not os.path.isfile(root_path):
|
||||
@@ -1557,10 +1656,13 @@ class NomadNetworkNode():
|
||||
releases_path = f"{repo['path']}.releases"
|
||||
|
||||
if tag == "latest":
|
||||
releases = self.owner.releases_list_data(releases_path)
|
||||
releases, latest_release = self.owner.releases_list_data(releases_path)
|
||||
if not releases: return None
|
||||
recent_releases = sorted(releases, key=lambda x: x['created'], reverse=True)
|
||||
tag = recent_releases[0]["tag"]
|
||||
if not latest_release:
|
||||
recent_releases = sorted(releases, key=lambda x: x['created'], reverse=True)
|
||||
tag = recent_releases[0]["tag"]
|
||||
|
||||
else: tag = latest_release
|
||||
|
||||
release_dir = os.path.join(releases_path, tag)
|
||||
artifacts_dir = os.path.join(release_dir, "artifacts")
|
||||
@@ -1589,6 +1691,7 @@ class NomadNetworkNode():
|
||||
|
||||
RNS.log(f"Artifact file resolved for artifact request {group_name}/{repo_name}/{tag}/{artifact}", RNS.LOG_DEBUG)
|
||||
|
||||
self.owner.release_download_succeeded(group_name, repo_name, remote_identity)
|
||||
return [open(artifact_path, "rb"), {"name": artifact.encode("utf-8")}]
|
||||
|
||||
def serve_download(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||
@@ -1626,7 +1729,10 @@ class NomadNetworkNode():
|
||||
|
||||
else:
|
||||
stream = self.get_blob_stream(repo_path, resolved_ref, file_path)
|
||||
if stream is not None: return [stream, {"name": file_name.encode("utf-8")}]
|
||||
if stream is not None:
|
||||
self.owner.download_succeeded(group_name, repo_name, remote_identity)
|
||||
return [stream, {"name": file_name.encode("utf-8")}]
|
||||
|
||||
else:
|
||||
RNS.log(f"Could not resolve blob stream for download request {group_name}/{repo_name}/{ref}/{file_path}", RNS.LOG_WARNING)
|
||||
return None
|
||||
@@ -1650,28 +1756,40 @@ class NomadNetworkNode():
|
||||
|
||||
try: doc_id = int(doc_id)
|
||||
except:
|
||||
if not type(doc_id) == str or not type(doc_id) == bytes: doc_id = f"{doc_id}"
|
||||
RNS.log(f"Could not parse document ID for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
|
||||
return None
|
||||
|
||||
repo = self.get_accessible_repository(remote_identity, group_name, repo_name)
|
||||
if not repo:
|
||||
RNS.log(f"Repository not found or no access for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
|
||||
RNS.log(f"Repository not found or no access for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id}", RNS.LOG_WARNING)
|
||||
return None
|
||||
|
||||
work_path = f"{repo['path']}.work"
|
||||
active_dir = os.path.join(work_path, "active", str(doc_id))
|
||||
doc_access = self.resolve_doc_permission(remote_identity, group_name, repo_name, doc_id, self.owner.PERM_READ)
|
||||
if not doc_access:
|
||||
RNS.log(f"No access for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id}", RNS.LOG_WARNING)
|
||||
return None
|
||||
|
||||
work_path = f"{repo['path']}.work"
|
||||
active_dir = os.path.join(work_path, "active", str(doc_id))
|
||||
completed_dir = os.path.join(work_path, "completed", str(doc_id))
|
||||
proposed_dir = os.path.join(work_path, "proposed", str(doc_id))
|
||||
if scope == "active": doc_dir = active_dir
|
||||
elif scope == "completed": doc_dir = completed_dir
|
||||
elif scope == "proposed": doc_dir = proposed_dir
|
||||
elif scope == "all":
|
||||
if os.path.isdir(active_dir):
|
||||
doc_dir = active_dir
|
||||
scope = "active"
|
||||
|
||||
else:
|
||||
elif os.path.isdir(completed_dir):
|
||||
doc_dir = completed_dir
|
||||
scope = "completed"
|
||||
|
||||
else:
|
||||
doc_dir = proposed_dir
|
||||
scope = "proposed"
|
||||
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
|
||||
if not os.path.isfile(root_path):
|
||||
@@ -1691,6 +1809,7 @@ class NomadNetworkNode():
|
||||
if content:
|
||||
if fmt == "micron": file_name = f"{title}.mu"
|
||||
else: file_name = f"{title}.md"
|
||||
self.owner.download_succeeded(group_name, repo_name, remote_identity)
|
||||
return [file_name, content.encode("utf-8")]
|
||||
|
||||
return None
|
||||
@@ -1846,6 +1965,15 @@ class NomadNetworkNode():
|
||||
|
||||
size = int(result.stdout.strip())
|
||||
|
||||
# Check if it's a tree via ls-tree
|
||||
check_tree_path = f"{ref}:{file_path}" if file_path else ref
|
||||
result = subprocess.run(["git", "ls-tree", check_tree_path],
|
||||
cwd=repo_path, capture_output=True, text=True,
|
||||
timeout=self.GIT_COMMAND_TIMEOUT, check=False)
|
||||
|
||||
if result.returncode == 0: is_tree = True
|
||||
else: is_tree = False
|
||||
|
||||
# Check if it's a symlink via ls-tree
|
||||
parent_dir = "/".join(file_path.split("/")[:-1])
|
||||
filename = file_path.split("/")[-1]
|
||||
@@ -1894,6 +2022,7 @@ class NomadNetworkNode():
|
||||
if first_line.startswith("-"): is_binary = True
|
||||
|
||||
return { "size": size,
|
||||
"is_tree": is_tree,
|
||||
"is_binary": is_binary,
|
||||
"is_symlink": is_symlink,
|
||||
"symlink_target": symlink_target }
|
||||
@@ -2193,17 +2322,17 @@ class NomadNetworkNode():
|
||||
for line in lines:
|
||||
if line.startswith("+"):
|
||||
if line.startswith("+++"): formatted_lines.append(self.m_escape(line))
|
||||
else: formatted_lines.append(f"`F0a0{self.m_escape(line)}`f")
|
||||
else: formatted_lines.append(f"{self.CLR_DIFF_A}{self.m_escape(line)}`f")
|
||||
|
||||
elif line.startswith("-"):
|
||||
if line.startswith("---"): formatted_lines.append(self.m_escape(f"\\{line}"))
|
||||
else: formatted_lines.append(f"`F900{self.m_escape(line)}`f")
|
||||
else: formatted_lines.append(f"{self.CLR_DIFF_R}{self.m_escape(line)}`f")
|
||||
elif line.startswith("@@"):
|
||||
formatted_lines.append(f"`F0aa{self.m_escape(line)}`f")
|
||||
formatted_lines.append(f"{self.CLR_DIFF_P}{self.m_escape(line)}`f")
|
||||
|
||||
elif line.startswith("diff ") or line.startswith("index ") or line.startswith("new file") or line.startswith("deleted file"):
|
||||
if line.startswith("diff --git a"): formatted_lines.append("")
|
||||
formatted_lines.append(f"`F666{self.m_escape(line)}`f")
|
||||
formatted_lines.append(f"{self.CLR_DIM}{self.m_escape(line)}`f")
|
||||
|
||||
else: formatted_lines.append(self.m_escape(line))
|
||||
|
||||
@@ -2264,7 +2393,10 @@ class NomadNetworkNode():
|
||||
# Stats Renderers #
|
||||
###################
|
||||
|
||||
def render_chart(self, data, labels, color="666", height=10):
|
||||
def render_chart(self, data, labels, color="666", height=10, secondary_color=None, gradient_factor=None):
|
||||
return self.render_chart_halfblock(data, labels, color=color, height=height, secondary_color=secondary_color, gradient_factor=gradient_factor)
|
||||
|
||||
def render_chart_full_block(self, data, labels, color="666", height=10):
|
||||
if not data or all(d == 0 for d in data): return "No data available\n"
|
||||
max_val = max(data) if max(data) > 0 else 1
|
||||
num_points = len(data)
|
||||
@@ -2273,8 +2405,8 @@ class NomadNetworkNode():
|
||||
indent = ""
|
||||
bar_width = 1
|
||||
|
||||
chart_lines = []
|
||||
chart_lines.append(f"{indent}`F{color}Peak: {max_val}`f\n")
|
||||
lines = []
|
||||
lines.append(f"{indent}`F{color}Peak: {max_val}`f\n")
|
||||
for row in range(height, 0, -1):
|
||||
threshold = (row - 1) / height * max_val
|
||||
row_line = f"{indent}│"
|
||||
@@ -2286,58 +2418,6 @@ class NomadNetworkNode():
|
||||
else: row_line += f"`F{color}{'░'*bar_width}`f{hsep}"
|
||||
else: row_line += f"{' '*bar_width}{hsep}"
|
||||
row_line += "\n"
|
||||
chart_lines.append(row_line)
|
||||
|
||||
hsj = "┴"*len(hsep)
|
||||
bottom_border = "└" + hsj.join(["─" * bar_width] * num_points) + "┘"
|
||||
chart_lines.append(indent + bottom_border + "\n")
|
||||
|
||||
chart_width = len(bottom_border)
|
||||
first_label = f"{labels[0][:12]:<12}"
|
||||
final_label = f"{labels[-1][:12]:>12}"
|
||||
middle_space = chart_width-len(first_label)-len(final_label)
|
||||
|
||||
label_line = f"{indent}`F666{first_label}`f"
|
||||
label_line += " " * middle_space
|
||||
label_line += f"`F666{final_label}`f\n"
|
||||
chart_lines.append(label_line)
|
||||
|
||||
return "".join(chart_lines)
|
||||
|
||||
# TODO: This is a weird idea, really. Probably redo it to something else.
|
||||
def render_combined_chart(self, views, fetches, pushes, labels, height=4):
|
||||
if not views or not labels: return "No data available\n"
|
||||
|
||||
all_data = [v + f + p for v, f, p in zip(views, fetches, pushes)]
|
||||
max_val = max(all_data) if all(all_data) > 0 else 1
|
||||
num_points = len(views)
|
||||
|
||||
hsep = ""
|
||||
indent = ""
|
||||
bar_width = 1
|
||||
|
||||
lines = []
|
||||
lines.append(f"{indent}`F66d██`f Views `F0a0██`f Fetches `Faa0██`f Pushes\n\n")
|
||||
for row in range(height, 0, -1):
|
||||
threshold = (row - 1) / height * max_val
|
||||
row_line = f"{indent}│"
|
||||
for i in range(num_points):
|
||||
v, f, p = views[i], fetches[i], pushes[i]
|
||||
total = v + f + p
|
||||
|
||||
if total > threshold:
|
||||
# Determine which "layer" this row represents
|
||||
# Priority: Pushes > fetches > views for display
|
||||
if p > 0 and threshold < (v + f + p) and threshold >= (v + f): row_line += f"`Faa0{'█'*bar_width}`f{hsep}"
|
||||
elif f > 0 and threshold < (v + f) and threshold >= v: row_line += f"`F0a0{'▓'*bar_width}`f{hsep}"
|
||||
elif v > 0 and threshold < v: row_line += f"`F66d{'░'*bar_width}`f{hsep}"
|
||||
else:
|
||||
# Mixed or partial, show dominant
|
||||
if p >= f and p >= v: row_line += f"`Faa0{'▒'*bar_width}`f{hsep}"
|
||||
elif f >= v: row_line += f"`F0a0{'▒'*bar_width}`f{hsep}"
|
||||
else: row_line += f"`F66d{'▒'*bar_width}`f{hsep}"
|
||||
else: row_line += f"{' '*bar_width}{hsep}"
|
||||
row_line += "\n"
|
||||
lines.append(row_line)
|
||||
|
||||
hsj = "┴"*len(hsep)
|
||||
@@ -2349,13 +2429,157 @@ class NomadNetworkNode():
|
||||
final_label = f"{labels[-1][:12]:>12}"
|
||||
middle_space = chart_width-len(first_label)-len(final_label)
|
||||
|
||||
label_line = f"{indent}`F666{first_label}`f"
|
||||
label_line = f"{indent}{self.CLR_DIM}{first_label}`f"
|
||||
label_line += " " * middle_space
|
||||
label_line += f"`F666{final_label}`f\n"
|
||||
label_line += f"{self.CLR_DIM}{final_label}`f\n"
|
||||
lines.append(label_line)
|
||||
|
||||
return "".join(lines)
|
||||
|
||||
def render_chart_halfblock(self, data, labels, color="666", height=10, secondary_color=None, gradient_factor=None):
|
||||
if not gradient_factor: gradient_factor = 1.3
|
||||
if not data or all(d == 0 for d in data): return "No data available\n"
|
||||
max_val = max(data) if max(data) > 0 else 1
|
||||
num_points = len(data)
|
||||
|
||||
def hex_to_rgb(h): return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
||||
def expand_color(c): return ''.join(ch * 2 for ch in c) if len(c) == 3 else c[:6]
|
||||
def gradient_color(t): return ''.join(f"{int(secondary_rgb[i] + (primary_rgb[i] - secondary_rgb[i]) * min(1, t*gradient_factor)):02x}" for i in range(3))
|
||||
|
||||
primary = expand_color(color)
|
||||
if secondary_color: secondary = expand_color(secondary_color)
|
||||
else: secondary = ''.join(f"{int(int(primary[i:i+2], 16) * 0.42):02x}" for i in (0, 2, 4))
|
||||
|
||||
primary_rgb = hex_to_rgb(primary)
|
||||
secondary_rgb = hex_to_rgb(secondary)
|
||||
|
||||
lines = [f"`FT{primary}Peak: {max_val}`f\n"]
|
||||
for row in range(height, 0, -1):
|
||||
row_top = (row / height) * max_val
|
||||
row_bottom = ((row - 1) / height) * max_val
|
||||
row_mid = (row_top + row_bottom) / 2
|
||||
|
||||
grad_top = row / height
|
||||
grad_mid = (row - 0.5) / height
|
||||
|
||||
line = "│"
|
||||
for val in data:
|
||||
upper_filled = val >= row_top
|
||||
lower_filled = val >= row_mid
|
||||
|
||||
if not upper_filled and not lower_filled: line += " "
|
||||
elif upper_filled: line += f"`FT{gradient_color(grad_top)}`BT{gradient_color(grad_mid)}▀`f`b"
|
||||
else: line += f"`FT{gradient_color(grad_mid)}▄`f"
|
||||
|
||||
lines.append(line + "\n")
|
||||
|
||||
hsep = ""
|
||||
indent = ""
|
||||
bar_width = 1
|
||||
|
||||
hsj = "┴"*len(hsep)
|
||||
bottom_border = "└" + hsj.join(["─" * bar_width] * num_points) + "┘"
|
||||
lines.append(indent + bottom_border + "\n")
|
||||
|
||||
chart_width = len(bottom_border)
|
||||
first_label = f"{labels[0][:12]:<12}"
|
||||
final_label = f"{labels[-1][:12]:>12}"
|
||||
middle_space = chart_width-len(first_label)-len(final_label)
|
||||
|
||||
label_line = f"{indent}{self.CLR_DIM}{first_label}`f"
|
||||
label_line += " " * middle_space
|
||||
label_line += f"{self.CLR_DIM}{final_label}`f\n"
|
||||
lines.append(label_line)
|
||||
|
||||
return "".join(lines)
|
||||
|
||||
def render_combined_chart(self, views, fetches, pushes, downloads, labels, height=6, colors=None, dim=0.87):
|
||||
if not views or not all([views, fetches, pushes, downloads]): return "No data available\n"
|
||||
|
||||
if colors is None:
|
||||
colors = { 'views': self.RCLR_VIEW,
|
||||
'fetches': self.RCLR_FETCH,
|
||||
'pushes': self.RCLR_PUSH,
|
||||
'downloads': self.RCLR_DOWNLOAD }
|
||||
|
||||
def expand(c): return ''.join(ch*2 for ch in c) if len(c) == 3 else c[:6]
|
||||
def hex_to_rgb(h): return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
||||
def gradient_color(c, g, t, f=1.0): c = hex_to_rgb(c); g = hex_to_rgb(g); return ''.join(f"{int(g[i] + (c[i] - g[i]) * min(1, t*f)):02x}" for i in range(3))
|
||||
cat_colors = {'views': gradient_color(expand(colors.get('views', self.RCLR_VIEW)), "000000", dim),
|
||||
'fetches': gradient_color(expand(colors.get('fetches', self.RCLR_FETCH)), "000000", dim),
|
||||
'pushes': gradient_color(expand(colors.get('pushes', self.RCLR_PUSH)), "000000", dim),
|
||||
'downloads': gradient_color(expand(colors.get('downloads', self.RCLR_DOWNLOAD)), "000000", dim) }
|
||||
|
||||
# Stack order
|
||||
categories = ['pushes', 'fetches', 'views', 'downloads']
|
||||
cat_data = [pushes, fetches, views, downloads]
|
||||
|
||||
num_points = len(views)
|
||||
legend = " ".join(f"`FT{cat_colors[cat]}`BT{cat_colors[cat]}██`f`b {cat.capitalize()}" for cat in categories)
|
||||
lines = [f"{legend}\n\n"]
|
||||
|
||||
for row in range(height, 0, -1):
|
||||
lower_min = (row - 1) / height
|
||||
lower_max = (row - 0.5) / height
|
||||
upper_min = (row - 0.5) / height
|
||||
upper_max = row / height
|
||||
|
||||
line = "│"
|
||||
|
||||
for i in range(num_points):
|
||||
total = sum(d[i] for d in cat_data)
|
||||
if total == 0:
|
||||
line += " "
|
||||
continue
|
||||
|
||||
cumsum = 0
|
||||
cat_ranges = {}
|
||||
for cat, data in zip(categories, cat_data):
|
||||
start = cumsum / total
|
||||
cumsum += data[i]
|
||||
end = cumsum / total
|
||||
cat_ranges[cat] = (start, end)
|
||||
|
||||
def pixel_to_cat(pmin, pmax):
|
||||
for cat in categories:
|
||||
cstart, cend = cat_ranges[cat]
|
||||
# Check if pixel overlaps this category
|
||||
# Return the category (they're mutually exclusive in stacked chart)
|
||||
if pmin < cend and pmax > cstart: return cat
|
||||
return None
|
||||
|
||||
upper_cat = pixel_to_cat(upper_min, upper_max)
|
||||
lower_cat = pixel_to_cat(lower_min, lower_max)
|
||||
|
||||
if upper_cat is None and lower_cat is None: line += " "
|
||||
elif upper_cat == lower_cat and upper_cat is not None:
|
||||
col = cat_colors[upper_cat]
|
||||
line += f"`FT{col}`BT{col}█`f`b"
|
||||
elif upper_cat is not None and lower_cat is not None:
|
||||
upper_col = cat_colors[upper_cat]
|
||||
lower_col = cat_colors[lower_cat]
|
||||
line += f"`FT{upper_col}`BT{lower_col}▀`f`b"
|
||||
elif upper_cat is not None:
|
||||
col = cat_colors[upper_cat]
|
||||
line += f"`FT{col}▀`f"
|
||||
else:
|
||||
col = cat_colors[lower_cat]
|
||||
line += f"`FT{col}▄`f"
|
||||
|
||||
lines.append(line + "\n")
|
||||
|
||||
bottom = "└" + "─" * num_points + "┘"
|
||||
lines.append(bottom + "\n")
|
||||
|
||||
if labels:
|
||||
first = str(labels[0])[:12]
|
||||
last = str(labels[-1])[:12]
|
||||
mid_space = len(bottom) - len(first) - len(last)
|
||||
lines.append(f"{self.CLR_DIM}{first}{' ' * mid_space}{last}`f\n")
|
||||
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
#######################
|
||||
# Connection Handlers #
|
||||
#######################
|
||||
|
||||
+1928
-224
File diff suppressed because it is too large
Load Diff
+82
-24
@@ -133,6 +133,10 @@ class MarkdownToMicron:
|
||||
self.__local_url_scope = self.local_url_scope
|
||||
self.syntax_highlighter = syntax_highlighter
|
||||
self.wcwidth = None
|
||||
|
||||
self.bold_links = True
|
||||
self.underline_links = True
|
||||
self.link_color = None
|
||||
|
||||
try:
|
||||
import wcwidth
|
||||
@@ -204,19 +208,21 @@ class MarkdownToMicron:
|
||||
code_content = '\n'.join(code_buffer)
|
||||
|
||||
if self.syntax_highlighter and code_block_lang:
|
||||
try:
|
||||
highlighted = self.syntax_highlighter.highlight(code_content, language=code_block_lang)
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(highlighted)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
if code_block_lang.lower() == "rawmu": result_lines.append(code_content)
|
||||
else:
|
||||
try:
|
||||
highlighted = self.syntax_highlighter.highlight(code_content, language=code_block_lang)
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(highlighted)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
|
||||
except Exception:
|
||||
# Fallback to plain literal block on any error
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(self.LITERAL_START)
|
||||
result_lines.append(self._escape_literals(code_content))
|
||||
result_lines.append(self.LITERAL_END)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
except Exception:
|
||||
# Fallback to plain literal block on any error
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(self.LITERAL_START)
|
||||
result_lines.append(self._escape_literals(code_content))
|
||||
result_lines.append(self.LITERAL_END)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
else:
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(self.LITERAL_START)
|
||||
@@ -343,8 +349,15 @@ class MarkdownToMicron:
|
||||
url = f"{self.local_url_scope}{url}"
|
||||
if anchor: url = f"{url}|anchor={anchor}"
|
||||
|
||||
undl = "`_" if self.underline_links else ""
|
||||
bold = "`!" if self.bold_links else ""
|
||||
text = text.replace('`', '')
|
||||
return f"`!`[{text}`{url}]`!"
|
||||
link = f"{undl}{bold}`[{text}`{url}]{bold}{undl}"
|
||||
|
||||
if self.link_color and len(self.link_color) == 3: link = f"`F{self.link_color}{link}`f"
|
||||
if self.link_color and len(self.link_color) == 6: link = f"`FT{self.link_color}{link}`f"
|
||||
|
||||
return link
|
||||
|
||||
text = re.sub(r'\x00LINK(\d+)\x00', restore_link, text)
|
||||
|
||||
@@ -673,19 +686,64 @@ class MarkdownToMicron:
|
||||
return " " * left + text + " " * right
|
||||
else:
|
||||
return text + " " * padding
|
||||
|
||||
|
||||
def _truncate_cell(self, text, width):
|
||||
if self._visible_width(text) <= width: return text
|
||||
|
||||
stripped = text
|
||||
stripped = re.sub(r'`[FB][0-9a-fA-F]{3}', '', stripped)
|
||||
stripped = re.sub(r'`[!*_]', '', stripped)
|
||||
stripped = re.sub(r'`f`b', '', stripped)
|
||||
|
||||
if len(stripped) <= width - 1: return text
|
||||
|
||||
truncated = stripped[:width - 1] + "…"
|
||||
return truncated
|
||||
|
||||
truncation_point = len(text)
|
||||
while truncation_point > 0 and self._visible_width(text[0:truncation_point]) >= width:
|
||||
truncation_point -= 1
|
||||
|
||||
truncated = text[:truncation_point]
|
||||
|
||||
# Yes, this is convoluted, but if someone else has
|
||||
# a better idea on how to handle unclosed micron
|
||||
# tags in the truncated cells, I'm all ears.
|
||||
active_tags = set()
|
||||
fg_active = False
|
||||
bg_active = False
|
||||
|
||||
i = 0
|
||||
while i < len(truncated):
|
||||
if truncated[i] == '`':
|
||||
if i + 1 < len(truncated):
|
||||
tag_char = truncated[i + 1]
|
||||
|
||||
if tag_char in '!*_=':
|
||||
if tag_char in active_tags: active_tags.remove(tag_char)
|
||||
else: active_tags.add(tag_char)
|
||||
i += 2
|
||||
continue
|
||||
|
||||
elif tag_char == 'f':
|
||||
fg_active = False
|
||||
i += 2
|
||||
continue
|
||||
|
||||
elif tag_char == 'b':
|
||||
bg_active = False
|
||||
i += 2
|
||||
continue
|
||||
|
||||
elif tag_char == 'F':
|
||||
fg_active = True
|
||||
if i + 2 < len(truncated) and truncated[i + 2] == 'T': i += 8
|
||||
else: i += 5
|
||||
continue
|
||||
|
||||
elif tag_char == 'B':
|
||||
bg_active = True
|
||||
if i + 2 < len(truncated) and truncated[i + 2] == 'T': i += 8
|
||||
else: i += 5
|
||||
continue
|
||||
i += 1
|
||||
|
||||
closers = []
|
||||
if fg_active: closers.append('`f')
|
||||
if bg_active: closers.append('`b')
|
||||
for fmt in active_tags: closers.append(f'`{fmt}')
|
||||
|
||||
return truncated + ''.join(closers) + "…"
|
||||
|
||||
def _wrap_text(self, text, width):
|
||||
if not text: return [""]
|
||||
|
||||
+92
-13
@@ -40,12 +40,15 @@ import base64
|
||||
|
||||
from RNS._version import __version__
|
||||
from RNS.vendor import umsgpack as mp
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
from RNS.vendor.validate import Validator
|
||||
from RNS.Cryptography.Hashes import sha256
|
||||
from RNS.Cryptography.Hashes import file_sha256
|
||||
|
||||
APP_NAME = "rns"
|
||||
DEFAULT_ASPECTS = f"{APP_NAME}.id"
|
||||
NO_MESSAGE = 0x01
|
||||
NO_META = 0x02
|
||||
|
||||
PRV_EXT = "rid"
|
||||
PUB_EXT = "pub"
|
||||
@@ -122,11 +125,14 @@ def main():
|
||||
parser.add_argument("-e", "--encrypt", metavar="file", action="store", nargs="*", default=None, help="encrypt file")
|
||||
parser.add_argument("-V", "--validate", metavar="path", action="store", nargs="*", default=None, help="validate signature")
|
||||
parser.add_argument("-s", "--sign", metavar="path", action="store", nargs="*", default=None, help="sign file")
|
||||
parser.add_argument("-S", "--sign-message", metavar="path", action="store", nargs="?", const=NO_MESSAGE, default=None, help="create embedded signed message")
|
||||
parser.add_argument("-S", "--sign-message", metavar="text", action="store", nargs="?", const=NO_MESSAGE, default=None, help="create embedded signed message")
|
||||
parser.add_argument("-E", "--embed-meta", metavar="path", action="store", nargs="?", const=NO_META, default=None, help="embed metadata structure from file")
|
||||
parser.add_argument("--meta-spec", metavar="path", action="store", nargs="?", default=None, help="validate metadata for embedding with spec from file")
|
||||
parser.add_argument("--raw", action="store_true", default=False, help="sign raw input data instead of hashing first")
|
||||
|
||||
# I/O Control
|
||||
parser.add_argument("-w", "--write", metavar="file", action="store", default=None, help="output file path", type=str)
|
||||
parser.add_argument("-w", "--write", metavar="path", action="store", default=None, help="output file path", type=str)
|
||||
parser.add_argument("-r", "--read", metavar="path", action="store", default=None, help="input file path for operations with optional file input", type=str)
|
||||
parser.add_argument("-f", "--force", action="store_true", default=None, help="write output even if it overwrites existing files")
|
||||
parser.add_argument("-I", "--stdin", action="store_true", default=False, help=argparse.SUPPRESS) # help="read input from STDIN instead of file"
|
||||
parser.add_argument("-O", "--stdout", action="store_true", default=False, help=argparse.SUPPRESS) # help="write output to STDOUT instead of file"
|
||||
@@ -139,17 +145,18 @@ def main():
|
||||
parser.add_argument("-P", "--print-private", action="store_true", default=False, help="allow displaying private keys")
|
||||
|
||||
# Formatting Control
|
||||
parser.add_argument("-b", "--base64", action="store_true", default=False, help="Use base64-encoded input and output")
|
||||
parser.add_argument("-B", "--base32", action="store_true", default=False, help="Use base32-encoded input and output")
|
||||
parser.add_argument("--hex", action="store_true", default=False, help="Use hex-encoded input and output")
|
||||
parser.add_argument("--base256", action="store_true", default=False, help="Use base256-encoded input and output")
|
||||
parser.add_argument("-b", "--base64", action="store_true", default=False, help="Use base64-encoded input and output")
|
||||
parser.add_argument("-U", "--base256", action="store_true", default=False, help="Use base256-encoded input and output")
|
||||
parser.add_argument("-F", "--hex", action="store_true", default=False, help="Use hex-encoded input and output")
|
||||
parser.add_argument("--meta", action="store_true", default=False, help="Display RSM metadata if available")
|
||||
|
||||
parser.add_argument("--version", action="version", version="rnid {version}".format(version=__version__))
|
||||
|
||||
args = parser.parse_args()
|
||||
validate_args(args)
|
||||
|
||||
op_requires_identity = (args.sign or args.encrypt or args.decrypt or args.announce or args.write
|
||||
op_requires_identity = (args.sign or args.sign_message or args.encrypt or args.decrypt or args.announce or args.write
|
||||
or args.print_identity or args.print_identity or args.export_pub or args.export_prv)
|
||||
|
||||
identity = get_operating_identity(args, allow_none=not op_requires_identity, no_cache=args.no_cache); op = False
|
||||
@@ -456,7 +463,6 @@ def validate_rsg(rsg, message=None, required_signer=None):
|
||||
if not "meta" in signed_data: return False, None, None
|
||||
if not "signer" in signed_data["meta"]: return False, None, None
|
||||
if not "pubkey" in signed_data["meta"]: return False, None, None
|
||||
if not "note" in signed_data["meta"]: return False, None, None
|
||||
|
||||
try:
|
||||
if type(required_signer) == RNS.Identity:
|
||||
@@ -479,7 +485,7 @@ def validate_rsg(rsg, message=None, required_signer=None):
|
||||
|
||||
return False, signed_data, signing_identity
|
||||
|
||||
def create_rsg(signer_identity, message, embed=False, note=None, meta=None, output="bin"):
|
||||
def create_rsg(signer_identity, message, embed=False, meta=None, output="bin"):
|
||||
if not output in ["bin", "hex", "base32", "base256", "base64"]: raise TypeError(f"Invalid output format for rsg creation")
|
||||
if not type(signer_identity) == RNS.Identity: raise TypeError(f"{signer_identity} is not a Reticulum Identity")
|
||||
if not signer_identity.get_private_key(): raise ValueError(f"{signer_identity} does not hold a private key")
|
||||
@@ -487,7 +493,7 @@ def create_rsg(signer_identity, message, embed=False, note=None, meta=None, outp
|
||||
signed_data = { "hashtype": "sha256", "hash": get_rsg_hash(message),
|
||||
"meta": { "signer": signer_identity.hash,
|
||||
"pubkey": signer_identity.get_public_key(),
|
||||
"note" : note } }
|
||||
"note" : None } } # TODO: Remove default note field in 1.2.9
|
||||
|
||||
if embed:
|
||||
if type(message) == str: message = message.encode("utf-8")
|
||||
@@ -495,7 +501,7 @@ def create_rsg(signer_identity, message, embed=False, note=None, meta=None, outp
|
||||
|
||||
if meta and type(meta) == dict:
|
||||
for key in meta:
|
||||
if not key in signed_data["meta"]: signed_data["meta"]["key"] = meta["key"]
|
||||
if not key in signed_data["meta"]: signed_data["meta"][key] = meta[key]
|
||||
|
||||
envelope = mp.packb(signed_data)
|
||||
signature = signer_identity.sign(envelope)
|
||||
@@ -558,6 +564,27 @@ def unwrap_rsg(wrapped_rsg):
|
||||
|
||||
return unwrapped if unwrapped else None
|
||||
|
||||
def rsg_meta_from_file(path, spec_path=None):
|
||||
if spec_path: meta_spec = ConfigObj(spec_path)
|
||||
else: meta_spec = None
|
||||
parsed = ConfigObj(path, configspec=meta_spec)
|
||||
|
||||
if meta_spec:
|
||||
validation = parsed.validate(Validator())
|
||||
if not validation == True: raise ValueError("Metadata did not pass spec validation")
|
||||
|
||||
return parsed.dict()
|
||||
|
||||
def rsg_meta_from_str(meta, spec=None):
|
||||
if spec: meta_spec = ConfigObj(spec.splitlines())
|
||||
else: meta_spec = None
|
||||
parsed = ConfigObj(meta.splitlines(), configspec=meta_spec)
|
||||
|
||||
if meta_spec:
|
||||
validation = parsed.validate(Validator())
|
||||
if not validation == True: raise ValueError("Metadata did not pass spec validation")
|
||||
|
||||
return parsed.dict()
|
||||
|
||||
###################################
|
||||
# Signing & Validation Operations #
|
||||
@@ -656,7 +683,40 @@ def validate_message(args, identity, __recursive=False):
|
||||
signer_description = f"\nThe message was NOT signed by {identity_str or signing_identity}" if identity else ""
|
||||
if not valid: print(f"Invalid signature in {signature_path}{signer_description}"); exit(R_INVALID_SIGNATURE)
|
||||
else:
|
||||
print(f"\nSignature is valid, the following message was signed by {signing_identity}:\n")
|
||||
if args.meta:
|
||||
print("RSM Metadata\n============\n")
|
||||
def recurse(entry, key, level=1):
|
||||
try:
|
||||
indent = " "*level
|
||||
if type(entry) == dict:
|
||||
print(f"d{indent}{key}:")
|
||||
for key in entry: recurse(entry[key], key, level=level+1)
|
||||
else:
|
||||
maxwidth = 64
|
||||
etype = "u"
|
||||
if type(entry) == str: etype = "s"
|
||||
elif type(entry) == bytes: etype = "b"
|
||||
elif type(entry) == list: etype = "l"
|
||||
elif type(entry) == dict: etype = "d"
|
||||
elif type(entry) == int: etype = "i"
|
||||
elif type(entry) == float: etype = "f"
|
||||
elif entry == None: etype = "N"
|
||||
if key == "note" and entry == None: return # TODO: Remove this check in 1.2.9
|
||||
if type(entry) == bytes: entry = RNS.hexrep(entry, delimit=False)
|
||||
leadin = f"{etype}{indent}{key}="; leadln = len(leadin)
|
||||
entry = f"{entry}"; chunk = entry[:maxwidth]; entry = entry[maxwidth:]
|
||||
print(f"{leadin}{chunk}")
|
||||
while len(entry): chunk = entry[:maxwidth]; entry = entry[maxwidth:]; print(f" "*leadln+chunk)
|
||||
except: print(f"E{indent}{key}=<Decode Error>")
|
||||
|
||||
meta = signed_data["meta"]
|
||||
for key in meta: entry = meta[key]; recurse(entry, key)
|
||||
print("\nValidation\n==========")
|
||||
|
||||
c = ":" if not args.meta else ""
|
||||
f = " following" if not args.meta else ""
|
||||
print(f"\nSignature is valid, the{f} message was signed by {signing_identity}{c}\n")
|
||||
if args.meta: print("Message\n=======\n")
|
||||
print(signed_data["message"].decode("utf-8"))
|
||||
|
||||
return exit(R_OK) if not __recursive else R_OK
|
||||
@@ -714,6 +774,7 @@ def sign(args, identity, __recursive=False):
|
||||
|
||||
def sign_message(args, identity):
|
||||
message = args.sign_message
|
||||
meta = None
|
||||
|
||||
if args.base32: output = "base32"
|
||||
elif args.base64: output = "base64"
|
||||
@@ -722,13 +783,31 @@ def sign_message(args, identity):
|
||||
else: output = "bin"
|
||||
|
||||
if output == "bin" and not args.write: print("No write path specified"); exit(R_INVALID_ARGS)
|
||||
if not identity.get_private_key(): print(f"Cannot sign \"{sign_path}\", the identity does not hold a private key"); exit(R_NO_PRVKEY)
|
||||
if not identity: print(f"Cannot sign, no working identity available"); exit(R_NO_IDENTITY)
|
||||
if not identity.get_private_key(): print(f"Cannot sign, the identity does not hold a private key"); exit(R_NO_PRVKEY)
|
||||
|
||||
if args.read:
|
||||
if message != NO_MESSAGE: print("Both an input file and command-line provided message was specified, aborting"); exit(R_INVALID_ARGS)
|
||||
sign_path = os.path.expanduser(args.read)
|
||||
if not os.path.isfile(sign_path): print(f"The file {sign_path} does not exist"); exit(R_NO_FILE)
|
||||
with open(sign_path, "r", encoding="utf-8") as fh: message = fh.read()
|
||||
|
||||
if message == NO_MESSAGE: message = get_editor_content()
|
||||
if not message: print("No message specified"); exit(R_INVALID_ARGS)
|
||||
|
||||
if args.embed_meta:
|
||||
meta_path = os.path.expanduser(args.embed_meta)
|
||||
meta_spec_path = meta_path+".spec" if not args.meta_spec else args.meta_spec
|
||||
if not os.path.isfile(meta_path): print(f"Metadata file {meta_path} does not exist"); exit(R_NO_FILE)
|
||||
if not os.path.isfile(meta_spec_path): meta_spec_path = None
|
||||
spec_info = f" using spec from {meta_spec_path}" if meta_spec_path else ""
|
||||
print(f"Embedding metadata from {meta_path}{spec_info}")
|
||||
|
||||
try: meta = rsg_meta_from_file(meta_path, spec_path=meta_spec_path)
|
||||
except Exception as e: print(f"Could not load metadata from {meta_path}: {e}"); exit(R_UNKNOWN_ERROR)
|
||||
|
||||
try:
|
||||
rsg = create_rsg(identity, message, embed=True, output=output)
|
||||
rsg = create_rsg(identity, message, embed=True, meta=meta, output=output)
|
||||
if not rsg: print(f"No signature created, not writing"); exit(R_UNKNOWN_ERROR)
|
||||
|
||||
if output == "bin":
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "1.2.6"
|
||||
__version__ = "1.2.8"
|
||||
|
||||
Vendored
+537
@@ -0,0 +1,537 @@
|
||||
# validate.py
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=
|
||||
#
|
||||
# A Validator object.
|
||||
#
|
||||
# Copyright (C) 2005-2014:
|
||||
# (name) : (email)
|
||||
# Michael Foord: fuzzyman AT voidspace DOT org DOT uk
|
||||
# Mark Andrews: mark AT la-la DOT com
|
||||
# Nicola Larosa: nico AT tekNico DOT net
|
||||
# Rob Dennis: rdennis AT gmail DOT com
|
||||
# Eli Courtwright: eli AT courtwright DOT org
|
||||
|
||||
# This software is licensed under the terms of the BSD license.
|
||||
# http://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
# ConfigObj 5 - main repository for documentation and issue tracking:
|
||||
# https://github.com/DiffSK/configobj
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pprint import pprint
|
||||
|
||||
__version__ = '1.0.1'
|
||||
|
||||
__all__ = (
|
||||
'dottedQuadToNum',
|
||||
'numToDottedQuad',
|
||||
'ValidateError',
|
||||
'VdtUnknownCheckError',
|
||||
'VdtParamError',
|
||||
'VdtTypeError',
|
||||
'VdtValueError',
|
||||
'VdtValueTooSmallError',
|
||||
'VdtValueTooBigError',
|
||||
'VdtValueTooShortError',
|
||||
'VdtValueTooLongError',
|
||||
'VdtMissingValue',
|
||||
'Validator',
|
||||
'is_integer',
|
||||
'is_float',
|
||||
'is_boolean',
|
||||
'is_list',
|
||||
'is_tuple',
|
||||
'is_ip_addr',
|
||||
'is_string',
|
||||
'is_int_list',
|
||||
'is_bool_list',
|
||||
'is_float_list',
|
||||
'is_string_list',
|
||||
'is_ip_addr_list',
|
||||
'is_mixed_list',
|
||||
'is_option',
|
||||
)
|
||||
|
||||
_list_arg = re.compile(r'''
|
||||
(?:
|
||||
([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*list\(
|
||||
(
|
||||
(?:
|
||||
\s*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s\)][^,\)]*?) # unquoted
|
||||
)
|
||||
\s*,\s*
|
||||
)*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s\)][^,\)]*?) # unquoted
|
||||
)? # last one
|
||||
)
|
||||
\)
|
||||
)
|
||||
''', re.VERBOSE | re.DOTALL) # two groups
|
||||
|
||||
_list_members = re.compile(r'''
|
||||
(
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s=][^,=]*?) # unquoted
|
||||
)
|
||||
(?:
|
||||
(?:\s*,\s*)|(?:\s*$) # comma
|
||||
)
|
||||
''', re.VERBOSE | re.DOTALL) # one group
|
||||
|
||||
_paramstring = r'''
|
||||
(?:
|
||||
(
|
||||
(?:
|
||||
[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*list\(
|
||||
(?:
|
||||
\s*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s\)][^,\)]*?) # unquoted
|
||||
)
|
||||
\s*,\s*
|
||||
)*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s\)][^,\)]*?) # unquoted
|
||||
)? # last one
|
||||
\)
|
||||
)|
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s=][^,=]*?)| # unquoted
|
||||
(?: # keyword argument
|
||||
[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s=][^,=]*?) # unquoted
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(?:
|
||||
(?:\s*,\s*)|(?:\s*$) # comma
|
||||
)
|
||||
)
|
||||
'''
|
||||
|
||||
_matchstring = '^%s*' % _paramstring
|
||||
|
||||
def dottedQuadToNum(ip):
|
||||
# import here to avoid it when ip_addr values are not used
|
||||
import socket, struct
|
||||
|
||||
try:
|
||||
return struct.unpack('!L',
|
||||
socket.inet_aton(ip.strip()))[0]
|
||||
except socket.error:
|
||||
raise ValueError('Not a good dotted-quad IP: %s' % ip)
|
||||
return
|
||||
|
||||
def numToDottedQuad(num):
|
||||
# import here to avoid it when ip_addr values are not used
|
||||
import socket, struct
|
||||
|
||||
# no need to intercept here, 4294967295L is fine
|
||||
if num > int(4294967295) or num < 0:
|
||||
raise ValueError('Not a good numeric IP: %s' % num)
|
||||
try:
|
||||
return socket.inet_ntoa(
|
||||
struct.pack('!L', int(num)))
|
||||
except (socket.error, struct.error, OverflowError):
|
||||
raise ValueError('Not a good numeric IP: %s' % num)
|
||||
|
||||
class ValidateError(Exception):
|
||||
"""
|
||||
This error indicates that the check failed.
|
||||
It can be the base class for more specific errors.
|
||||
"""
|
||||
|
||||
class VdtMissingValue(ValidateError):
|
||||
"""No value was supplied to a check that needed one."""
|
||||
|
||||
class VdtUnknownCheckError(ValidateError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the check "{}" is unknown.'.format(value))
|
||||
|
||||
|
||||
class VdtParamError(SyntaxError):
|
||||
NOT_GIVEN = object()
|
||||
|
||||
def __init__(self, name_or_msg, value=NOT_GIVEN):
|
||||
if value is self.NOT_GIVEN:
|
||||
SyntaxError.__init__(self, name_or_msg)
|
||||
else:
|
||||
SyntaxError.__init__(self, 'passed an incorrect value "{}" for parameter "{}".'.format(value, name_or_msg))
|
||||
|
||||
|
||||
class VdtTypeError(ValidateError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is of the wrong type.'.format(value))
|
||||
|
||||
|
||||
class VdtValueError(ValidateError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is unacceptable.'.format(value))
|
||||
|
||||
|
||||
class VdtValueTooSmallError(VdtValueError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is too small.'.format(value))
|
||||
|
||||
|
||||
class VdtValueTooBigError(VdtValueError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is too big.'.format(value))
|
||||
|
||||
|
||||
class VdtValueTooShortError(VdtValueError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(
|
||||
self,
|
||||
'the value "{}" is too short.'.format(value))
|
||||
|
||||
class VdtValueTooLongError(VdtValueError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is too long.'.format(value))
|
||||
|
||||
class Validator(object):
|
||||
# this regex does the initial parsing of the checks
|
||||
_func_re = re.compile(r'([^\(\)]+?)\((.*)\)', re.DOTALL)
|
||||
|
||||
# this regex takes apart keyword arguments
|
||||
_key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$', re.DOTALL)
|
||||
|
||||
|
||||
# this regex finds keyword=list(....) type values
|
||||
_list_arg = _list_arg
|
||||
|
||||
# this regex takes individual values out of lists - in one pass
|
||||
_list_members = _list_members
|
||||
|
||||
# These regexes check a set of arguments for validity
|
||||
# and then pull the members out
|
||||
_paramfinder = re.compile(_paramstring, re.VERBOSE | re.DOTALL)
|
||||
_matchfinder = re.compile(_matchstring, re.VERBOSE | re.DOTALL)
|
||||
|
||||
def __init__(self, functions=None):
|
||||
self.functions = {
|
||||
'': self._pass,
|
||||
'integer': is_integer,
|
||||
'float': is_float,
|
||||
'boolean': is_boolean,
|
||||
'ip_addr': is_ip_addr,
|
||||
'string': is_string,
|
||||
'list': is_list,
|
||||
'tuple': is_tuple,
|
||||
'int_list': is_int_list,
|
||||
'float_list': is_float_list,
|
||||
'bool_list': is_bool_list,
|
||||
'ip_addr_list': is_ip_addr_list,
|
||||
'string_list': is_string_list,
|
||||
'mixed_list': is_mixed_list,
|
||||
'pass': self._pass,
|
||||
'option': is_option,
|
||||
'force_list': force_list,
|
||||
}
|
||||
if functions is not None:
|
||||
self.functions.update(functions)
|
||||
# tekNico: for use by ConfigObj
|
||||
self.baseErrorClass = ValidateError
|
||||
self._cache = {}
|
||||
|
||||
def check(self, check, value, missing=False):
|
||||
fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check)
|
||||
|
||||
if missing:
|
||||
if default is None:
|
||||
# no information needed here - to be handled by caller
|
||||
raise VdtMissingValue()
|
||||
value = self._handle_none(default)
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return self._check_value(value, fun_name, fun_args, fun_kwargs)
|
||||
|
||||
def _handle_none(self, value):
|
||||
if value == 'None':
|
||||
return None
|
||||
elif value in ("'None'", '"None"'):
|
||||
# Special case a quoted None
|
||||
value = self._unquote(value)
|
||||
return value
|
||||
|
||||
def _parse_with_caching(self, check):
|
||||
if check in self._cache:
|
||||
fun_name, fun_args, fun_kwargs, default = self._cache[check]
|
||||
# We call list and dict below to work with *copies* of the data
|
||||
# rather than the original (which are mutable of course)
|
||||
fun_args = list(fun_args)
|
||||
fun_kwargs = dict(fun_kwargs)
|
||||
else:
|
||||
fun_name, fun_args, fun_kwargs, default = self._parse_check(check)
|
||||
fun_kwargs = {str(key): value for (key, value) in list(fun_kwargs.items())}
|
||||
self._cache[check] = fun_name, list(fun_args), dict(fun_kwargs), default
|
||||
return fun_name, fun_args, fun_kwargs, default
|
||||
|
||||
def _check_value(self, value, fun_name, fun_args, fun_kwargs):
|
||||
try:
|
||||
fun = self.functions[fun_name]
|
||||
except KeyError:
|
||||
raise VdtUnknownCheckError(fun_name)
|
||||
else:
|
||||
return fun(value, *fun_args, **fun_kwargs)
|
||||
|
||||
def _parse_check(self, check):
|
||||
fun_match = self._func_re.match(check)
|
||||
if fun_match:
|
||||
fun_name = fun_match.group(1)
|
||||
arg_string = fun_match.group(2)
|
||||
arg_match = self._matchfinder.match(arg_string)
|
||||
if arg_match is None:
|
||||
# Bad syntax
|
||||
raise VdtParamError('Bad syntax in check "%s".' % check)
|
||||
fun_args = []
|
||||
fun_kwargs = {}
|
||||
# pull out args of group 2
|
||||
for arg in self._paramfinder.findall(arg_string):
|
||||
# args may need whitespace removing (before removing quotes)
|
||||
arg = arg.strip()
|
||||
listmatch = self._list_arg.match(arg)
|
||||
if listmatch:
|
||||
key, val = self._list_handle(listmatch)
|
||||
fun_kwargs[key] = val
|
||||
continue
|
||||
keymatch = self._key_arg.match(arg)
|
||||
if keymatch:
|
||||
val = keymatch.group(2)
|
||||
if not val in ("'None'", '"None"'):
|
||||
# Special case a quoted None
|
||||
val = self._unquote(val)
|
||||
fun_kwargs[keymatch.group(1)] = val
|
||||
continue
|
||||
|
||||
fun_args.append(self._unquote(arg))
|
||||
else:
|
||||
# allows for function names without (args)
|
||||
return check, (), {}, None
|
||||
|
||||
# Default must be deleted if the value is specified too,
|
||||
# otherwise the check function will get a spurious "default" keyword arg
|
||||
default = fun_kwargs.pop('default', None)
|
||||
return fun_name, fun_args, fun_kwargs, default
|
||||
|
||||
def _unquote(self, val):
|
||||
if (len(val) >= 2) and (val[0] in ("'", '"')) and (val[0] == val[-1]):
|
||||
val = val[1:-1]
|
||||
return val
|
||||
|
||||
def _list_handle(self, listmatch):
|
||||
out = []
|
||||
name = listmatch.group(1)
|
||||
args = listmatch.group(2)
|
||||
for arg in self._list_members.findall(args):
|
||||
out.append(self._unquote(arg))
|
||||
return name, out
|
||||
|
||||
def _pass(self, value):
|
||||
return value
|
||||
|
||||
def get_default_value(self, check):
|
||||
fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check)
|
||||
if default is None:
|
||||
raise KeyError('Check "%s" has no default value.' % check)
|
||||
value = self._handle_none(default)
|
||||
if value is None:
|
||||
return value
|
||||
return self._check_value(value, fun_name, fun_args, fun_kwargs)
|
||||
|
||||
def _is_num_param(names, values, to_float=False):
|
||||
fun = to_float and float or int
|
||||
out_params = []
|
||||
for (name, val) in zip(names, values):
|
||||
if val is None:
|
||||
out_params.append(val)
|
||||
elif isinstance(val, (int, float, str)):
|
||||
try:
|
||||
out_params.append(fun(val))
|
||||
except ValueError:
|
||||
raise VdtParamError(name, val)
|
||||
else:
|
||||
raise VdtParamError(name, val)
|
||||
return out_params
|
||||
|
||||
# built in checks
|
||||
# you can override these by setting the appropriate name
|
||||
# in Validator.functions
|
||||
# note: if the params are specified wrongly in your input string,
|
||||
# you will also raise errors.
|
||||
def is_integer(value, min=None, max=None):
|
||||
(min_val, max_val) = _is_num_param( # pylint: disable=unbalanced-tuple-unpacking
|
||||
('min', 'max'), (min, max))
|
||||
if not isinstance(value, (int, str)):
|
||||
raise VdtTypeError(value)
|
||||
if isinstance(value, str):
|
||||
# if it's a string - does it represent an integer ?
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise VdtTypeError(value)
|
||||
if (min_val is not None) and (value < min_val):
|
||||
raise VdtValueTooSmallError(value)
|
||||
if (max_val is not None) and (value > max_val):
|
||||
raise VdtValueTooBigError(value)
|
||||
return value
|
||||
|
||||
|
||||
def is_float(value, min=None, max=None):
|
||||
(min_val, max_val) = _is_num_param(
|
||||
('min', 'max'), (min, max), to_float=True)
|
||||
if not isinstance(value, (int, float, str)):
|
||||
raise VdtTypeError(value)
|
||||
if not isinstance(value, float):
|
||||
# if it's a string - does it represent a float ?
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
raise VdtTypeError(value)
|
||||
if (min_val is not None) and (value < min_val):
|
||||
raise VdtValueTooSmallError(value)
|
||||
if (max_val is not None) and (value > max_val):
|
||||
raise VdtValueTooBigError(value)
|
||||
return value
|
||||
|
||||
bool_dict = {
|
||||
True: True, 'on': True, '1': True, 'true': True, 'yes': True,
|
||||
False: False, 'off': False, '0': False, 'false': False, 'no': False,
|
||||
}
|
||||
|
||||
def is_boolean(value):
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return bool_dict[value.lower()]
|
||||
except KeyError:
|
||||
raise VdtTypeError(value)
|
||||
# we do an equality test rather than an identity test
|
||||
# this ensures Python 2.2 compatibility
|
||||
# and allows 0 and 1 to represent True and False
|
||||
if value == False:
|
||||
return False
|
||||
elif value == True:
|
||||
return True
|
||||
else:
|
||||
raise VdtTypeError(value)
|
||||
|
||||
|
||||
def is_ip_addr(value):
|
||||
if not isinstance(value, str):
|
||||
raise VdtTypeError(value)
|
||||
value = value.strip()
|
||||
try:
|
||||
dottedQuadToNum(value)
|
||||
except ValueError:
|
||||
raise VdtValueError(value)
|
||||
return value
|
||||
|
||||
|
||||
def is_list(value, min=None, max=None):
|
||||
(min_len, max_len) = _is_num_param( # pylint: disable=unbalanced-tuple-unpacking
|
||||
('min', 'max'), (min, max))
|
||||
if isinstance(value, str):
|
||||
raise VdtTypeError(value)
|
||||
try:
|
||||
num_members = len(value)
|
||||
except TypeError:
|
||||
raise VdtTypeError(value)
|
||||
if min_len is not None and num_members < min_len:
|
||||
raise VdtValueTooShortError(value)
|
||||
if max_len is not None and num_members > max_len:
|
||||
raise VdtValueTooLongError(value)
|
||||
return list(value)
|
||||
|
||||
|
||||
def is_tuple(value, min=None, max=None):
|
||||
return tuple(is_list(value, min, max))
|
||||
|
||||
def is_string(value, min=None, max=None):
|
||||
if not isinstance(value, str):
|
||||
raise VdtTypeError(value)
|
||||
(min_len, max_len) = _is_num_param(
|
||||
('min', 'max'), (min, max))
|
||||
try:
|
||||
num_members = len(value)
|
||||
except TypeError:
|
||||
raise VdtTypeError(value)
|
||||
if min_len is not None and num_members < min_len:
|
||||
raise VdtValueTooShortError(value)
|
||||
if max_len is not None and num_members > max_len:
|
||||
raise VdtValueTooLongError(value)
|
||||
return value
|
||||
|
||||
|
||||
def is_int_list(value, min=None, max=None):
|
||||
return [is_integer(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def is_bool_list(value, min=None, max=None):
|
||||
return [is_boolean(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def is_float_list(value, min=None, max=None):
|
||||
return [is_float(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def is_string_list(value, min=None, max=None):
|
||||
if isinstance(value, str):
|
||||
raise VdtTypeError(value)
|
||||
return [is_string(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def is_ip_addr_list(value, min=None, max=None):
|
||||
return [is_ip_addr(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def force_list(value, min=None, max=None):
|
||||
if not isinstance(value, (list, tuple)):
|
||||
value = [value]
|
||||
return is_list(value, min, max)
|
||||
|
||||
fun_dict = {
|
||||
int: is_integer,
|
||||
'int': is_integer,
|
||||
'integer': is_integer,
|
||||
float: is_float,
|
||||
'float': is_float,
|
||||
'ip_addr': is_ip_addr,
|
||||
str: is_string,
|
||||
'str': is_string,
|
||||
'string': is_string,
|
||||
bool: is_boolean,
|
||||
'bool': is_boolean,
|
||||
'boolean': is_boolean,
|
||||
}
|
||||
|
||||
def is_mixed_list(value, *args):
|
||||
try: length = len(value)
|
||||
except TypeError: raise VdtTypeError(value)
|
||||
if length < len(args): raise VdtValueTooShortError(value)
|
||||
elif length > len(args): raise VdtValueTooLongError(value)
|
||||
try: return [fun_dict[arg](val) for arg, val in zip(args, value)]
|
||||
except KeyError as cause: raise VdtParamError('mixed_list', cause)
|
||||
|
||||
|
||||
def is_option(value, *options):
|
||||
if not isinstance(value, str): raise VdtTypeError(value)
|
||||
if not value in options: raise VdtValueError(value)
|
||||
return value
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: 6d7f4aac8313ba495ab156ec11ab15c0
|
||||
config: d77ea3044971177c0a1e0c192353c10a
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
Git Over Reticulum
|
||||
******************
|
||||
|
||||
A set of utilities for distributed collaborative software development and publishing is included in RNS.
|
||||
A set of utilities for distributed collaborative software development and publishing are included in RNS.
|
||||
|
||||
The system consists of two parts: The ``rngit`` node that hosts repositories, and the ``git-remote-rns`` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: ``rns://DESTINATION_HASH/group/repo``.
|
||||
|
||||
If you set a branch to track a Reticulum remote as the default upstream, you can simply use ``git`` as you normally would; all commands work transparently and as expected.
|
||||
|
||||
.. warning::
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, ``rngit`` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible `permission system`_ for allowing many users to interact with many different repositories on a single node, ``rngit`` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
|
||||
.. _permission system: #permissions
|
||||
|
||||
The rngit Utility
|
||||
=================
|
||||
@@ -31,7 +33,7 @@ Run ``rngit`` to start a repository node:
|
||||
|
||||
On the first run, ``rngit`` will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.
|
||||
|
||||
View your identity and destination hashes:
|
||||
Them, view your identity and destination hashes:
|
||||
|
||||
.. code:: text
|
||||
|
||||
@@ -73,6 +75,13 @@ Get changes from a remote repository:
|
||||
|
||||
$ git pull rns_remote master
|
||||
|
||||
Fork an existing repository from a remote to your ``rngit`` node:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit fork rns://8a37cdd16938ce79861561adbd59023a/reticulum/lxmf rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
|
||||
**All Command-Line Options (rngit)**
|
||||
|
||||
.. code:: text
|
||||
@@ -104,12 +113,223 @@ The ``git-remote-rns`` helper is automatically invoked by Git when interacting w
|
||||
The client configuration file is located at ``~/.rngit/client_config`` and allows adjusting parameters such as the reference batch size for transfers.
|
||||
|
||||
|
||||
Repository Creation & Management
|
||||
================================
|
||||
|
||||
The ``rngit`` utility provides several ways to create and manage repositories on a node: creating empty repositories, forking from existing repositories, and mirroring remote repositories.
|
||||
|
||||
Creating Empty Repositories
|
||||
---------------------------
|
||||
|
||||
To create a new empty repository on a remote node:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit create rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Repository public/myrepo created
|
||||
|
||||
This creates a bare Git repository at the specified path. You must have ``create`` permission for the target group. When a repository is created, the creator automatically receives ``adm`` (admin) permissions on the repository through an auto-generated ``.allowed`` file.
|
||||
|
||||
**All Command-Line Options (rngit create)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit create [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
repository
|
||||
|
||||
Reticulum Git Repository Creation
|
||||
|
||||
positional arguments:
|
||||
repository URL of repository to create
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
Forking Repositories
|
||||
--------------------
|
||||
|
||||
Forking creates a copy of an existing repository (from any accessible Git URL) on your ``rngit`` node. Forks maintain a reference to their upstream source for later synchronization.
|
||||
|
||||
To fork a repository:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit fork https://github.com/user/original rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
Repository forked to public/myfork
|
||||
|
||||
The source can be any valid Git URL, including:
|
||||
|
||||
- HTTPS URLs: ``https://github.com/user/repo.git``
|
||||
- Git URLs: ``git://host.com/repo.git``
|
||||
- SSH URLs: ``ssh://git@host.com/repo.git``
|
||||
- Reticulum URLs: ``rns://DESTINATION_HASH/group/repo``
|
||||
- Local paths: ``/path/to/repo.git``
|
||||
|
||||
Forks are created as bare repositories with metadata tracking their origin. The fork process:
|
||||
|
||||
1. Creates a new bare repository
|
||||
2. Fetches all refs (``+refs/*:refs/*``) from the source
|
||||
3. Sets ``repository.rngit.type`` to ``fork``
|
||||
4. Sets ``repository.rngit.upstream.source`` to the source URL
|
||||
5. Grants creator admin permissions
|
||||
|
||||
**All Command-Line Options (rngit fork)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit fork [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
source target
|
||||
|
||||
Reticulum Git Repository Forker
|
||||
|
||||
positional arguments:
|
||||
source URL of source repository
|
||||
target URL of target repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
Mirroring Repositories
|
||||
----------------------
|
||||
|
||||
Mirrors are similar to forks but are designed for keeping a local copy synchronized with an upstream repository. Mirrors can be automatically updated on a configurable schedule.
|
||||
|
||||
To create a mirror:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit mirror https://github.com/user/upstream rns://50824b711717f97c2fb1166ceddd5ea9/public/mymirror
|
||||
|
||||
Repository mirrored to public/mymirror
|
||||
|
||||
Mirrors are created with the same process as forks, but with ``repository.rngit.type`` set to ``mirror`` and an additional ``repository.rngit.upstream.sync`` timestamp tracking the last successful synchronization.
|
||||
|
||||
**All Command-Line Options (rngit mirror)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit mirror [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
source target
|
||||
|
||||
Reticulum Git Mirror Management
|
||||
|
||||
positional arguments:
|
||||
source URL of source repository
|
||||
target URL of target repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
Automatic Mirror Synchronization
|
||||
--------------------------------
|
||||
|
||||
The ``rngit`` node can automatically keep mirrors synchronized with their upstream sources. This is configured in the main configuration file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[rngit]
|
||||
mirror_interval = 24
|
||||
|
||||
The ``mirror_interval`` specifies the synchronization interval in hours (default: 24). The node checks for mirrors needing sync every 15 minutes, and fetches updates from upstream if the configured interval has elapsed since the last sync.
|
||||
|
||||
For automatic sync to happen, the repository must have been created with ``rngit mirror``. Sync failures are logged but do not prevent future retry attempts. The sync timestamp is only updated on successful completion.
|
||||
|
||||
Manual Synchronization
|
||||
----------------------
|
||||
|
||||
Both forks and mirrors can be manually synchronized on demand using the ``sync`` command:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit sync rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
Repository synced
|
||||
|
||||
This fetches all refs from the upstream source configured when the repository was created. You must have ``read`` and ``write`` permissions for the repository to perform a manual sync.
|
||||
|
||||
For mirrors, manual sync also updates the sync timestamp, effectively resetting the automatic sync timer.
|
||||
|
||||
**All Command-Line Options (rngit sync)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit sync [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
repository
|
||||
|
||||
Reticulum Git Repository Syncer
|
||||
|
||||
positional arguments:
|
||||
repository URL of repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
Git Configuration Parameters
|
||||
----------------------------
|
||||
|
||||
Repositories created through ``rngit`` store metadata in Git configuration:
|
||||
|
||||
- ``repository.rngit.type`` - Either ``fork`` or ``mirror``
|
||||
- ``repository.rngit.upstream.source`` - The source URL used during creation
|
||||
- ``repository.rngit.upstream.sync`` - Unix timestamp of last successful sync for mirrors
|
||||
|
||||
These parameters are used by the sync system and can be queried using standard Git commands:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git config --get repository.rngit.type
|
||||
mirror
|
||||
|
||||
$ git config --get repository.rngit.upstream.source
|
||||
https://github.com/user/upstream
|
||||
|
||||
$ git config --get repository.rngit.upstream.sync
|
||||
1716230400
|
||||
|
||||
|
||||
|
||||
|
||||
Repository Structure
|
||||
====================
|
||||
|
||||
The ``rngit`` node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is ``group_name/repo_name``. For example, a repository at ``/var/git/public/myrepo`` would be accessible as ``public/myrepo`` via the URL ``rns://DESTINATION_HASH/public/myrepo``.
|
||||
|
||||
**Configuration**
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
The ``rngit`` node configuration file is located at ``~/.rngit/config`` (or ``/etc/rngit/config`` for system-wide installations). The default configuration includes:
|
||||
|
||||
@@ -118,20 +338,203 @@ The ``rngit`` node configuration file is located at ``~/.rngit/config`` (or ``/e
|
||||
- Announce intervals for network visibility
|
||||
- Optional statistics recording for repository activity
|
||||
|
||||
Access permissions can be configured at the group level in the config file, or per-repository using ``.allowed`` files. Permissions use the format ``permission:target`` where permission is ``r`` (read), ``w`` (write), ``rw`` (read/write), ``c`` (create) or ``s`` (stats) and target is ``all``, ``none``, or a specific identity hash.
|
||||
|
||||
The ``s`` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set ``record_stats = yes`` in the ``[rngit]`` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to ``stats_ignore_identities``.
|
||||
Permissions
|
||||
===========
|
||||
|
||||
Repository-specific ``.allowed`` files can be static text files or executable scripts that output permission rules to stdout. A ``group.allowed`` file in a repository group directory applies to all repositories within that group.
|
||||
The ``rngit`` permission system provides fine-grained access control at multiple levels: group-level, repository-level, and document-level. Permissions can be statically configured in files or dynamically generated via executable scripts.
|
||||
|
||||
Access permissions can be configured at the group level in the config file or per-group ``.allowed`` files, or per-repository ``.allowed`` files. The ``s`` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set ``record_stats = yes`` in the ``[rngit]`` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to ``stats_ignore_identities``.
|
||||
|
||||
By default, **no** permissions are granted for anything! You will have to enable the permissions you require to be able to actually *do* something with ``rngit``.
|
||||
|
||||
Permissions can be modified by editing the ``rngit`` config file, individual ``.allowed`` files on disk, or remotely using the ``rngit perms`` command.
|
||||
|
||||
Permission Types
|
||||
----------------
|
||||
|
||||
The following permissions are supported:
|
||||
|
||||
- ``r`` (read) - Clone, fetch, and view repositories and work documents
|
||||
- ``w`` (write) - Push changes and manage work documents
|
||||
- ``rw`` (read/write) - Combined read and write access
|
||||
- ``c`` (create) - Create, fork or mirror new repositories within a group
|
||||
- ``s`` (stats) - View repository activity statistics
|
||||
- ``rel`` (release) - Create and manage releases
|
||||
- ``i`` (interact) - Comment on and interact with work documents
|
||||
- ``p`` (propose) - Propose new work documents (without full write access)
|
||||
- ``adm`` (admin) - Full access
|
||||
|
||||
Permission targets can be:
|
||||
|
||||
- ``all`` or ``a`` - Everyone
|
||||
- ``none`` or ``n`` - Nobody
|
||||
- A specific Reticulum identity hash
|
||||
|
||||
Permission Hierarchy
|
||||
--------------------
|
||||
|
||||
Permissions are resolved in the following hierarchy:
|
||||
|
||||
1. **Repository-level permissions** - Checked first, if none exists group permissions are checked
|
||||
2. **Group-level permissions** - Used as fallback if no repository-level permissions are set
|
||||
3. **Admin override** - Finally, potential admin rights are checked
|
||||
|
||||
For work documents, work document specific permissions are always checked first, and work documents have additional specific checks such as modifications only being possible by the document author.
|
||||
|
||||
Configuration Methods
|
||||
---------------------
|
||||
|
||||
**Group-Level Configuration**
|
||||
|
||||
Group permissions can be configured in the ``[access]`` section of the main config file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[access]
|
||||
public = r:all, w:9710b86ba12c42d1d8f30f74fe509286
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
collaborative = r:all, i:all, p:all, w:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
Additionally, they can be configured in a group ``group_name.allowed`` file, placed next to the ``group_name`` group directory.
|
||||
|
||||
**Repository-Level Configuration**
|
||||
|
||||
Repository-specific permissions are set in ``.allowed`` files placed next to the repository directory (for example, ``myrepo.allowed`` for ``myrepo``):
|
||||
|
||||
.. code:: text
|
||||
|
||||
# myrepo.allowed
|
||||
r:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
rel:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
**Dynamic Permissions**
|
||||
|
||||
Permission files can be made executable to generate permissions dynamically:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ chmod +x myrepo.allowed
|
||||
|
||||
When executable, the script is run and its stdout is parsed as permission rules. This allows integration with external authentication systems.
|
||||
|
||||
Work Document Permissions
|
||||
-------------------------
|
||||
|
||||
Work documents support additional permission granularity through ``.allowed`` files in the work directory (e.g., ``42.allowed`` for document #42). These files use the same permission syntax but only support:
|
||||
|
||||
- ``r`` (read) - View the document
|
||||
- ``w`` (write) - Edit the document
|
||||
- ``i`` (interact) - Comment on the document
|
||||
- ``p`` (propose) - Propose changes (future use)
|
||||
- ``adm`` (admin) - Full control over the document
|
||||
|
||||
Document permissions override repository permissions for that specific document. Work document permissions can be updated simply by editing the ``.allowed`` file, or remotely by using the ``rngit work`` command.
|
||||
|
||||
Creator Permissions
|
||||
-------------------
|
||||
|
||||
When a user creates a repository (via ``create``, ``fork``, or ``mirror``), they are automatically granted ``adm`` (admin) permissions on that repository.
|
||||
|
||||
When a user creates a work document, they automatically receive ``interact`` and ``write`` permissions on that document.
|
||||
|
||||
Permission Examples
|
||||
-------------------
|
||||
|
||||
**Example 1: Public Read, Restricted Write**
|
||||
|
||||
.. code:: text
|
||||
|
||||
r:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
Everyone can read, only the specified identity can write.
|
||||
|
||||
**Example 2: Collaborative Development**
|
||||
|
||||
.. code:: text
|
||||
|
||||
r:all
|
||||
i:all
|
||||
p:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
rel:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
Everyone can read, interact (comment), and propose work documents. Only the specified identity can write, create releases, and manage work documents fully.
|
||||
|
||||
**Example 3: Private Repository**
|
||||
|
||||
.. code:: text
|
||||
|
||||
rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
rw:a1b2c3d4e5f686ba12c42d1ba12ef1aa
|
||||
|
||||
Only the two specified identities have any access (read or write).
|
||||
|
||||
**Example 4: Mirror with Stats**
|
||||
|
||||
.. code:: text
|
||||
|
||||
r:all
|
||||
s:all
|
||||
w:none
|
||||
|
||||
Everyone can read and view stats, but nobody can push (mirror is read-only from upstream).
|
||||
|
||||
Permission Short Forms
|
||||
----------------------
|
||||
|
||||
Permissions can be specified using short or long forms:
|
||||
|
||||
- ``r`` = ``read``
|
||||
- ``w`` = ``write``
|
||||
- ``rw`` = ``readwrite``
|
||||
- ``c`` = ``create``
|
||||
- ``s`` = ``stats``
|
||||
- ``rel`` = ``release``
|
||||
- ``i`` = ``interact``
|
||||
- ``p`` = ``propose``
|
||||
- ``adm`` = ``admin``
|
||||
|
||||
Targets can also use short forms:
|
||||
|
||||
- ``a`` = ``all`` = ``everyone``
|
||||
- ``n`` = ``none`` = ``nobody``
|
||||
|
||||
Permission Configuration Locations
|
||||
----------------------------------
|
||||
|
||||
- User install: ``~/.rngit/config``
|
||||
- System install: ``/etc/rngit/config``
|
||||
- Group permissions: ``<group_root>/<group_name>.allowed``
|
||||
- Repository permissions: ``<group_root>/<group_name>/<repo_name>.allowed``
|
||||
- Document permissions: ``<group_root>/<group_name>.work/<doc_id>.allowed``
|
||||
|
||||
Identity & Destination Aliases
|
||||
==============================
|
||||
|
||||
To make permission and remote destination management easier, you can locally define aliases for commonly used identity and destination hashes. Identity aliases used in permissions resolution can be defined in the ``[aliases]`` section of the ``~/.rngit/config`` file, while destination aliases are defined in the ``[aliases]`` section of the ``~/.rngit/client_config`` file.
|
||||
|
||||
All alias definitions take the form of ``aliased_name = HASH``:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[aliases]
|
||||
alice = d09285e660cfe27cee6d9a0beb58b7e0
|
||||
bob = ffcffb4e255e156e77f79b82c13086a6
|
||||
|
||||
**Aliases are always resolved locally!** If for example you fork a repository with ``rngit fork rns://bobs_node/public/repo_name rns://my_node/forks/repo_name``, the forked repository will of course still reference the full, original destination hash, and use this for subsequent upstream syncs.
|
||||
|
||||
Serving Pages Over Nomad Network
|
||||
================================
|
||||
|
||||
In addition to providing Git repository access via the Git remote helper protocol, ``rngit`` can also run a `Nomad Network <https://github.com/markqvist/nomadnet>`_ compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
|
||||
In addition to providing Git repository access via the Git remote helper protocol and command-line tools, ``rngit`` can also run a `Nomad Network <https://github.com/markqvist/nomadnet>`_ compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
|
||||
|
||||
When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.
|
||||
|
||||
**Enabling the Git Page Node**
|
||||
Enabling the Git Page Node
|
||||
--------------------------
|
||||
|
||||
To enable the page node, add the following to your ``~/.rngit/config`` file:
|
||||
|
||||
@@ -151,7 +554,8 @@ When the page node is enabled, ``rngit`` will listen on a Nomad Network node des
|
||||
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
|
||||
|
||||
**Accessing Repository Pages**
|
||||
Accessing Repository Pages
|
||||
--------------------------
|
||||
|
||||
Once the page node is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:
|
||||
|
||||
@@ -168,7 +572,7 @@ Once the page node is running, you can access it from any Nomad Network client b
|
||||
All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.
|
||||
|
||||
Formatting & Syntax Highlighting
|
||||
================================
|
||||
--------------------------------
|
||||
|
||||
If the ``pygments`` Python module is installed on your system, the page node will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.
|
||||
|
||||
@@ -191,8 +595,10 @@ Code blocks in Markdown can include language hints for syntax highlighting:
|
||||
print("Hello, Reticulum!")
|
||||
```
|
||||
|
||||
You can use ``rawmu`` code blocks to render raw Micron inside Markdown files. If you create a code block with the language hint ``rawmu``, everything inside it will be treated as Micron directly.
|
||||
|
||||
Customizing Templates
|
||||
=====================
|
||||
---------------------
|
||||
|
||||
The page node uses a template system that allows complete customization of the generated pages. Templates are stored in the ``~/.rngit/templates/`` directory as Micron files.
|
||||
|
||||
@@ -233,11 +639,12 @@ By default, the page node uses Nerd Font icons. If you prefer simpler icons or y
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = yes
|
||||
|
||||
**Repository Statistics**
|
||||
Repository Statistics
|
||||
---------------------
|
||||
|
||||
When statistics recording is enabled (see the ``record_stats`` configuration option), the page node can display activity charts for each repository. The statistics page shows:
|
||||
|
||||
- Total and peak views, fetches and pushes
|
||||
- Total and peak views, downloads, fetches and pushes
|
||||
- Daily activity charts over a 90-day period
|
||||
- Combined activity visualization
|
||||
|
||||
@@ -247,28 +654,29 @@ To view statistics, a user must have the ``s`` (stats) permission for the reposi
|
||||
|
||||
The page node includes a "Thanks" feature that allows users to express appreciation for a repository. On each repository page, a "Thanks" link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.
|
||||
|
||||
**Configuration Example**
|
||||
Configuration Example
|
||||
---------------------
|
||||
|
||||
A complete page node configuration might look like this:
|
||||
A complete node configuration might look like this:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[rngit]
|
||||
node_name = My Git Node
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
node_name = My Git Node
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
|
||||
[repositories]
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
|
||||
[access]
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
|
||||
|
||||
Release Management
|
||||
@@ -276,7 +684,8 @@ Release Management
|
||||
|
||||
In addition to hosting Git repositories, ``rngit`` provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the ``rngit release`` subcommand, and are also viewable through the Nomad Network page interface.
|
||||
|
||||
**The Release Workflow**
|
||||
The Release Workflow
|
||||
--------------------
|
||||
|
||||
Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The ``rngit`` client will open your configured ``$EDITOR`` to compose release notes, then upload all artifacts to the remote repository node.
|
||||
|
||||
@@ -295,7 +704,8 @@ This will:
|
||||
|
||||
If no ``$EDITOR`` environment variable is set, ``rngit`` will try to use ``nano``, ``vim`` or ``vi``. The editor will show a template with instructions. Lines starting with ``#`` will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.
|
||||
|
||||
**Release Storage & Structure**
|
||||
Release Storage & Structure
|
||||
---------------------------
|
||||
|
||||
Releases are stored on the node in a directory named ``repo_name.releases`` next to the bare repository. Each release is a subdirectory containing:
|
||||
|
||||
@@ -304,6 +714,10 @@ Releases are stored on the node in a directory named ``repo_name.releases`` next
|
||||
- ``artifacts/`` - All uploaded files
|
||||
- ``THANKS`` - Appreciation count from users
|
||||
|
||||
|
||||
Command-Line Interaction
|
||||
------------------------
|
||||
|
||||
**Listing Releases**
|
||||
|
||||
To view all releases for a repository:
|
||||
@@ -375,8 +789,6 @@ Release management requires the ``release`` permission, configured the same way
|
||||
|
||||
When the Nomad Network page node is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.
|
||||
|
||||
Only releases with ``published`` status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.
|
||||
|
||||
**All Command-Line Options (rngit release)**
|
||||
|
||||
.. code:: text
|
||||
@@ -411,6 +823,9 @@ Work Documents
|
||||
|
||||
In addition to releases, ``rngit`` provides a work document management system for tracking tasks, investigations, issues and progress related to repositories. Work documents are stored as structured msgpack data and support threaded updates and comments.
|
||||
|
||||
Working With Work Documents
|
||||
---------------------------
|
||||
|
||||
**Listing Work Documents**
|
||||
|
||||
To view work documents for a repository:
|
||||
@@ -427,7 +842,7 @@ To view work documents for a repository:
|
||||
1 Implemented new feature 9710b86ba12c4f2e… 2025-01-15 14:32 3
|
||||
2 Fixed bug in parser 8f3a21c9d84e927b… 2025-01-14 09:15 1
|
||||
|
||||
Use ``--scope completed`` to view completed work documents, or ``--scope all`` to see both active and completed.
|
||||
Use ``--scope completed`` to view completed work documents, ``--scope proposed`` to view proposed documents, or ``--scope all`` to see all scopes.
|
||||
|
||||
**Viewing a Work Document**
|
||||
|
||||
@@ -485,6 +900,34 @@ To add an update to a work document:
|
||||
|
||||
This opens your editor to compose the update.
|
||||
|
||||
Proposing Work Documents
|
||||
------------------------
|
||||
|
||||
Users with ``propose`` permission can create work document proposals without full ``write`` access. Proposals are created in a "proposed" state and must be activated by a user with appropriate permissions before becoming active.
|
||||
|
||||
To propose a work document:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo propose --title "Feature proposal"
|
||||
|
||||
This opens your editor to compose the proposal content. When saved, the document is created in the "proposed" scope. The creator automatically receives ``interact`` and ``write`` permissions on the proposed document.
|
||||
|
||||
Proposed documents are visible through ``--scope proposed`` or ``--scope all``:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list --scope proposed
|
||||
|
||||
**Permissions for Proposals**
|
||||
|
||||
- Creating proposals requires ``propose`` permission on the repository
|
||||
- The creator automatically gets ``interact`` and ``write`` on their proposed document
|
||||
- Activating a proposal requires ``write`` and ``interact`` permissions
|
||||
|
||||
State Management
|
||||
----------------
|
||||
|
||||
**Completing Work Documents**
|
||||
|
||||
To mark a work document as completed (moving it from ``active`` to ``completed``):
|
||||
@@ -516,20 +959,51 @@ To delete a work document and all its comments:
|
||||
Are you sure you want to delete active work document #1? [y/N]: y
|
||||
Work document #1 deleted
|
||||
|
||||
**Permissions**
|
||||
Managing Work Document Permissions
|
||||
----------------------------------
|
||||
|
||||
Users can view work documents and updates if the have ``read`` permission for the repository. If users have ``read`` and ``interact``, they can also post updates/comments on existing work documents. Work document management requires having ``write`` and ``interact`` permission to the repository. These permissions are configured the same way as any other repository permissions. In the config file or ``.allowed`` files, use ``i:target`` to grant work document interaction rights:
|
||||
Users with administrative access to a work document can manage its specific permissions. This allows fine-grained control over who can read, write, comment on, or administer individual work documents.
|
||||
|
||||
To view or edit permissions for a work document:
|
||||
|
||||
.. code:: text
|
||||
|
||||
# In .allowed file or config
|
||||
i:all # Allow everyone
|
||||
i:9710b86... # Allow specific identity
|
||||
i:none # Deny everyone
|
||||
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo perms -d 1
|
||||
|
||||
**Author Verification**
|
||||
This opens your editor with the current permission configuration:
|
||||
|
||||
Users can only edit or delete work documents and updates they created. The author is cryptographically verified from the interacting link's ``remote_identity``.
|
||||
.. code:: text
|
||||
|
||||
r:all
|
||||
i:9710b86ba12c42d1d8f30f74fe509286
|
||||
adm:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
Permission rules follow the same format as repository permissions:
|
||||
|
||||
- ``r:target`` - Grant read access
|
||||
- ``w:target`` - Grant write access
|
||||
- ``i:target`` - Grant interact (comment) access
|
||||
- ``adm:target`` - Grant admin access
|
||||
|
||||
Targets can be ``all``, ``none``, or a specific identity hash.
|
||||
|
||||
**Who Can Edit Permissions**
|
||||
|
||||
Document permissions can be edited by:
|
||||
|
||||
- The original author (if they also have ``interact`` and ``write`` on the repository)
|
||||
- Any user with ``admin`` permission on the document
|
||||
- Repository admins (through inherited permissions)
|
||||
|
||||
**Permission Precedence**
|
||||
|
||||
Document-specific permissions override repository-level permissions for that document. If document permissions exist, they are checked first; if access is not granted there, repository permissions are checked.
|
||||
|
||||
**Author Rights:**
|
||||
|
||||
- Users can only edit or delete work documents they created
|
||||
- The author is cryptographically verified from the interacting link's ``remote_identity``
|
||||
- Document creators automatically receive ``interact`` and ``write`` on their documents
|
||||
|
||||
**Storage Format**
|
||||
|
||||
@@ -537,6 +1011,7 @@ Work documents are stored in a ``repo_name.work`` directory next to the reposito
|
||||
|
||||
- ``active/`` - Active work documents
|
||||
- ``completed/`` - Completed work documents
|
||||
- ``proposed/`` - Proposed work documents
|
||||
|
||||
Each document is a numbered directory containing:
|
||||
|
||||
@@ -560,7 +1035,8 @@ When the Nomad Network page node is enabled, work documents are viewable through
|
||||
|
||||
positional arguments:
|
||||
repository URL of remote repository
|
||||
operation list, view, create, edit, delete, update or complete
|
||||
operation list, view, create, propose, edit, delete,
|
||||
update, complete, activate or perms
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -568,8 +1044,8 @@ When the Nomad Network page node is enabled, work documents are viewable through
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
--scope SCOPE document scope: active, completed or all
|
||||
-t, --title TITLE document title for create
|
||||
--scope SCOPE document scope: active, completed, proposed or all
|
||||
-t, --title TITLE document title for create/propose
|
||||
-d, --id ID document ID
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DOCUMENTATION_OPTIONS = {
|
||||
VERSION: '1.2.6',
|
||||
VERSION: '1.2.8',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Code Examples - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Code Examples - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -3664,7 +3664,7 @@ will be fully on-par with natively included interfaces, including all supported
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -295,7 +295,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#"><link rel="search" title="Search" href="search.html">
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -178,7 +178,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -202,7 +202,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -839,7 +839,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Getting Started Fast - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Getting Started Fast - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -967,7 +967,7 @@ All other available modules will still be loaded when needed.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
+492
-48
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Git Over Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Git Over Reticulum - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -263,12 +263,12 @@
|
||||
<article role="main" id="furo-main-content">
|
||||
<section id="git-over-reticulum">
|
||||
<span id="git-main"></span><h1>Git Over Reticulum<a class="headerlink" href="#git-over-reticulum" title="Link to this heading">¶</a></h1>
|
||||
<p>A set of utilities for distributed collaborative software development and publishing is included in RNS.</p>
|
||||
<p>A set of utilities for distributed collaborative software development and publishing are included in RNS.</p>
|
||||
<p>The system consists of two parts: The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node that hosts repositories, and the <code class="docutils literal notranslate"><span class="pre">git-remote-rns</span></code> helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: <code class="docutils literal notranslate"><span class="pre">rns://DESTINATION_HASH/group/repo</span></code>.</p>
|
||||
<p>If you set a branch to track a Reticulum remote as the default upstream, you can simply use <code class="docutils literal notranslate"><span class="pre">git</span></code> as you normally would; all commands work transparently and as expected.</p>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p><strong>The rngit program is a new addition to RNS!</strong> This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.</p>
|
||||
<p><strong>The rngit program is a new addition to RNS!</strong> This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible <a class="reference external" href="#permissions">permission system</a> for allowing many users to interact with many different repositories on a single node, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.</p>
|
||||
</div>
|
||||
<section id="the-rngit-utility">
|
||||
<h2>The rngit Utility<a class="headerlink" href="#the-rngit-utility" title="Link to this heading">¶</a></h2>
|
||||
@@ -282,7 +282,7 @@
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>On the first run, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.</p>
|
||||
<p>View your identity and destination hashes:</p>
|
||||
<p>Them, view your identity and destination hashes:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit --print-identity
|
||||
|
||||
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
|
||||
@@ -311,6 +311,10 @@ Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git pull rns_remote master
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Fork an existing repository from a remote to your <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit fork rns://8a37cdd16938ce79861561adbd59023a/reticulum/lxmf rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>All Command-Line Options (rngit)</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit.py [-h] [--config CONFIG] [--rnsconfig RNSCONFIG] [-s] [-i] [-v]
|
||||
[-q] [--version]
|
||||
@@ -338,10 +342,189 @@ options:
|
||||
</ul>
|
||||
<p>The client configuration file is located at <code class="docutils literal notranslate"><span class="pre">~/.rngit/client_config</span></code> and allows adjusting parameters such as the reference batch size for transfers.</p>
|
||||
</section>
|
||||
<section id="repository-creation-management">
|
||||
<h2>Repository Creation & Management<a class="headerlink" href="#repository-creation-management" title="Link to this heading">¶</a></h2>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> utility provides several ways to create and manage repositories on a node: creating empty repositories, forking from existing repositories, and mirroring remote repositories.</p>
|
||||
<section id="creating-empty-repositories">
|
||||
<h3>Creating Empty Repositories<a class="headerlink" href="#creating-empty-repositories" title="Link to this heading">¶</a></h3>
|
||||
<p>To create a new empty repository on a remote node:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit create rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Repository public/myrepo created
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This creates a bare Git repository at the specified path. You must have <code class="docutils literal notranslate"><span class="pre">create</span></code> permission for the target group. When a repository is created, the creator automatically receives <code class="docutils literal notranslate"><span class="pre">adm</span></code> (admin) permissions on the repository through an auto-generated <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> file.</p>
|
||||
<p><strong>All Command-Line Options (rngit create)</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit create [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
repository
|
||||
|
||||
Reticulum Git Repository Creation
|
||||
|
||||
positional arguments:
|
||||
repository URL of repository to create
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="forking-repositories">
|
||||
<h3>Forking Repositories<a class="headerlink" href="#forking-repositories" title="Link to this heading">¶</a></h3>
|
||||
<p>Forking creates a copy of an existing repository (from any accessible Git URL) on your <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node. Forks maintain a reference to their upstream source for later synchronization.</p>
|
||||
<p>To fork a repository:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit fork https://github.com/user/original rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
Repository forked to public/myfork
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The source can be any valid Git URL, including:</p>
|
||||
<ul class="simple">
|
||||
<li><p>HTTPS URLs: <code class="docutils literal notranslate"><span class="pre">https://github.com/user/repo.git</span></code></p></li>
|
||||
<li><p>Git URLs: <code class="docutils literal notranslate"><span class="pre">git://host.com/repo.git</span></code></p></li>
|
||||
<li><p>SSH URLs: <code class="docutils literal notranslate"><span class="pre">ssh://git@host.com/repo.git</span></code></p></li>
|
||||
<li><p>Reticulum URLs: <code class="docutils literal notranslate"><span class="pre">rns://DESTINATION_HASH/group/repo</span></code></p></li>
|
||||
<li><p>Local paths: <code class="docutils literal notranslate"><span class="pre">/path/to/repo.git</span></code></p></li>
|
||||
</ul>
|
||||
<p>Forks are created as bare repositories with metadata tracking their origin. The fork process:</p>
|
||||
<ol class="arabic simple">
|
||||
<li><p>Creates a new bare repository</p></li>
|
||||
<li><p>Fetches all refs (<code class="docutils literal notranslate"><span class="pre">+refs/*:refs/*</span></code>) from the source</p></li>
|
||||
<li><p>Sets <code class="docutils literal notranslate"><span class="pre">repository.rngit.type</span></code> to <code class="docutils literal notranslate"><span class="pre">fork</span></code></p></li>
|
||||
<li><p>Sets <code class="docutils literal notranslate"><span class="pre">repository.rngit.upstream.source</span></code> to the source URL</p></li>
|
||||
<li><p>Grants creator admin permissions</p></li>
|
||||
</ol>
|
||||
<p><strong>All Command-Line Options (rngit fork)</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit fork [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
source target
|
||||
|
||||
Reticulum Git Repository Forker
|
||||
|
||||
positional arguments:
|
||||
source URL of source repository
|
||||
target URL of target repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="mirroring-repositories">
|
||||
<h3>Mirroring Repositories<a class="headerlink" href="#mirroring-repositories" title="Link to this heading">¶</a></h3>
|
||||
<p>Mirrors are similar to forks but are designed for keeping a local copy synchronized with an upstream repository. Mirrors can be automatically updated on a configurable schedule.</p>
|
||||
<p>To create a mirror:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit mirror https://github.com/user/upstream rns://50824b711717f97c2fb1166ceddd5ea9/public/mymirror
|
||||
|
||||
Repository mirrored to public/mymirror
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Mirrors are created with the same process as forks, but with <code class="docutils literal notranslate"><span class="pre">repository.rngit.type</span></code> set to <code class="docutils literal notranslate"><span class="pre">mirror</span></code> and an additional <code class="docutils literal notranslate"><span class="pre">repository.rngit.upstream.sync</span></code> timestamp tracking the last successful synchronization.</p>
|
||||
<p><strong>All Command-Line Options (rngit mirror)</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit mirror [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
source target
|
||||
|
||||
Reticulum Git Mirror Management
|
||||
|
||||
positional arguments:
|
||||
source URL of source repository
|
||||
target URL of target repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="automatic-mirror-synchronization">
|
||||
<h3>Automatic Mirror Synchronization<a class="headerlink" href="#automatic-mirror-synchronization" title="Link to this heading">¶</a></h3>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node can automatically keep mirrors synchronized with their upstream sources. This is configured in the main configuration file:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[rngit]
|
||||
mirror_interval = 24
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">mirror_interval</span></code> specifies the synchronization interval in hours (default: 24). The node checks for mirrors needing sync every 15 minutes, and fetches updates from upstream if the configured interval has elapsed since the last sync.</p>
|
||||
<p>For automatic sync to happen, the repository must have been created with <code class="docutils literal notranslate"><span class="pre">rngit</span> <span class="pre">mirror</span></code>. Sync failures are logged but do not prevent future retry attempts. The sync timestamp is only updated on successful completion.</p>
|
||||
</section>
|
||||
<section id="manual-synchronization">
|
||||
<h3>Manual Synchronization<a class="headerlink" href="#manual-synchronization" title="Link to this heading">¶</a></h3>
|
||||
<p>Both forks and mirrors can be manually synchronized on demand using the <code class="docutils literal notranslate"><span class="pre">sync</span></code> command:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit sync rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
Repository synced
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This fetches all refs from the upstream source configured when the repository was created. You must have <code class="docutils literal notranslate"><span class="pre">read</span></code> and <code class="docutils literal notranslate"><span class="pre">write</span></code> permissions for the repository to perform a manual sync.</p>
|
||||
<p>For mirrors, manual sync also updates the sync timestamp, effectively resetting the automatic sync timer.</p>
|
||||
<p><strong>All Command-Line Options (rngit sync)</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit sync [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
repository
|
||||
|
||||
Reticulum Git Repository Syncer
|
||||
|
||||
positional arguments:
|
||||
repository URL of repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="git-configuration-parameters">
|
||||
<h3>Git Configuration Parameters<a class="headerlink" href="#git-configuration-parameters" title="Link to this heading">¶</a></h3>
|
||||
<p>Repositories created through <code class="docutils literal notranslate"><span class="pre">rngit</span></code> store metadata in Git configuration:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">repository.rngit.type</span></code> - Either <code class="docutils literal notranslate"><span class="pre">fork</span></code> or <code class="docutils literal notranslate"><span class="pre">mirror</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">repository.rngit.upstream.source</span></code> - The source URL used during creation</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">repository.rngit.upstream.sync</span></code> - Unix timestamp of last successful sync for mirrors</p></li>
|
||||
</ul>
|
||||
<p>These parameters are used by the sync system and can be queried using standard Git commands:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git config --get repository.rngit.type
|
||||
mirror
|
||||
|
||||
$ git config --get repository.rngit.upstream.source
|
||||
https://github.com/user/upstream
|
||||
|
||||
$ git config --get repository.rngit.upstream.sync
|
||||
1716230400
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section id="repository-structure">
|
||||
<h2>Repository Structure<a class="headerlink" href="#repository-structure" title="Link to this heading">¶</a></h2>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is <code class="docutils literal notranslate"><span class="pre">group_name/repo_name</span></code>. For example, a repository at <code class="docutils literal notranslate"><span class="pre">/var/git/public/myrepo</span></code> would be accessible as <code class="docutils literal notranslate"><span class="pre">public/myrepo</span></code> via the URL <code class="docutils literal notranslate"><span class="pre">rns://DESTINATION_HASH/public/myrepo</span></code>.</p>
|
||||
<p><strong>Configuration</strong></p>
|
||||
<section id="configuration">
|
||||
<h3>Configuration<a class="headerlink" href="#configuration" title="Link to this heading">¶</a></h3>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node configuration file is located at <code class="docutils literal notranslate"><span class="pre">~/.rngit/config</span></code> (or <code class="docutils literal notranslate"><span class="pre">/etc/rngit/config</span></code> for system-wide installations). The default configuration includes:</p>
|
||||
<ul class="simple">
|
||||
<li><p>Repository group paths defining where to find bare repositories</p></li>
|
||||
@@ -349,15 +532,167 @@ options:
|
||||
<li><p>Announce intervals for network visibility</p></li>
|
||||
<li><p>Optional statistics recording for repository activity</p></li>
|
||||
</ul>
|
||||
<p>Access permissions can be configured at the group level in the config file, or per-repository using <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files. Permissions use the format <code class="docutils literal notranslate"><span class="pre">permission:target</span></code> where permission is <code class="docutils literal notranslate"><span class="pre">r</span></code> (read), <code class="docutils literal notranslate"><span class="pre">w</span></code> (write), <code class="docutils literal notranslate"><span class="pre">rw</span></code> (read/write), <code class="docutils literal notranslate"><span class="pre">c</span></code> (create) or <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) and target is <code class="docutils literal notranslate"><span class="pre">all</span></code>, <code class="docutils literal notranslate"><span class="pre">none</span></code>, or a specific identity hash.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set <code class="docutils literal notranslate"><span class="pre">record_stats</span> <span class="pre">=</span> <span class="pre">yes</span></code> in the <code class="docutils literal notranslate"><span class="pre">[rngit]</span></code> section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to <code class="docutils literal notranslate"><span class="pre">stats_ignore_identities</span></code>.</p>
|
||||
<p>Repository-specific <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files can be static text files or executable scripts that output permission rules to stdout. A <code class="docutils literal notranslate"><span class="pre">group.allowed</span></code> file in a repository group directory applies to all repositories within that group.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="permissions">
|
||||
<h2>Permissions<a class="headerlink" href="#permissions" title="Link to this heading">¶</a></h2>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> permission system provides fine-grained access control at multiple levels: group-level, repository-level, and document-level. Permissions can be statically configured in files or dynamically generated via executable scripts.</p>
|
||||
<p>Access permissions can be configured at the group level in the config file or per-group <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files, or per-repository <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files. The <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set <code class="docutils literal notranslate"><span class="pre">record_stats</span> <span class="pre">=</span> <span class="pre">yes</span></code> in the <code class="docutils literal notranslate"><span class="pre">[rngit]</span></code> section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to <code class="docutils literal notranslate"><span class="pre">stats_ignore_identities</span></code>.</p>
|
||||
<p>By default, <strong>no</strong> permissions are granted for anything! You will have to enable the permissions you require to be able to actually <em>do</em> something with <code class="docutils literal notranslate"><span class="pre">rngit</span></code>.</p>
|
||||
<p>Permissions can be modified by editing the <code class="docutils literal notranslate"><span class="pre">rngit</span></code> config file, individual <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files on disk, or remotely using the <code class="docutils literal notranslate"><span class="pre">rngit</span> <span class="pre">perms</span></code> command.</p>
|
||||
<section id="permission-types">
|
||||
<h3>Permission Types<a class="headerlink" href="#permission-types" title="Link to this heading">¶</a></h3>
|
||||
<p>The following permissions are supported:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">r</span></code> (read) - Clone, fetch, and view repositories and work documents</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">w</span></code> (write) - Push changes and manage work documents</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">rw</span></code> (read/write) - Combined read and write access</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">c</span></code> (create) - Create, fork or mirror new repositories within a group</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) - View repository activity statistics</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">rel</span></code> (release) - Create and manage releases</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">i</span></code> (interact) - Comment on and interact with work documents</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">p</span></code> (propose) - Propose new work documents (without full write access)</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">adm</span></code> (admin) - Full access</p></li>
|
||||
</ul>
|
||||
<p>Permission targets can be:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">all</span></code> or <code class="docutils literal notranslate"><span class="pre">a</span></code> - Everyone</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">none</span></code> or <code class="docutils literal notranslate"><span class="pre">n</span></code> - Nobody</p></li>
|
||||
<li><p>A specific Reticulum identity hash</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="permission-hierarchy">
|
||||
<h3>Permission Hierarchy<a class="headerlink" href="#permission-hierarchy" title="Link to this heading">¶</a></h3>
|
||||
<p>Permissions are resolved in the following hierarchy:</p>
|
||||
<ol class="arabic simple">
|
||||
<li><p><strong>Repository-level permissions</strong> - Checked first, if none exists group permissions are checked</p></li>
|
||||
<li><p><strong>Group-level permissions</strong> - Used as fallback if no repository-level permissions are set</p></li>
|
||||
<li><p><strong>Admin override</strong> - Finally, potential admin rights are checked</p></li>
|
||||
</ol>
|
||||
<p>For work documents, work document specific permissions are always checked first, and work documents have additional specific checks such as modifications only being possible by the document author.</p>
|
||||
</section>
|
||||
<section id="configuration-methods">
|
||||
<h3>Configuration Methods<a class="headerlink" href="#configuration-methods" title="Link to this heading">¶</a></h3>
|
||||
<p><strong>Group-Level Configuration</strong></p>
|
||||
<p>Group permissions can be configured in the <code class="docutils literal notranslate"><span class="pre">[access]</span></code> section of the main config file:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[access]
|
||||
public = r:all, w:9710b86ba12c42d1d8f30f74fe509286
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
collaborative = r:all, i:all, p:all, w:9710b86ba12c42d1d8f30f74fe509286
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Additionally, they can be configured in a group <code class="docutils literal notranslate"><span class="pre">group_name.allowed</span></code> file, placed next to the <code class="docutils literal notranslate"><span class="pre">group_name</span></code> group directory.</p>
|
||||
<p><strong>Repository-Level Configuration</strong></p>
|
||||
<p>Repository-specific permissions are set in <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files placed next to the repository directory (for example, <code class="docutils literal notranslate"><span class="pre">myrepo.allowed</span></code> for <code class="docutils literal notranslate"><span class="pre">myrepo</span></code>):</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span># myrepo.allowed
|
||||
r:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
rel:9710b86ba12c42d1d8f30f74fe509286
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Dynamic Permissions</strong></p>
|
||||
<p>Permission files can be made executable to generate permissions dynamically:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ chmod +x myrepo.allowed
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>When executable, the script is run and its stdout is parsed as permission rules. This allows integration with external authentication systems.</p>
|
||||
</section>
|
||||
<section id="work-document-permissions">
|
||||
<h3>Work Document Permissions<a class="headerlink" href="#work-document-permissions" title="Link to this heading">¶</a></h3>
|
||||
<p>Work documents support additional permission granularity through <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files in the work directory (e.g., <code class="docutils literal notranslate"><span class="pre">42.allowed</span></code> for document #42). These files use the same permission syntax but only support:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">r</span></code> (read) - View the document</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">w</span></code> (write) - Edit the document</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">i</span></code> (interact) - Comment on the document</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">p</span></code> (propose) - Propose changes (future use)</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">adm</span></code> (admin) - Full control over the document</p></li>
|
||||
</ul>
|
||||
<p>Document permissions override repository permissions for that specific document. Work document permissions can be updated simply by editing the <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> file, or remotely by using the <code class="docutils literal notranslate"><span class="pre">rngit</span> <span class="pre">work</span></code> command.</p>
|
||||
</section>
|
||||
<section id="creator-permissions">
|
||||
<h3>Creator Permissions<a class="headerlink" href="#creator-permissions" title="Link to this heading">¶</a></h3>
|
||||
<p>When a user creates a repository (via <code class="docutils literal notranslate"><span class="pre">create</span></code>, <code class="docutils literal notranslate"><span class="pre">fork</span></code>, or <code class="docutils literal notranslate"><span class="pre">mirror</span></code>), they are automatically granted <code class="docutils literal notranslate"><span class="pre">adm</span></code> (admin) permissions on that repository.</p>
|
||||
<p>When a user creates a work document, they automatically receive <code class="docutils literal notranslate"><span class="pre">interact</span></code> and <code class="docutils literal notranslate"><span class="pre">write</span></code> permissions on that document.</p>
|
||||
</section>
|
||||
<section id="permission-examples">
|
||||
<h3>Permission Examples<a class="headerlink" href="#permission-examples" title="Link to this heading">¶</a></h3>
|
||||
<p><strong>Example 1: Public Read, Restricted Write</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>r:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Everyone can read, only the specified identity can write.</p>
|
||||
<p><strong>Example 2: Collaborative Development</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>r:all
|
||||
i:all
|
||||
p:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
rel:9710b86ba12c42d1d8f30f74fe509286
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Everyone can read, interact (comment), and propose work documents. Only the specified identity can write, create releases, and manage work documents fully.</p>
|
||||
<p><strong>Example 3: Private Repository</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
rw:a1b2c3d4e5f686ba12c42d1ba12ef1aa
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Only the two specified identities have any access (read or write).</p>
|
||||
<p><strong>Example 4: Mirror with Stats</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>r:all
|
||||
s:all
|
||||
w:none
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Everyone can read and view stats, but nobody can push (mirror is read-only from upstream).</p>
|
||||
</section>
|
||||
<section id="permission-short-forms">
|
||||
<h3>Permission Short Forms<a class="headerlink" href="#permission-short-forms" title="Link to this heading">¶</a></h3>
|
||||
<p>Permissions can be specified using short or long forms:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">r</span></code> = <code class="docutils literal notranslate"><span class="pre">read</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">w</span></code> = <code class="docutils literal notranslate"><span class="pre">write</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">rw</span></code> = <code class="docutils literal notranslate"><span class="pre">readwrite</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">c</span></code> = <code class="docutils literal notranslate"><span class="pre">create</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">s</span></code> = <code class="docutils literal notranslate"><span class="pre">stats</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">rel</span></code> = <code class="docutils literal notranslate"><span class="pre">release</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">i</span></code> = <code class="docutils literal notranslate"><span class="pre">interact</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">p</span></code> = <code class="docutils literal notranslate"><span class="pre">propose</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">adm</span></code> = <code class="docutils literal notranslate"><span class="pre">admin</span></code></p></li>
|
||||
</ul>
|
||||
<p>Targets can also use short forms:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">a</span></code> = <code class="docutils literal notranslate"><span class="pre">all</span></code> = <code class="docutils literal notranslate"><span class="pre">everyone</span></code></p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">n</span></code> = <code class="docutils literal notranslate"><span class="pre">none</span></code> = <code class="docutils literal notranslate"><span class="pre">nobody</span></code></p></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="permission-configuration-locations">
|
||||
<h3>Permission Configuration Locations<a class="headerlink" href="#permission-configuration-locations" title="Link to this heading">¶</a></h3>
|
||||
<ul class="simple">
|
||||
<li><p>User install: <code class="docutils literal notranslate"><span class="pre">~/.rngit/config</span></code></p></li>
|
||||
<li><p>System install: <code class="docutils literal notranslate"><span class="pre">/etc/rngit/config</span></code></p></li>
|
||||
<li><p>Group permissions: <code class="docutils literal notranslate"><span class="pre"><group_root>/<group_name>.allowed</span></code></p></li>
|
||||
<li><p>Repository permissions: <code class="docutils literal notranslate"><span class="pre"><group_root>/<group_name>/<repo_name>.allowed</span></code></p></li>
|
||||
<li><p>Document permissions: <code class="docutils literal notranslate"><span class="pre"><group_root>/<group_name>.work/<doc_id>.allowed</span></code></p></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
<section id="identity-destination-aliases">
|
||||
<h2>Identity & Destination Aliases<a class="headerlink" href="#identity-destination-aliases" title="Link to this heading">¶</a></h2>
|
||||
<p>To make permission and remote destination management easier, you can locally define aliases for commonly used identity and destination hashes. Identity aliases used in permissions resolution can be defined in the <code class="docutils literal notranslate"><span class="pre">[aliases]</span></code> section of the <code class="docutils literal notranslate"><span class="pre">~/.rngit/config</span></code> file, while destination aliases are defined in the <code class="docutils literal notranslate"><span class="pre">[aliases]</span></code> section of the <code class="docutils literal notranslate"><span class="pre">~/.rngit/client_config</span></code> file.</p>
|
||||
<p>All alias definitions take the form of <code class="docutils literal notranslate"><span class="pre">aliased_name</span> <span class="pre">=</span> <span class="pre">HASH</span></code>:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[aliases]
|
||||
alice = d09285e660cfe27cee6d9a0beb58b7e0
|
||||
bob = ffcffb4e255e156e77f79b82c13086a6
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Aliases are always resolved locally!</strong> If for example you fork a repository with <code class="docutils literal notranslate"><span class="pre">rngit</span> <span class="pre">fork</span> <span class="pre">rns://bobs_node/public/repo_name</span> <span class="pre">rns://my_node/forks/repo_name</span></code>, the forked repository will of course still reference the full, original destination hash, and use this for subsequent upstream syncs.</p>
|
||||
</section>
|
||||
<section id="serving-pages-over-nomad-network">
|
||||
<h2>Serving Pages Over Nomad Network<a class="headerlink" href="#serving-pages-over-nomad-network" title="Link to this heading">¶</a></h2>
|
||||
<p>In addition to providing Git repository access via the Git remote helper protocol, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> can also run a <a class="reference external" href="https://github.com/markqvist/nomadnet">Nomad Network</a> compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.</p>
|
||||
<p>In addition to providing Git repository access via the Git remote helper protocol and command-line tools, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> can also run a <a class="reference external" href="https://github.com/markqvist/nomadnet">Nomad Network</a> compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.</p>
|
||||
<p>When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.</p>
|
||||
<p><strong>Enabling the Git Page Node</strong></p>
|
||||
<section id="enabling-the-git-page-node">
|
||||
<h3>Enabling the Git Page Node<a class="headerlink" href="#enabling-the-git-page-node" title="Link to this heading">¶</a></h3>
|
||||
<p>To enable the page node, add the following to your <code class="docutils literal notranslate"><span class="pre">~/.rngit/config</span></code> file:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[pages]
|
||||
serve_nomadnet = yes
|
||||
@@ -372,7 +707,9 @@ Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Accessing Repository Pages</strong></p>
|
||||
</section>
|
||||
<section id="accessing-repository-pages">
|
||||
<h3>Accessing Repository Pages<a class="headerlink" href="#accessing-repository-pages" title="Link to this heading">¶</a></h3>
|
||||
<p>Once the page node is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:</p>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Front Page</strong> - Lists all repository groups accessible to your identity</p></li>
|
||||
@@ -388,7 +725,7 @@ Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
|
||||
<p>All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.</p>
|
||||
</section>
|
||||
<section id="formatting-syntax-highlighting">
|
||||
<h2>Formatting & Syntax Highlighting<a class="headerlink" href="#formatting-syntax-highlighting" title="Link to this heading">¶</a></h2>
|
||||
<h3>Formatting & Syntax Highlighting<a class="headerlink" href="#formatting-syntax-highlighting" title="Link to this heading">¶</a></h3>
|
||||
<p>If the <code class="docutils literal notranslate"><span class="pre">pygments</span></code> Python module is installed on your system, the page node will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.</p>
|
||||
<p>To enable syntax highlighting, install pygments:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>pip install pygments
|
||||
@@ -403,9 +740,10 @@ def hello_world():
|
||||
```
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>You can use <code class="docutils literal notranslate"><span class="pre">rawmu</span></code> code blocks to render raw Micron inside Markdown files. If you create a code block with the language hint <code class="docutils literal notranslate"><span class="pre">rawmu</span></code>, everything inside it will be treated as Micron directly.</p>
|
||||
</section>
|
||||
<section id="customizing-templates">
|
||||
<h2>Customizing Templates<a class="headerlink" href="#customizing-templates" title="Link to this heading">¶</a></h2>
|
||||
<h3>Customizing Templates<a class="headerlink" href="#customizing-templates" title="Link to this heading">¶</a></h3>
|
||||
<p>The page node uses a template system that allows complete customization of the generated pages. Templates are stored in the <code class="docutils literal notranslate"><span class="pre">~/.rngit/templates/</span></code> directory as Micron files.</p>
|
||||
<p>The following template files are supported:</p>
|
||||
<ul class="simple">
|
||||
@@ -439,41 +777,47 @@ serve_nomadnet = yes
|
||||
unicode_icons = yes
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Repository Statistics</strong></p>
|
||||
</section>
|
||||
<section id="repository-statistics">
|
||||
<h3>Repository Statistics<a class="headerlink" href="#repository-statistics" title="Link to this heading">¶</a></h3>
|
||||
<p>When statistics recording is enabled (see the <code class="docutils literal notranslate"><span class="pre">record_stats</span></code> configuration option), the page node can display activity charts for each repository. The statistics page shows:</p>
|
||||
<ul class="simple">
|
||||
<li><p>Total and peak views, fetches and pushes</p></li>
|
||||
<li><p>Total and peak views, downloads, fetches and pushes</p></li>
|
||||
<li><p>Daily activity charts over a 90-day period</p></li>
|
||||
<li><p>Combined activity visualization</p></li>
|
||||
</ul>
|
||||
<p>To view statistics, a user must have the <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) permission for the repository. See the Access Configuration section for details on setting permissions.</p>
|
||||
<p><strong>Repository Thanks</strong></p>
|
||||
<p>The page node includes a “Thanks” feature that allows users to express appreciation for a repository. On each repository page, a “Thanks” link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.</p>
|
||||
<p><strong>Configuration Example</strong></p>
|
||||
<p>A complete page node configuration might look like this:</p>
|
||||
</section>
|
||||
<section id="configuration-example">
|
||||
<h3>Configuration Example<a class="headerlink" href="#configuration-example" title="Link to this heading">¶</a></h3>
|
||||
<p>A complete node configuration might look like this:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[rngit]
|
||||
node_name = My Git Node
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
node_name = My Git Node
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
|
||||
[repositories]
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
|
||||
[access]
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section id="release-management">
|
||||
<h2>Release Management<a class="headerlink" href="#release-management" title="Link to this heading">¶</a></h2>
|
||||
<p>In addition to hosting Git repositories, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the <code class="docutils literal notranslate"><span class="pre">rngit</span> <span class="pre">release</span></code> subcommand, and are also viewable through the Nomad Network page interface.</p>
|
||||
<p><strong>The Release Workflow</strong></p>
|
||||
<section id="the-release-workflow">
|
||||
<h3>The Release Workflow<a class="headerlink" href="#the-release-workflow" title="Link to this heading">¶</a></h3>
|
||||
<p>Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> client will open your configured <code class="docutils literal notranslate"><span class="pre">$EDITOR</span></code> to compose release notes, then upload all artifacts to the remote repository node.</p>
|
||||
<p>To create a release, specify the tag name and path to artifacts:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo create v1.2.0:./dist
|
||||
@@ -487,7 +831,9 @@ unicode_icons = no
|
||||
<li><p>Publish the release</p></li>
|
||||
</ol>
|
||||
<p>If no <code class="docutils literal notranslate"><span class="pre">$EDITOR</span></code> environment variable is set, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> will try to use <code class="docutils literal notranslate"><span class="pre">nano</span></code>, <code class="docutils literal notranslate"><span class="pre">vim</span></code> or <code class="docutils literal notranslate"><span class="pre">vi</span></code>. The editor will show a template with instructions. Lines starting with <code class="docutils literal notranslate"><span class="pre">#</span></code> will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.</p>
|
||||
<p><strong>Release Storage & Structure</strong></p>
|
||||
</section>
|
||||
<section id="release-storage-structure">
|
||||
<h3>Release Storage & Structure<a class="headerlink" href="#release-storage-structure" title="Link to this heading">¶</a></h3>
|
||||
<p>Releases are stored on the node in a directory named <code class="docutils literal notranslate"><span class="pre">repo_name.releases</span></code> next to the bare repository. Each release is a subdirectory containing:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">META</span></code> - Release metadata in ConfigObj format</p></li>
|
||||
@@ -495,6 +841,9 @@ unicode_icons = no
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">artifacts/</span></code> - All uploaded files</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">THANKS</span></code> - Appreciation count from users</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="command-line-interaction">
|
||||
<h3>Command-Line Interaction<a class="headerlink" href="#command-line-interaction" title="Link to this heading">¶</a></h3>
|
||||
<p><strong>Listing Releases</strong></p>
|
||||
<p>To view all releases for a repository:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
|
||||
@@ -552,7 +901,6 @@ rel:none # Deny everyone
|
||||
</div>
|
||||
<p><strong>Nomad Network Interface</strong></p>
|
||||
<p>When the Nomad Network page node is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.</p>
|
||||
<p>Only releases with <code class="docutils literal notranslate"><span class="pre">published</span></code> status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.</p>
|
||||
<p><strong>All Command-Line Options (rngit release)</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit release [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
@@ -577,9 +925,12 @@ options:
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section id="work-documents">
|
||||
<h2>Work Documents<a class="headerlink" href="#work-documents" title="Link to this heading">¶</a></h2>
|
||||
<p>In addition to releases, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> provides a work document management system for tracking tasks, investigations, issues and progress related to repositories. Work documents are stored as structured msgpack data and support threaded updates and comments.</p>
|
||||
<section id="working-with-work-documents">
|
||||
<h3>Working With Work Documents<a class="headerlink" href="#working-with-work-documents" title="Link to this heading">¶</a></h3>
|
||||
<p><strong>Listing Work Documents</strong></p>
|
||||
<p>To view work documents for a repository:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
|
||||
@@ -593,7 +944,7 @@ ID Title Author Created Comments
|
||||
2 Fixed bug in parser 8f3a21c9d84e927b… 2025-01-14 09:15 1
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Use <code class="docutils literal notranslate"><span class="pre">--scope</span> <span class="pre">completed</span></code> to view completed work documents, or <code class="docutils literal notranslate"><span class="pre">--scope</span> <span class="pre">all</span></code> to see both active and completed.</p>
|
||||
<p>Use <code class="docutils literal notranslate"><span class="pre">--scope</span> <span class="pre">completed</span></code> to view completed work documents, <code class="docutils literal notranslate"><span class="pre">--scope</span> <span class="pre">proposed</span></code> to view proposed documents, or <code class="docutils literal notranslate"><span class="pre">--scope</span> <span class="pre">all</span></code> to see all scopes.</p>
|
||||
<p><strong>Viewing a Work Document</strong></p>
|
||||
<p>To view a specific work document with all its comments:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo view -d 1
|
||||
@@ -635,6 +986,28 @@ Initial analysis complete
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This opens your editor to compose the update.</p>
|
||||
</section>
|
||||
<section id="proposing-work-documents">
|
||||
<h3>Proposing Work Documents<a class="headerlink" href="#proposing-work-documents" title="Link to this heading">¶</a></h3>
|
||||
<p>Users with <code class="docutils literal notranslate"><span class="pre">propose</span></code> permission can create work document proposals without full <code class="docutils literal notranslate"><span class="pre">write</span></code> access. Proposals are created in a “proposed” state and must be activated by a user with appropriate permissions before becoming active.</p>
|
||||
<p>To propose a work document:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo propose --title "Feature proposal"
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This opens your editor to compose the proposal content. When saved, the document is created in the “proposed” scope. The creator automatically receives <code class="docutils literal notranslate"><span class="pre">interact</span></code> and <code class="docutils literal notranslate"><span class="pre">write</span></code> permissions on the proposed document.</p>
|
||||
<p>Proposed documents are visible through <code class="docutils literal notranslate"><span class="pre">--scope</span> <span class="pre">proposed</span></code> or <code class="docutils literal notranslate"><span class="pre">--scope</span> <span class="pre">all</span></code>:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list --scope proposed
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Permissions for Proposals</strong></p>
|
||||
<ul class="simple">
|
||||
<li><p>Creating proposals requires <code class="docutils literal notranslate"><span class="pre">propose</span></code> permission on the repository</p></li>
|
||||
<li><p>The creator automatically gets <code class="docutils literal notranslate"><span class="pre">interact</span></code> and <code class="docutils literal notranslate"><span class="pre">write</span></code> on their proposed document</p></li>
|
||||
<li><p>Activating a proposal requires <code class="docutils literal notranslate"><span class="pre">write</span></code> and <code class="docutils literal notranslate"><span class="pre">interact</span></code> permissions</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="state-management">
|
||||
<h3>State Management<a class="headerlink" href="#state-management" title="Link to this heading">¶</a></h3>
|
||||
<p><strong>Completing Work Documents</strong></p>
|
||||
<p>To mark a work document as completed (moving it from <code class="docutils literal notranslate"><span class="pre">active</span></code> to <code class="docutils literal notranslate"><span class="pre">completed</span></code>):</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo complete -d 1
|
||||
@@ -657,21 +1030,49 @@ Are you sure you want to delete active work document #1? [y/N]: y
|
||||
Work document #1 deleted
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Permissions</strong></p>
|
||||
<p>Users can view work documents and updates if the have <code class="docutils literal notranslate"><span class="pre">read</span></code> permission for the repository. If users have <code class="docutils literal notranslate"><span class="pre">read</span></code> and <code class="docutils literal notranslate"><span class="pre">interact</span></code>, they can also post updates/comments on existing work documents. Work document management requires having <code class="docutils literal notranslate"><span class="pre">write</span></code> and <code class="docutils literal notranslate"><span class="pre">interact</span></code> permission to the repository. These permissions are configured the same way as any other repository permissions. In the config file or <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files, use <code class="docutils literal notranslate"><span class="pre">i:target</span></code> to grant work document interaction rights:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span># In .allowed file or config
|
||||
i:all # Allow everyone
|
||||
i:9710b86... # Allow specific identity
|
||||
i:none # Deny everyone
|
||||
</section>
|
||||
<section id="managing-work-document-permissions">
|
||||
<h3>Managing Work Document Permissions<a class="headerlink" href="#managing-work-document-permissions" title="Link to this heading">¶</a></h3>
|
||||
<p>Users with administrative access to a work document can manage its specific permissions. This allows fine-grained control over who can read, write, comment on, or administer individual work documents.</p>
|
||||
<p>To view or edit permissions for a work document:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo perms -d 1
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Author Verification</strong></p>
|
||||
<p>Users can only edit or delete work documents and updates they created. The author is cryptographically verified from the interacting link’s <code class="docutils literal notranslate"><span class="pre">remote_identity</span></code>.</p>
|
||||
<p>This opens your editor with the current permission configuration:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>r:all
|
||||
i:9710b86ba12c42d1d8f30f74fe509286
|
||||
adm:9710b86ba12c42d1d8f30f74fe509286
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Permission rules follow the same format as repository permissions:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">r:target</span></code> - Grant read access</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">w:target</span></code> - Grant write access</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">i:target</span></code> - Grant interact (comment) access</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">adm:target</span></code> - Grant admin access</p></li>
|
||||
</ul>
|
||||
<p>Targets can be <code class="docutils literal notranslate"><span class="pre">all</span></code>, <code class="docutils literal notranslate"><span class="pre">none</span></code>, or a specific identity hash.</p>
|
||||
<p><strong>Who Can Edit Permissions</strong></p>
|
||||
<p>Document permissions can be edited by:</p>
|
||||
<ul class="simple">
|
||||
<li><p>The original author (if they also have <code class="docutils literal notranslate"><span class="pre">interact</span></code> and <code class="docutils literal notranslate"><span class="pre">write</span></code> on the repository)</p></li>
|
||||
<li><p>Any user with <code class="docutils literal notranslate"><span class="pre">admin</span></code> permission on the document</p></li>
|
||||
<li><p>Repository admins (through inherited permissions)</p></li>
|
||||
</ul>
|
||||
<p><strong>Permission Precedence</strong></p>
|
||||
<p>Document-specific permissions override repository-level permissions for that document. If document permissions exist, they are checked first; if access is not granted there, repository permissions are checked.</p>
|
||||
<p><strong>Author Rights:</strong></p>
|
||||
<ul class="simple">
|
||||
<li><p>Users can only edit or delete work documents they created</p></li>
|
||||
<li><p>The author is cryptographically verified from the interacting link’s <code class="docutils literal notranslate"><span class="pre">remote_identity</span></code></p></li>
|
||||
<li><p>Document creators automatically receive <code class="docutils literal notranslate"><span class="pre">interact</span></code> and <code class="docutils literal notranslate"><span class="pre">write</span></code> on their documents</p></li>
|
||||
</ul>
|
||||
<p><strong>Storage Format</strong></p>
|
||||
<p>Work documents are stored in a <code class="docutils literal notranslate"><span class="pre">repo_name.work</span></code> directory next to the repository, containing:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">active/</span></code> - Active work documents</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">completed/</span></code> - Completed work documents</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">proposed/</span></code> - Proposed work documents</p></li>
|
||||
</ul>
|
||||
<p>Each document is a numbered directory containing:</p>
|
||||
<ul class="simple">
|
||||
@@ -690,7 +1091,8 @@ Reticulum Git Work Document Manager
|
||||
|
||||
positional arguments:
|
||||
repository URL of remote repository
|
||||
operation list, view, create, edit, delete, update or complete
|
||||
operation list, view, create, propose, edit, delete,
|
||||
update, complete, activate or perms
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -698,8 +1100,8 @@ options:
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
--scope SCOPE document scope: active, completed or all
|
||||
-t, --title TITLE document title for create
|
||||
--scope SCOPE document scope: active, completed, proposed or all
|
||||
-t, --title TITLE document title for create/propose
|
||||
-d, --id ID document ID
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
@@ -707,6 +1109,7 @@ options:
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
@@ -765,12 +1168,53 @@ options:
|
||||
<ul>
|
||||
<li><a class="reference internal" href="#">Git Over Reticulum</a><ul>
|
||||
<li><a class="reference internal" href="#the-rngit-utility">The rngit Utility</a></li>
|
||||
<li><a class="reference internal" href="#repository-structure">Repository Structure</a></li>
|
||||
<li><a class="reference internal" href="#serving-pages-over-nomad-network">Serving Pages Over Nomad Network</a></li>
|
||||
<li><a class="reference internal" href="#repository-creation-management">Repository Creation & Management</a><ul>
|
||||
<li><a class="reference internal" href="#creating-empty-repositories">Creating Empty Repositories</a></li>
|
||||
<li><a class="reference internal" href="#forking-repositories">Forking Repositories</a></li>
|
||||
<li><a class="reference internal" href="#mirroring-repositories">Mirroring Repositories</a></li>
|
||||
<li><a class="reference internal" href="#automatic-mirror-synchronization">Automatic Mirror Synchronization</a></li>
|
||||
<li><a class="reference internal" href="#manual-synchronization">Manual Synchronization</a></li>
|
||||
<li><a class="reference internal" href="#git-configuration-parameters">Git Configuration Parameters</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#repository-structure">Repository Structure</a><ul>
|
||||
<li><a class="reference internal" href="#configuration">Configuration</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#permissions">Permissions</a><ul>
|
||||
<li><a class="reference internal" href="#permission-types">Permission Types</a></li>
|
||||
<li><a class="reference internal" href="#permission-hierarchy">Permission Hierarchy</a></li>
|
||||
<li><a class="reference internal" href="#configuration-methods">Configuration Methods</a></li>
|
||||
<li><a class="reference internal" href="#work-document-permissions">Work Document Permissions</a></li>
|
||||
<li><a class="reference internal" href="#creator-permissions">Creator Permissions</a></li>
|
||||
<li><a class="reference internal" href="#permission-examples">Permission Examples</a></li>
|
||||
<li><a class="reference internal" href="#permission-short-forms">Permission Short Forms</a></li>
|
||||
<li><a class="reference internal" href="#permission-configuration-locations">Permission Configuration Locations</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#identity-destination-aliases">Identity & Destination Aliases</a></li>
|
||||
<li><a class="reference internal" href="#serving-pages-over-nomad-network">Serving Pages Over Nomad Network</a><ul>
|
||||
<li><a class="reference internal" href="#enabling-the-git-page-node">Enabling the Git Page Node</a></li>
|
||||
<li><a class="reference internal" href="#accessing-repository-pages">Accessing Repository Pages</a></li>
|
||||
<li><a class="reference internal" href="#formatting-syntax-highlighting">Formatting & Syntax Highlighting</a></li>
|
||||
<li><a class="reference internal" href="#customizing-templates">Customizing Templates</a></li>
|
||||
<li><a class="reference internal" href="#release-management">Release Management</a></li>
|
||||
<li><a class="reference internal" href="#work-documents">Work Documents</a></li>
|
||||
<li><a class="reference internal" href="#repository-statistics">Repository Statistics</a></li>
|
||||
<li><a class="reference internal" href="#configuration-example">Configuration Example</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#release-management">Release Management</a><ul>
|
||||
<li><a class="reference internal" href="#the-release-workflow">The Release Workflow</a></li>
|
||||
<li><a class="reference internal" href="#release-storage-structure">Release Storage & Structure</a></li>
|
||||
<li><a class="reference internal" href="#command-line-interaction">Command-Line Interaction</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#work-documents">Work Documents</a><ul>
|
||||
<li><a class="reference internal" href="#working-with-work-documents">Working With Work Documents</a></li>
|
||||
<li><a class="reference internal" href="#proposing-work-documents">Proposing Work Documents</a></li>
|
||||
<li><a class="reference internal" href="#state-management">State Management</a></li>
|
||||
<li><a class="reference internal" href="#managing-work-document-permissions">Managing Work Document Permissions</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -782,7 +1226,7 @@ options:
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Communications Hardware - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Communications Hardware - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -675,7 +675,7 @@ can be used with Reticulum. This includes virtual software modems such as
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
+51
-10
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -525,12 +525,53 @@ to participate in the development of Reticulum itself.</p>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Git Over Reticulum</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#the-rngit-utility">The rngit Utility</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#repository-structure">Repository Structure</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#serving-pages-over-nomad-network">Serving Pages Over Nomad Network</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#formatting-syntax-highlighting">Formatting & Syntax Highlighting</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#customizing-templates">Customizing Templates</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#release-management">Release Management</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#work-documents">Work Documents</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#repository-creation-management">Repository Creation & Management</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#creating-empty-repositories">Creating Empty Repositories</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#forking-repositories">Forking Repositories</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#mirroring-repositories">Mirroring Repositories</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#automatic-mirror-synchronization">Automatic Mirror Synchronization</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#manual-synchronization">Manual Synchronization</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#git-configuration-parameters">Git Configuration Parameters</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#repository-structure">Repository Structure</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#configuration">Configuration</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#permissions">Permissions</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-types">Permission Types</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-hierarchy">Permission Hierarchy</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#configuration-methods">Configuration Methods</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#work-document-permissions">Work Document Permissions</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#creator-permissions">Creator Permissions</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-examples">Permission Examples</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-short-forms">Permission Short Forms</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-configuration-locations">Permission Configuration Locations</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#identity-destination-aliases">Identity & Destination Aliases</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#serving-pages-over-nomad-network">Serving Pages Over Nomad Network</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#enabling-the-git-page-node">Enabling the Git Page Node</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#accessing-repository-pages">Accessing Repository Pages</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#formatting-syntax-highlighting">Formatting & Syntax Highlighting</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#customizing-templates">Customizing Templates</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#repository-statistics">Repository Statistics</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#configuration-example">Configuration Example</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#release-management">Release Management</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#the-release-workflow">The Release Workflow</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#release-storage-structure">Release Storage & Structure</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#command-line-interaction">Command-Line Interaction</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#work-documents">Work Documents</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#working-with-work-documents">Working With Work Documents</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#proposing-work-documents">Proposing Work Documents</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#state-management">State Management</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#managing-work-document-permissions">Managing Work Document Permissions</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a><ul>
|
||||
@@ -643,7 +684,7 @@ to participate in the development of Reticulum itself.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Configuring Interfaces - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Configuring Interfaces - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -1773,7 +1773,7 @@ interface basis under the relevant interface configuration section.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Reticulum License - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Reticulum License - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -344,7 +344,7 @@ SOFTWARE.
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Building Networks - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Building Networks - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -663,7 +663,7 @@ differently than a mobile device roaming between radio cells.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>API Reference - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>API Reference - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -2484,7 +2484,7 @@ will announce it.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>Search - Reticulum Network Stack 1.2.6 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<title>Search - Reticulum Network Stack 1.2.8 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="#" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -303,7 +303,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Programs Using Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Programs Using Reticulum - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -512,7 +512,7 @@ plugin system for expandability.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Support Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Support Reticulum - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -382,7 +382,7 @@ circumstances, so we rely on old-fashioned human feedback.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Understanding Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Understanding Reticulum - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -1337,7 +1337,7 @@ those risks are acceptable to you.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Using Reticulum on Your System - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Using Reticulum on Your System - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -1635,7 +1635,7 @@ systemctl --user enable rnsd.service
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>What is Reticulum? - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>What is Reticulum? - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -504,7 +504,7 @@ network, and vice versa.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Zen of Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<title>Zen of Reticulum - Reticulum Network Stack 1.2.8 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.8 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.8 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -676,7 +676,7 @@ Imagine a messaging app. You write it once. It works on a laptop connected to fi
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></script>
|
||||
</div><script src="_static/documentation_options.js?v=4d6f9085"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
+480
-41
@@ -1,13 +1,13 @@
|
||||
# Git Over Reticulum
|
||||
|
||||
A set of utilities for distributed collaborative software development and publishing is included in RNS.
|
||||
A set of utilities for distributed collaborative software development and publishing are included in RNS.
|
||||
|
||||
The system consists of two parts: The `rngit` node that hosts repositories, and the `git-remote-rns` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: `rns://DESTINATION_HASH/group/repo`.
|
||||
|
||||
If you set a branch to track a Reticulum remote as the default upstream, you can simply use `git` as you normally would; all commands work transparently and as expected.
|
||||
|
||||
#### WARNING
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, `rngit` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible [permission system](#permissions) for allowing many users to interact with many different repositories on a single node, `rngit` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
|
||||
## The rngit Utility
|
||||
|
||||
@@ -26,7 +26,7 @@ $ rngit
|
||||
|
||||
On the first run, `rngit` will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.
|
||||
|
||||
View your identity and destination hashes:
|
||||
Them, view your identity and destination hashes:
|
||||
|
||||
```text
|
||||
$ rngit --print-identity
|
||||
@@ -68,6 +68,12 @@ Get changes from a remote repository:
|
||||
$ git pull rns_remote master
|
||||
```
|
||||
|
||||
Fork an existing repository from a remote to your `rngit` node:
|
||||
|
||||
```text
|
||||
$ rngit fork rns://8a37cdd16938ce79861561adbd59023a/reticulum/lxmf rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
```
|
||||
|
||||
**All Command-Line Options (rngit)**
|
||||
|
||||
```text
|
||||
@@ -98,11 +104,211 @@ The `git-remote-rns` helper is automatically invoked by Git when interacting wit
|
||||
|
||||
The client configuration file is located at `~/.rngit/client_config` and allows adjusting parameters such as the reference batch size for transfers.
|
||||
|
||||
## Repository Creation & Management
|
||||
|
||||
The `rngit` utility provides several ways to create and manage repositories on a node: creating empty repositories, forking from existing repositories, and mirroring remote repositories.
|
||||
|
||||
### Creating Empty Repositories
|
||||
|
||||
To create a new empty repository on a remote node:
|
||||
|
||||
```text
|
||||
$ rngit create rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Repository public/myrepo created
|
||||
```
|
||||
|
||||
This creates a bare Git repository at the specified path. You must have `create` permission for the target group. When a repository is created, the creator automatically receives `adm` (admin) permissions on the repository through an auto-generated `.allowed` file.
|
||||
|
||||
**All Command-Line Options (rngit create)**
|
||||
|
||||
```text
|
||||
usage: rngit create [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
repository
|
||||
|
||||
Reticulum Git Repository Creation
|
||||
|
||||
positional arguments:
|
||||
repository URL of repository to create
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
```
|
||||
|
||||
### Forking Repositories
|
||||
|
||||
Forking creates a copy of an existing repository (from any accessible Git URL) on your `rngit` node. Forks maintain a reference to their upstream source for later synchronization.
|
||||
|
||||
To fork a repository:
|
||||
|
||||
```text
|
||||
$ rngit fork https://github.com/user/original rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
Repository forked to public/myfork
|
||||
```
|
||||
|
||||
The source can be any valid Git URL, including:
|
||||
|
||||
- HTTPS URLs: `https://github.com/user/repo.git`
|
||||
- Git URLs: `git://host.com/repo.git`
|
||||
- SSH URLs: `ssh://git@host.com/repo.git`
|
||||
- Reticulum URLs: `rns://DESTINATION_HASH/group/repo`
|
||||
- Local paths: `/path/to/repo.git`
|
||||
|
||||
Forks are created as bare repositories with metadata tracking their origin. The fork process:
|
||||
|
||||
1. Creates a new bare repository
|
||||
2. Fetches all refs (`+refs/*:refs/*`) from the source
|
||||
3. Sets `repository.rngit.type` to `fork`
|
||||
4. Sets `repository.rngit.upstream.source` to the source URL
|
||||
5. Grants creator admin permissions
|
||||
|
||||
**All Command-Line Options (rngit fork)**
|
||||
|
||||
```text
|
||||
usage: rngit fork [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
source target
|
||||
|
||||
Reticulum Git Repository Forker
|
||||
|
||||
positional arguments:
|
||||
source URL of source repository
|
||||
target URL of target repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
```
|
||||
|
||||
### Mirroring Repositories
|
||||
|
||||
Mirrors are similar to forks but are designed for keeping a local copy synchronized with an upstream repository. Mirrors can be automatically updated on a configurable schedule.
|
||||
|
||||
To create a mirror:
|
||||
|
||||
```text
|
||||
$ rngit mirror https://github.com/user/upstream rns://50824b711717f97c2fb1166ceddd5ea9/public/mymirror
|
||||
|
||||
Repository mirrored to public/mymirror
|
||||
```
|
||||
|
||||
Mirrors are created with the same process as forks, but with `repository.rngit.type` set to `mirror` and an additional `repository.rngit.upstream.sync` timestamp tracking the last successful synchronization.
|
||||
|
||||
**All Command-Line Options (rngit mirror)**
|
||||
|
||||
```text
|
||||
usage: rngit mirror [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
source target
|
||||
|
||||
Reticulum Git Mirror Management
|
||||
|
||||
positional arguments:
|
||||
source URL of source repository
|
||||
target URL of target repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
```
|
||||
|
||||
### Automatic Mirror Synchronization
|
||||
|
||||
The `rngit` node can automatically keep mirrors synchronized with their upstream sources. This is configured in the main configuration file:
|
||||
|
||||
```text
|
||||
[rngit]
|
||||
mirror_interval = 24
|
||||
```
|
||||
|
||||
The `mirror_interval` specifies the synchronization interval in hours (default: 24). The node checks for mirrors needing sync every 15 minutes, and fetches updates from upstream if the configured interval has elapsed since the last sync.
|
||||
|
||||
For automatic sync to happen, the repository must have been created with `rngit mirror`. Sync failures are logged but do not prevent future retry attempts. The sync timestamp is only updated on successful completion.
|
||||
|
||||
### Manual Synchronization
|
||||
|
||||
Both forks and mirrors can be manually synchronized on demand using the `sync` command:
|
||||
|
||||
```text
|
||||
$ rngit sync rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
Repository synced
|
||||
```
|
||||
|
||||
This fetches all refs from the upstream source configured when the repository was created. You must have `read` and `write` permissions for the repository to perform a manual sync.
|
||||
|
||||
For mirrors, manual sync also updates the sync timestamp, effectively resetting the automatic sync timer.
|
||||
|
||||
**All Command-Line Options (rngit sync)**
|
||||
|
||||
```text
|
||||
usage: rngit sync [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
repository
|
||||
|
||||
Reticulum Git Repository Syncer
|
||||
|
||||
positional arguments:
|
||||
repository URL of repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
```
|
||||
|
||||
### Git Configuration Parameters
|
||||
|
||||
Repositories created through `rngit` store metadata in Git configuration:
|
||||
|
||||
- `repository.rngit.type` - Either `fork` or `mirror`
|
||||
- `repository.rngit.upstream.source` - The source URL used during creation
|
||||
- `repository.rngit.upstream.sync` - Unix timestamp of last successful sync for mirrors
|
||||
|
||||
These parameters are used by the sync system and can be queried using standard Git commands:
|
||||
|
||||
```text
|
||||
$ git config --get repository.rngit.type
|
||||
mirror
|
||||
|
||||
$ git config --get repository.rngit.upstream.source
|
||||
https://github.com/user/upstream
|
||||
|
||||
$ git config --get repository.rngit.upstream.sync
|
||||
1716230400
|
||||
```
|
||||
|
||||
## Repository Structure
|
||||
|
||||
The `rngit` node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is `group_name/repo_name`. For example, a repository at `/var/git/public/myrepo` would be accessible as `public/myrepo` via the URL `rns://DESTINATION_HASH/public/myrepo`.
|
||||
|
||||
**Configuration**
|
||||
### Configuration
|
||||
|
||||
The `rngit` node configuration file is located at `~/.rngit/config` (or `/etc/rngit/config` for system-wide installations). The default configuration includes:
|
||||
|
||||
@@ -111,19 +317,190 @@ The `rngit` node configuration file is located at `~/.rngit/config` (or `/etc/rn
|
||||
- Announce intervals for network visibility
|
||||
- Optional statistics recording for repository activity
|
||||
|
||||
Access permissions can be configured at the group level in the config file, or per-repository using `.allowed` files. Permissions use the format `permission:target` where permission is `r` (read), `w` (write), `rw` (read/write), `c` (create) or `s` (stats) and target is `all`, `none`, or a specific identity hash.
|
||||
## Permissions
|
||||
|
||||
The `s` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set `record_stats = yes` in the `[rngit]` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to `stats_ignore_identities`.
|
||||
The `rngit` permission system provides fine-grained access control at multiple levels: group-level, repository-level, and document-level. Permissions can be statically configured in files or dynamically generated via executable scripts.
|
||||
|
||||
Repository-specific `.allowed` files can be static text files or executable scripts that output permission rules to stdout. A `group.allowed` file in a repository group directory applies to all repositories within that group.
|
||||
Access permissions can be configured at the group level in the config file or per-group `.allowed` files, or per-repository `.allowed` files. The `s` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set `record_stats = yes` in the `[rngit]` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to `stats_ignore_identities`.
|
||||
|
||||
By default, **no** permissions are granted for anything! You will have to enable the permissions you require to be able to actually *do* something with `rngit`.
|
||||
|
||||
Permissions can be modified by editing the `rngit` config file, individual `.allowed` files on disk, or remotely using the `rngit perms` command.
|
||||
|
||||
### Permission Types
|
||||
|
||||
The following permissions are supported:
|
||||
|
||||
- `r` (read) - Clone, fetch, and view repositories and work documents
|
||||
- `w` (write) - Push changes and manage work documents
|
||||
- `rw` (read/write) - Combined read and write access
|
||||
- `c` (create) - Create, fork or mirror new repositories within a group
|
||||
- `s` (stats) - View repository activity statistics
|
||||
- `rel` (release) - Create and manage releases
|
||||
- `i` (interact) - Comment on and interact with work documents
|
||||
- `p` (propose) - Propose new work documents (without full write access)
|
||||
- `adm` (admin) - Full access
|
||||
|
||||
Permission targets can be:
|
||||
|
||||
- `all` or `a` - Everyone
|
||||
- `none` or `n` - Nobody
|
||||
- A specific Reticulum identity hash
|
||||
|
||||
### Permission Hierarchy
|
||||
|
||||
Permissions are resolved in the following hierarchy:
|
||||
|
||||
1. **Repository-level permissions** - Checked first, if none exists group permissions are checked
|
||||
2. **Group-level permissions** - Used as fallback if no repository-level permissions are set
|
||||
3. **Admin override** - Finally, potential admin rights are checked
|
||||
|
||||
For work documents, work document specific permissions are always checked first, and work documents have additional specific checks such as modifications only being possible by the document author.
|
||||
|
||||
### Configuration Methods
|
||||
|
||||
**Group-Level Configuration**
|
||||
|
||||
Group permissions can be configured in the `[access]` section of the main config file:
|
||||
|
||||
```text
|
||||
[access]
|
||||
public = r:all, w:9710b86ba12c42d1d8f30f74fe509286
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
collaborative = r:all, i:all, p:all, w:9710b86ba12c42d1d8f30f74fe509286
|
||||
```
|
||||
|
||||
Additionally, they can be configured in a group `group_name.allowed` file, placed next to the `group_name` group directory.
|
||||
|
||||
**Repository-Level Configuration**
|
||||
|
||||
Repository-specific permissions are set in `.allowed` files placed next to the repository directory (for example, `myrepo.allowed` for `myrepo`):
|
||||
|
||||
```text
|
||||
# myrepo.allowed
|
||||
r:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
rel:9710b86ba12c42d1d8f30f74fe509286
|
||||
```
|
||||
|
||||
**Dynamic Permissions**
|
||||
|
||||
Permission files can be made executable to generate permissions dynamically:
|
||||
|
||||
```text
|
||||
$ chmod +x myrepo.allowed
|
||||
```
|
||||
|
||||
When executable, the script is run and its stdout is parsed as permission rules. This allows integration with external authentication systems.
|
||||
|
||||
### Work Document Permissions
|
||||
|
||||
Work documents support additional permission granularity through `.allowed` files in the work directory (e.g., `42.allowed` for document #42). These files use the same permission syntax but only support:
|
||||
|
||||
- `r` (read) - View the document
|
||||
- `w` (write) - Edit the document
|
||||
- `i` (interact) - Comment on the document
|
||||
- `p` (propose) - Propose changes (future use)
|
||||
- `adm` (admin) - Full control over the document
|
||||
|
||||
Document permissions override repository permissions for that specific document. Work document permissions can be updated simply by editing the `.allowed` file, or remotely by using the `rngit work` command.
|
||||
|
||||
### Creator Permissions
|
||||
|
||||
When a user creates a repository (via `create`, `fork`, or `mirror`), they are automatically granted `adm` (admin) permissions on that repository.
|
||||
|
||||
When a user creates a work document, they automatically receive `interact` and `write` permissions on that document.
|
||||
|
||||
### Permission Examples
|
||||
|
||||
**Example 1: Public Read, Restricted Write**
|
||||
|
||||
```text
|
||||
r:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
```
|
||||
|
||||
Everyone can read, only the specified identity can write.
|
||||
|
||||
**Example 2: Collaborative Development**
|
||||
|
||||
```text
|
||||
r:all
|
||||
i:all
|
||||
p:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
rel:9710b86ba12c42d1d8f30f74fe509286
|
||||
```
|
||||
|
||||
Everyone can read, interact (comment), and propose work documents. Only the specified identity can write, create releases, and manage work documents fully.
|
||||
|
||||
**Example 3: Private Repository**
|
||||
|
||||
```text
|
||||
rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
rw:a1b2c3d4e5f686ba12c42d1ba12ef1aa
|
||||
```
|
||||
|
||||
Only the two specified identities have any access (read or write).
|
||||
|
||||
**Example 4: Mirror with Stats**
|
||||
|
||||
```text
|
||||
r:all
|
||||
s:all
|
||||
w:none
|
||||
```
|
||||
|
||||
Everyone can read and view stats, but nobody can push (mirror is read-only from upstream).
|
||||
|
||||
### Permission Short Forms
|
||||
|
||||
Permissions can be specified using short or long forms:
|
||||
|
||||
- `r` = `read`
|
||||
- `w` = `write`
|
||||
- `rw` = `readwrite`
|
||||
- `c` = `create`
|
||||
- `s` = `stats`
|
||||
- `rel` = `release`
|
||||
- `i` = `interact`
|
||||
- `p` = `propose`
|
||||
- `adm` = `admin`
|
||||
|
||||
Targets can also use short forms:
|
||||
|
||||
- `a` = `all` = `everyone`
|
||||
- `n` = `none` = `nobody`
|
||||
|
||||
### Permission Configuration Locations
|
||||
|
||||
- User install: `~/.rngit/config`
|
||||
- System install: `/etc/rngit/config`
|
||||
- Group permissions: `<group_root>/<group_name>.allowed`
|
||||
- Repository permissions: `<group_root>/<group_name>/<repo_name>.allowed`
|
||||
- Document permissions: `<group_root>/<group_name>.work/<doc_id>.allowed`
|
||||
|
||||
## Identity & Destination Aliases
|
||||
|
||||
To make permission and remote destination management easier, you can locally define aliases for commonly used identity and destination hashes. Identity aliases used in permissions resolution can be defined in the `[aliases]` section of the `~/.rngit/config` file, while destination aliases are defined in the `[aliases]` section of the `~/.rngit/client_config` file.
|
||||
|
||||
All alias definitions take the form of `aliased_name = HASH`:
|
||||
|
||||
```text
|
||||
[aliases]
|
||||
alice = d09285e660cfe27cee6d9a0beb58b7e0
|
||||
bob = ffcffb4e255e156e77f79b82c13086a6
|
||||
```
|
||||
|
||||
**Aliases are always resolved locally!** If for example you fork a repository with `rngit fork rns://bobs_node/public/repo_name rns://my_node/forks/repo_name`, the forked repository will of course still reference the full, original destination hash, and use this for subsequent upstream syncs.
|
||||
|
||||
## Serving Pages Over Nomad Network
|
||||
|
||||
In addition to providing Git repository access via the Git remote helper protocol, `rngit` can also run a [Nomad Network](https://github.com/markqvist/nomadnet) compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
|
||||
In addition to providing Git repository access via the Git remote helper protocol and command-line tools, `rngit` can also run a [Nomad Network](https://github.com/markqvist/nomadnet) compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
|
||||
|
||||
When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.
|
||||
|
||||
**Enabling the Git Page Node**
|
||||
### Enabling the Git Page Node
|
||||
|
||||
To enable the page node, add the following to your `~/.rngit/config` file:
|
||||
|
||||
@@ -143,7 +520,7 @@ Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
|
||||
```
|
||||
|
||||
**Accessing Repository Pages**
|
||||
### Accessing Repository Pages
|
||||
|
||||
Once the page node is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:
|
||||
|
||||
@@ -159,7 +536,7 @@ Once the page node is running, you can access it from any Nomad Network client b
|
||||
|
||||
All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.
|
||||
|
||||
## Formatting & Syntax Highlighting
|
||||
### Formatting & Syntax Highlighting
|
||||
|
||||
If the `pygments` Python module is installed on your system, the page node will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.
|
||||
|
||||
@@ -182,7 +559,9 @@ def hello_world():
|
||||
```
|
||||
```
|
||||
|
||||
## Customizing Templates
|
||||
You can use `rawmu` code blocks to render raw Micron inside Markdown files. If you create a code block with the language hint `rawmu`, everything inside it will be treated as Micron directly.
|
||||
|
||||
### Customizing Templates
|
||||
|
||||
The page node uses a template system that allows complete customization of the generated pages. Templates are stored in the `~/.rngit/templates/` directory as Micron files.
|
||||
|
||||
@@ -223,11 +602,11 @@ serve_nomadnet = yes
|
||||
unicode_icons = yes
|
||||
```
|
||||
|
||||
**Repository Statistics**
|
||||
### Repository Statistics
|
||||
|
||||
When statistics recording is enabled (see the `record_stats` configuration option), the page node can display activity charts for each repository. The statistics page shows:
|
||||
|
||||
- Total and peak views, fetches and pushes
|
||||
- Total and peak views, downloads, fetches and pushes
|
||||
- Daily activity charts over a 90-day period
|
||||
- Combined activity visualization
|
||||
|
||||
@@ -237,34 +616,34 @@ To view statistics, a user must have the `s` (stats) permission for the reposito
|
||||
|
||||
The page node includes a “Thanks” feature that allows users to express appreciation for a repository. On each repository page, a “Thanks” link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.
|
||||
|
||||
**Configuration Example**
|
||||
### Configuration Example
|
||||
|
||||
A complete page node configuration might look like this:
|
||||
A complete node configuration might look like this:
|
||||
|
||||
```text
|
||||
[rngit]
|
||||
node_name = My Git Node
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
node_name = My Git Node
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
|
||||
[repositories]
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
|
||||
[access]
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
```
|
||||
|
||||
## Release Management
|
||||
|
||||
In addition to hosting Git repositories, `rngit` provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the `rngit release` subcommand, and are also viewable through the Nomad Network page interface.
|
||||
|
||||
**The Release Workflow**
|
||||
### The Release Workflow
|
||||
|
||||
Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The `rngit` client will open your configured `$EDITOR` to compose release notes, then upload all artifacts to the remote repository node.
|
||||
|
||||
@@ -283,7 +662,7 @@ This will:
|
||||
|
||||
If no `$EDITOR` environment variable is set, `rngit` will try to use `nano`, `vim` or `vi`. The editor will show a template with instructions. Lines starting with `#` will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.
|
||||
|
||||
**Release Storage & Structure**
|
||||
### Release Storage & Structure
|
||||
|
||||
Releases are stored on the node in a directory named `repo_name.releases` next to the bare repository. Each release is a subdirectory containing:
|
||||
|
||||
@@ -292,6 +671,8 @@ Releases are stored on the node in a directory named `repo_name.releases` next t
|
||||
- `artifacts/` - All uploaded files
|
||||
- `THANKS` - Appreciation count from users
|
||||
|
||||
### Command-Line Interaction
|
||||
|
||||
**Listing Releases**
|
||||
|
||||
To view all releases for a repository:
|
||||
@@ -363,8 +744,6 @@ rel:none # Deny everyone
|
||||
|
||||
When the Nomad Network page node is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.
|
||||
|
||||
Only releases with `published` status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.
|
||||
|
||||
**All Command-Line Options (rngit release)**
|
||||
|
||||
```text
|
||||
@@ -394,6 +773,8 @@ options:
|
||||
|
||||
In addition to releases, `rngit` provides a work document management system for tracking tasks, investigations, issues and progress related to repositories. Work documents are stored as structured msgpack data and support threaded updates and comments.
|
||||
|
||||
### Working With Work Documents
|
||||
|
||||
**Listing Work Documents**
|
||||
|
||||
To view work documents for a repository:
|
||||
@@ -410,7 +791,7 @@ ID Title Author Created Comments
|
||||
2 Fixed bug in parser 8f3a21c9d84e927b… 2025-01-14 09:15 1
|
||||
```
|
||||
|
||||
Use `--scope completed` to view completed work documents, or `--scope all` to see both active and completed.
|
||||
Use `--scope completed` to view completed work documents, `--scope proposed` to view proposed documents, or `--scope all` to see all scopes.
|
||||
|
||||
**Viewing a Work Document**
|
||||
|
||||
@@ -468,6 +849,32 @@ $ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo update -d 1
|
||||
|
||||
This opens your editor to compose the update.
|
||||
|
||||
### Proposing Work Documents
|
||||
|
||||
Users with `propose` permission can create work document proposals without full `write` access. Proposals are created in a “proposed” state and must be activated by a user with appropriate permissions before becoming active.
|
||||
|
||||
To propose a work document:
|
||||
|
||||
```text
|
||||
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo propose --title "Feature proposal"
|
||||
```
|
||||
|
||||
This opens your editor to compose the proposal content. When saved, the document is created in the “proposed” scope. The creator automatically receives `interact` and `write` permissions on the proposed document.
|
||||
|
||||
Proposed documents are visible through `--scope proposed` or `--scope all`:
|
||||
|
||||
```text
|
||||
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list --scope proposed
|
||||
```
|
||||
|
||||
**Permissions for Proposals**
|
||||
|
||||
- Creating proposals requires `propose` permission on the repository
|
||||
- The creator automatically gets `interact` and `write` on their proposed document
|
||||
- Activating a proposal requires `write` and `interact` permissions
|
||||
|
||||
### State Management
|
||||
|
||||
**Completing Work Documents**
|
||||
|
||||
To mark a work document as completed (moving it from `active` to `completed`):
|
||||
@@ -499,20 +906,50 @@ Are you sure you want to delete active work document #1? [y/N]: y
|
||||
Work document #1 deleted
|
||||
```
|
||||
|
||||
**Permissions**
|
||||
### Managing Work Document Permissions
|
||||
|
||||
Users can view work documents and updates if the have `read` permission for the repository. If users have `read` and `interact`, they can also post updates/comments on existing work documents. Work document management requires having `write` and `interact` permission to the repository. These permissions are configured the same way as any other repository permissions. In the config file or `.allowed` files, use `i:target` to grant work document interaction rights:
|
||||
Users with administrative access to a work document can manage its specific permissions. This allows fine-grained control over who can read, write, comment on, or administer individual work documents.
|
||||
|
||||
To view or edit permissions for a work document:
|
||||
|
||||
```text
|
||||
# In .allowed file or config
|
||||
i:all # Allow everyone
|
||||
i:9710b86... # Allow specific identity
|
||||
i:none # Deny everyone
|
||||
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo perms -d 1
|
||||
```
|
||||
|
||||
**Author Verification**
|
||||
This opens your editor with the current permission configuration:
|
||||
|
||||
Users can only edit or delete work documents and updates they created. The author is cryptographically verified from the interacting link’s `remote_identity`.
|
||||
```text
|
||||
r:all
|
||||
i:9710b86ba12c42d1d8f30f74fe509286
|
||||
adm:9710b86ba12c42d1d8f30f74fe509286
|
||||
```
|
||||
|
||||
Permission rules follow the same format as repository permissions:
|
||||
|
||||
- `r:target` - Grant read access
|
||||
- `w:target` - Grant write access
|
||||
- `i:target` - Grant interact (comment) access
|
||||
- `adm:target` - Grant admin access
|
||||
|
||||
Targets can be `all`, `none`, or a specific identity hash.
|
||||
|
||||
**Who Can Edit Permissions**
|
||||
|
||||
Document permissions can be edited by:
|
||||
|
||||
- The original author (if they also have `interact` and `write` on the repository)
|
||||
- Any user with `admin` permission on the document
|
||||
- Repository admins (through inherited permissions)
|
||||
|
||||
**Permission Precedence**
|
||||
|
||||
Document-specific permissions override repository-level permissions for that document. If document permissions exist, they are checked first; if access is not granted there, repository permissions are checked.
|
||||
|
||||
**Author Rights:**
|
||||
|
||||
- Users can only edit or delete work documents they created
|
||||
- The author is cryptographically verified from the interacting link’s `remote_identity`
|
||||
- Document creators automatically receive `interact` and `write` on their documents
|
||||
|
||||
**Storage Format**
|
||||
|
||||
@@ -520,6 +957,7 @@ Work documents are stored in a `repo_name.work` directory next to the repository
|
||||
|
||||
- `active/` - Active work documents
|
||||
- `completed/` - Completed work documents
|
||||
- `proposed/` - Proposed work documents
|
||||
|
||||
Each document is a numbered directory containing:
|
||||
|
||||
@@ -542,7 +980,8 @@ Reticulum Git Work Document Manager
|
||||
|
||||
positional arguments:
|
||||
repository URL of remote repository
|
||||
operation list, view, create, edit, delete, update or complete
|
||||
operation list, view, create, propose, edit, delete,
|
||||
update, complete, activate or perms
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -550,8 +989,8 @@ options:
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
--scope SCOPE document scope: active, completed or all
|
||||
-t, --title TITLE document title for create
|
||||
--scope SCOPE document scope: active, completed, proposed or all
|
||||
-t, --title TITLE document title for create/propose
|
||||
-d, --id ID document ID
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
|
||||
+31
-2
@@ -191,12 +191,41 @@ to participate in the development of Reticulum itself.
|
||||
* [Heterogeneous Connectivity](networks.md#heterogeneous-connectivity)
|
||||
* [Git Over Reticulum](git.md)
|
||||
* [The rngit Utility](git.md#the-rngit-utility)
|
||||
* [Repository Creation & Management](git.md#repository-creation-management)
|
||||
* [Creating Empty Repositories](git.md#creating-empty-repositories)
|
||||
* [Forking Repositories](git.md#forking-repositories)
|
||||
* [Mirroring Repositories](git.md#mirroring-repositories)
|
||||
* [Automatic Mirror Synchronization](git.md#automatic-mirror-synchronization)
|
||||
* [Manual Synchronization](git.md#manual-synchronization)
|
||||
* [Git Configuration Parameters](git.md#git-configuration-parameters)
|
||||
* [Repository Structure](git.md#repository-structure)
|
||||
* [Configuration](git.md#configuration)
|
||||
* [Permissions](git.md#permissions)
|
||||
* [Permission Types](git.md#permission-types)
|
||||
* [Permission Hierarchy](git.md#permission-hierarchy)
|
||||
* [Configuration Methods](git.md#configuration-methods)
|
||||
* [Work Document Permissions](git.md#work-document-permissions)
|
||||
* [Creator Permissions](git.md#creator-permissions)
|
||||
* [Permission Examples](git.md#permission-examples)
|
||||
* [Permission Short Forms](git.md#permission-short-forms)
|
||||
* [Permission Configuration Locations](git.md#permission-configuration-locations)
|
||||
* [Identity & Destination Aliases](git.md#identity-destination-aliases)
|
||||
* [Serving Pages Over Nomad Network](git.md#serving-pages-over-nomad-network)
|
||||
* [Formatting & Syntax Highlighting](git.md#formatting-syntax-highlighting)
|
||||
* [Customizing Templates](git.md#customizing-templates)
|
||||
* [Enabling the Git Page Node](git.md#enabling-the-git-page-node)
|
||||
* [Accessing Repository Pages](git.md#accessing-repository-pages)
|
||||
* [Formatting & Syntax Highlighting](git.md#formatting-syntax-highlighting)
|
||||
* [Customizing Templates](git.md#customizing-templates)
|
||||
* [Repository Statistics](git.md#repository-statistics)
|
||||
* [Configuration Example](git.md#configuration-example)
|
||||
* [Release Management](git.md#release-management)
|
||||
* [The Release Workflow](git.md#the-release-workflow)
|
||||
* [Release Storage & Structure](git.md#release-storage-structure)
|
||||
* [Command-Line Interaction](git.md#command-line-interaction)
|
||||
* [Work Documents](git.md#work-documents)
|
||||
* [Working With Work Documents](git.md#working-with-work-documents)
|
||||
* [Proposing Work Documents](git.md#proposing-work-documents)
|
||||
* [State Management](git.md#state-management)
|
||||
* [Managing Work Document Permissions](git.md#managing-work-document-permissions)
|
||||
* [Support Reticulum](support.md)
|
||||
* [Donations](support.md#donations)
|
||||
* [Provide Feedback](support.md#provide-feedback)
|
||||
|
||||
+517
-41
@@ -4,14 +4,16 @@
|
||||
Git Over Reticulum
|
||||
******************
|
||||
|
||||
A set of utilities for distributed collaborative software development and publishing is included in RNS.
|
||||
A set of utilities for distributed collaborative software development and publishing are included in RNS.
|
||||
|
||||
The system consists of two parts: The ``rngit`` node that hosts repositories, and the ``git-remote-rns`` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: ``rns://DESTINATION_HASH/group/repo``.
|
||||
|
||||
If you set a branch to track a Reticulum remote as the default upstream, you can simply use ``git`` as you normally would; all commands work transparently and as expected.
|
||||
|
||||
.. warning::
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, ``rngit`` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible `permission system`_ for allowing many users to interact with many different repositories on a single node, ``rngit`` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
|
||||
.. _permission system: #permissions
|
||||
|
||||
The rngit Utility
|
||||
=================
|
||||
@@ -31,7 +33,7 @@ Run ``rngit`` to start a repository node:
|
||||
|
||||
On the first run, ``rngit`` will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.
|
||||
|
||||
View your identity and destination hashes:
|
||||
Them, view your identity and destination hashes:
|
||||
|
||||
.. code:: text
|
||||
|
||||
@@ -73,6 +75,13 @@ Get changes from a remote repository:
|
||||
|
||||
$ git pull rns_remote master
|
||||
|
||||
Fork an existing repository from a remote to your ``rngit`` node:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit fork rns://8a37cdd16938ce79861561adbd59023a/reticulum/lxmf rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
|
||||
**All Command-Line Options (rngit)**
|
||||
|
||||
.. code:: text
|
||||
@@ -104,12 +113,223 @@ The ``git-remote-rns`` helper is automatically invoked by Git when interacting w
|
||||
The client configuration file is located at ``~/.rngit/client_config`` and allows adjusting parameters such as the reference batch size for transfers.
|
||||
|
||||
|
||||
Repository Creation & Management
|
||||
================================
|
||||
|
||||
The ``rngit`` utility provides several ways to create and manage repositories on a node: creating empty repositories, forking from existing repositories, and mirroring remote repositories.
|
||||
|
||||
Creating Empty Repositories
|
||||
---------------------------
|
||||
|
||||
To create a new empty repository on a remote node:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit create rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Repository public/myrepo created
|
||||
|
||||
This creates a bare Git repository at the specified path. You must have ``create`` permission for the target group. When a repository is created, the creator automatically receives ``adm`` (admin) permissions on the repository through an auto-generated ``.allowed`` file.
|
||||
|
||||
**All Command-Line Options (rngit create)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit create [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
repository
|
||||
|
||||
Reticulum Git Repository Creation
|
||||
|
||||
positional arguments:
|
||||
repository URL of repository to create
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
Forking Repositories
|
||||
--------------------
|
||||
|
||||
Forking creates a copy of an existing repository (from any accessible Git URL) on your ``rngit`` node. Forks maintain a reference to their upstream source for later synchronization.
|
||||
|
||||
To fork a repository:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit fork https://github.com/user/original rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
Repository forked to public/myfork
|
||||
|
||||
The source can be any valid Git URL, including:
|
||||
|
||||
- HTTPS URLs: ``https://github.com/user/repo.git``
|
||||
- Git URLs: ``git://host.com/repo.git``
|
||||
- SSH URLs: ``ssh://git@host.com/repo.git``
|
||||
- Reticulum URLs: ``rns://DESTINATION_HASH/group/repo``
|
||||
- Local paths: ``/path/to/repo.git``
|
||||
|
||||
Forks are created as bare repositories with metadata tracking their origin. The fork process:
|
||||
|
||||
1. Creates a new bare repository
|
||||
2. Fetches all refs (``+refs/*:refs/*``) from the source
|
||||
3. Sets ``repository.rngit.type`` to ``fork``
|
||||
4. Sets ``repository.rngit.upstream.source`` to the source URL
|
||||
5. Grants creator admin permissions
|
||||
|
||||
**All Command-Line Options (rngit fork)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit fork [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
source target
|
||||
|
||||
Reticulum Git Repository Forker
|
||||
|
||||
positional arguments:
|
||||
source URL of source repository
|
||||
target URL of target repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
Mirroring Repositories
|
||||
----------------------
|
||||
|
||||
Mirrors are similar to forks but are designed for keeping a local copy synchronized with an upstream repository. Mirrors can be automatically updated on a configurable schedule.
|
||||
|
||||
To create a mirror:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit mirror https://github.com/user/upstream rns://50824b711717f97c2fb1166ceddd5ea9/public/mymirror
|
||||
|
||||
Repository mirrored to public/mymirror
|
||||
|
||||
Mirrors are created with the same process as forks, but with ``repository.rngit.type`` set to ``mirror`` and an additional ``repository.rngit.upstream.sync`` timestamp tracking the last successful synchronization.
|
||||
|
||||
**All Command-Line Options (rngit mirror)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit mirror [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
source target
|
||||
|
||||
Reticulum Git Mirror Management
|
||||
|
||||
positional arguments:
|
||||
source URL of source repository
|
||||
target URL of target repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
Automatic Mirror Synchronization
|
||||
--------------------------------
|
||||
|
||||
The ``rngit`` node can automatically keep mirrors synchronized with their upstream sources. This is configured in the main configuration file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[rngit]
|
||||
mirror_interval = 24
|
||||
|
||||
The ``mirror_interval`` specifies the synchronization interval in hours (default: 24). The node checks for mirrors needing sync every 15 minutes, and fetches updates from upstream if the configured interval has elapsed since the last sync.
|
||||
|
||||
For automatic sync to happen, the repository must have been created with ``rngit mirror``. Sync failures are logged but do not prevent future retry attempts. The sync timestamp is only updated on successful completion.
|
||||
|
||||
Manual Synchronization
|
||||
----------------------
|
||||
|
||||
Both forks and mirrors can be manually synchronized on demand using the ``sync`` command:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit sync rns://50824b711717f97c2fb1166ceddd5ea9/public/myfork
|
||||
|
||||
Repository synced
|
||||
|
||||
This fetches all refs from the upstream source configured when the repository was created. You must have ``read`` and ``write`` permissions for the repository to perform a manual sync.
|
||||
|
||||
For mirrors, manual sync also updates the sync timestamp, effectively resetting the automatic sync timer.
|
||||
|
||||
**All Command-Line Options (rngit sync)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit sync [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i PATH] [-v] [-q] [--version]
|
||||
repository
|
||||
|
||||
Reticulum Git Repository Syncer
|
||||
|
||||
positional arguments:
|
||||
repository URL of repository
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
Git Configuration Parameters
|
||||
----------------------------
|
||||
|
||||
Repositories created through ``rngit`` store metadata in Git configuration:
|
||||
|
||||
- ``repository.rngit.type`` - Either ``fork`` or ``mirror``
|
||||
- ``repository.rngit.upstream.source`` - The source URL used during creation
|
||||
- ``repository.rngit.upstream.sync`` - Unix timestamp of last successful sync for mirrors
|
||||
|
||||
These parameters are used by the sync system and can be queried using standard Git commands:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git config --get repository.rngit.type
|
||||
mirror
|
||||
|
||||
$ git config --get repository.rngit.upstream.source
|
||||
https://github.com/user/upstream
|
||||
|
||||
$ git config --get repository.rngit.upstream.sync
|
||||
1716230400
|
||||
|
||||
|
||||
|
||||
|
||||
Repository Structure
|
||||
====================
|
||||
|
||||
The ``rngit`` node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is ``group_name/repo_name``. For example, a repository at ``/var/git/public/myrepo`` would be accessible as ``public/myrepo`` via the URL ``rns://DESTINATION_HASH/public/myrepo``.
|
||||
|
||||
**Configuration**
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
The ``rngit`` node configuration file is located at ``~/.rngit/config`` (or ``/etc/rngit/config`` for system-wide installations). The default configuration includes:
|
||||
|
||||
@@ -118,20 +338,203 @@ The ``rngit`` node configuration file is located at ``~/.rngit/config`` (or ``/e
|
||||
- Announce intervals for network visibility
|
||||
- Optional statistics recording for repository activity
|
||||
|
||||
Access permissions can be configured at the group level in the config file, or per-repository using ``.allowed`` files. Permissions use the format ``permission:target`` where permission is ``r`` (read), ``w`` (write), ``rw`` (read/write), ``c`` (create) or ``s`` (stats) and target is ``all``, ``none``, or a specific identity hash.
|
||||
|
||||
The ``s`` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set ``record_stats = yes`` in the ``[rngit]`` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to ``stats_ignore_identities``.
|
||||
Permissions
|
||||
===========
|
||||
|
||||
Repository-specific ``.allowed`` files can be static text files or executable scripts that output permission rules to stdout. A ``group.allowed`` file in a repository group directory applies to all repositories within that group.
|
||||
The ``rngit`` permission system provides fine-grained access control at multiple levels: group-level, repository-level, and document-level. Permissions can be statically configured in files or dynamically generated via executable scripts.
|
||||
|
||||
Access permissions can be configured at the group level in the config file or per-group ``.allowed`` files, or per-repository ``.allowed`` files. The ``s`` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set ``record_stats = yes`` in the ``[rngit]`` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to ``stats_ignore_identities``.
|
||||
|
||||
By default, **no** permissions are granted for anything! You will have to enable the permissions you require to be able to actually *do* something with ``rngit``.
|
||||
|
||||
Permissions can be modified by editing the ``rngit`` config file, individual ``.allowed`` files on disk, or remotely using the ``rngit perms`` command.
|
||||
|
||||
Permission Types
|
||||
----------------
|
||||
|
||||
The following permissions are supported:
|
||||
|
||||
- ``r`` (read) - Clone, fetch, and view repositories and work documents
|
||||
- ``w`` (write) - Push changes and manage work documents
|
||||
- ``rw`` (read/write) - Combined read and write access
|
||||
- ``c`` (create) - Create, fork or mirror new repositories within a group
|
||||
- ``s`` (stats) - View repository activity statistics
|
||||
- ``rel`` (release) - Create and manage releases
|
||||
- ``i`` (interact) - Comment on and interact with work documents
|
||||
- ``p`` (propose) - Propose new work documents (without full write access)
|
||||
- ``adm`` (admin) - Full access
|
||||
|
||||
Permission targets can be:
|
||||
|
||||
- ``all`` or ``a`` - Everyone
|
||||
- ``none`` or ``n`` - Nobody
|
||||
- A specific Reticulum identity hash
|
||||
|
||||
Permission Hierarchy
|
||||
--------------------
|
||||
|
||||
Permissions are resolved in the following hierarchy:
|
||||
|
||||
1. **Repository-level permissions** - Checked first, if none exists group permissions are checked
|
||||
2. **Group-level permissions** - Used as fallback if no repository-level permissions are set
|
||||
3. **Admin override** - Finally, potential admin rights are checked
|
||||
|
||||
For work documents, work document specific permissions are always checked first, and work documents have additional specific checks such as modifications only being possible by the document author.
|
||||
|
||||
Configuration Methods
|
||||
---------------------
|
||||
|
||||
**Group-Level Configuration**
|
||||
|
||||
Group permissions can be configured in the ``[access]`` section of the main config file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[access]
|
||||
public = r:all, w:9710b86ba12c42d1d8f30f74fe509286
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
collaborative = r:all, i:all, p:all, w:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
Additionally, they can be configured in a group ``group_name.allowed`` file, placed next to the ``group_name`` group directory.
|
||||
|
||||
**Repository-Level Configuration**
|
||||
|
||||
Repository-specific permissions are set in ``.allowed`` files placed next to the repository directory (for example, ``myrepo.allowed`` for ``myrepo``):
|
||||
|
||||
.. code:: text
|
||||
|
||||
# myrepo.allowed
|
||||
r:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
rel:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
**Dynamic Permissions**
|
||||
|
||||
Permission files can be made executable to generate permissions dynamically:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ chmod +x myrepo.allowed
|
||||
|
||||
When executable, the script is run and its stdout is parsed as permission rules. This allows integration with external authentication systems.
|
||||
|
||||
Work Document Permissions
|
||||
-------------------------
|
||||
|
||||
Work documents support additional permission granularity through ``.allowed`` files in the work directory (e.g., ``42.allowed`` for document #42). These files use the same permission syntax but only support:
|
||||
|
||||
- ``r`` (read) - View the document
|
||||
- ``w`` (write) - Edit the document
|
||||
- ``i`` (interact) - Comment on the document
|
||||
- ``p`` (propose) - Propose changes (future use)
|
||||
- ``adm`` (admin) - Full control over the document
|
||||
|
||||
Document permissions override repository permissions for that specific document. Work document permissions can be updated simply by editing the ``.allowed`` file, or remotely by using the ``rngit work`` command.
|
||||
|
||||
Creator Permissions
|
||||
-------------------
|
||||
|
||||
When a user creates a repository (via ``create``, ``fork``, or ``mirror``), they are automatically granted ``adm`` (admin) permissions on that repository.
|
||||
|
||||
When a user creates a work document, they automatically receive ``interact`` and ``write`` permissions on that document.
|
||||
|
||||
Permission Examples
|
||||
-------------------
|
||||
|
||||
**Example 1: Public Read, Restricted Write**
|
||||
|
||||
.. code:: text
|
||||
|
||||
r:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
Everyone can read, only the specified identity can write.
|
||||
|
||||
**Example 2: Collaborative Development**
|
||||
|
||||
.. code:: text
|
||||
|
||||
r:all
|
||||
i:all
|
||||
p:all
|
||||
w:9710b86ba12c42d1d8f30f74fe509286
|
||||
rel:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
Everyone can read, interact (comment), and propose work documents. Only the specified identity can write, create releases, and manage work documents fully.
|
||||
|
||||
**Example 3: Private Repository**
|
||||
|
||||
.. code:: text
|
||||
|
||||
rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
rw:a1b2c3d4e5f686ba12c42d1ba12ef1aa
|
||||
|
||||
Only the two specified identities have any access (read or write).
|
||||
|
||||
**Example 4: Mirror with Stats**
|
||||
|
||||
.. code:: text
|
||||
|
||||
r:all
|
||||
s:all
|
||||
w:none
|
||||
|
||||
Everyone can read and view stats, but nobody can push (mirror is read-only from upstream).
|
||||
|
||||
Permission Short Forms
|
||||
----------------------
|
||||
|
||||
Permissions can be specified using short or long forms:
|
||||
|
||||
- ``r`` = ``read``
|
||||
- ``w`` = ``write``
|
||||
- ``rw`` = ``readwrite``
|
||||
- ``c`` = ``create``
|
||||
- ``s`` = ``stats``
|
||||
- ``rel`` = ``release``
|
||||
- ``i`` = ``interact``
|
||||
- ``p`` = ``propose``
|
||||
- ``adm`` = ``admin``
|
||||
|
||||
Targets can also use short forms:
|
||||
|
||||
- ``a`` = ``all`` = ``everyone``
|
||||
- ``n`` = ``none`` = ``nobody``
|
||||
|
||||
Permission Configuration Locations
|
||||
----------------------------------
|
||||
|
||||
- User install: ``~/.rngit/config``
|
||||
- System install: ``/etc/rngit/config``
|
||||
- Group permissions: ``<group_root>/<group_name>.allowed``
|
||||
- Repository permissions: ``<group_root>/<group_name>/<repo_name>.allowed``
|
||||
- Document permissions: ``<group_root>/<group_name>.work/<doc_id>.allowed``
|
||||
|
||||
Identity & Destination Aliases
|
||||
==============================
|
||||
|
||||
To make permission and remote destination management easier, you can locally define aliases for commonly used identity and destination hashes. Identity aliases used in permissions resolution can be defined in the ``[aliases]`` section of the ``~/.rngit/config`` file, while destination aliases are defined in the ``[aliases]`` section of the ``~/.rngit/client_config`` file.
|
||||
|
||||
All alias definitions take the form of ``aliased_name = HASH``:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[aliases]
|
||||
alice = d09285e660cfe27cee6d9a0beb58b7e0
|
||||
bob = ffcffb4e255e156e77f79b82c13086a6
|
||||
|
||||
**Aliases are always resolved locally!** If for example you fork a repository with ``rngit fork rns://bobs_node/public/repo_name rns://my_node/forks/repo_name``, the forked repository will of course still reference the full, original destination hash, and use this for subsequent upstream syncs.
|
||||
|
||||
Serving Pages Over Nomad Network
|
||||
================================
|
||||
|
||||
In addition to providing Git repository access via the Git remote helper protocol, ``rngit`` can also run a `Nomad Network <https://github.com/markqvist/nomadnet>`_ compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
|
||||
In addition to providing Git repository access via the Git remote helper protocol and command-line tools, ``rngit`` can also run a `Nomad Network <https://github.com/markqvist/nomadnet>`_ compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
|
||||
|
||||
When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.
|
||||
|
||||
**Enabling the Git Page Node**
|
||||
Enabling the Git Page Node
|
||||
--------------------------
|
||||
|
||||
To enable the page node, add the following to your ``~/.rngit/config`` file:
|
||||
|
||||
@@ -151,7 +554,8 @@ When the page node is enabled, ``rngit`` will listen on a Nomad Network node des
|
||||
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
|
||||
|
||||
**Accessing Repository Pages**
|
||||
Accessing Repository Pages
|
||||
--------------------------
|
||||
|
||||
Once the page node is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:
|
||||
|
||||
@@ -168,7 +572,7 @@ Once the page node is running, you can access it from any Nomad Network client b
|
||||
All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.
|
||||
|
||||
Formatting & Syntax Highlighting
|
||||
================================
|
||||
--------------------------------
|
||||
|
||||
If the ``pygments`` Python module is installed on your system, the page node will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.
|
||||
|
||||
@@ -191,8 +595,10 @@ Code blocks in Markdown can include language hints for syntax highlighting:
|
||||
print("Hello, Reticulum!")
|
||||
```
|
||||
|
||||
You can use ``rawmu`` code blocks to render raw Micron inside Markdown files. If you create a code block with the language hint ``rawmu``, everything inside it will be treated as Micron directly.
|
||||
|
||||
Customizing Templates
|
||||
=====================
|
||||
---------------------
|
||||
|
||||
The page node uses a template system that allows complete customization of the generated pages. Templates are stored in the ``~/.rngit/templates/`` directory as Micron files.
|
||||
|
||||
@@ -233,11 +639,12 @@ By default, the page node uses Nerd Font icons. If you prefer simpler icons or y
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = yes
|
||||
|
||||
**Repository Statistics**
|
||||
Repository Statistics
|
||||
---------------------
|
||||
|
||||
When statistics recording is enabled (see the ``record_stats`` configuration option), the page node can display activity charts for each repository. The statistics page shows:
|
||||
|
||||
- Total and peak views, fetches and pushes
|
||||
- Total and peak views, downloads, fetches and pushes
|
||||
- Daily activity charts over a 90-day period
|
||||
- Combined activity visualization
|
||||
|
||||
@@ -247,28 +654,29 @@ To view statistics, a user must have the ``s`` (stats) permission for the reposi
|
||||
|
||||
The page node includes a "Thanks" feature that allows users to express appreciation for a repository. On each repository page, a "Thanks" link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.
|
||||
|
||||
**Configuration Example**
|
||||
Configuration Example
|
||||
---------------------
|
||||
|
||||
A complete page node configuration might look like this:
|
||||
A complete node configuration might look like this:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[rngit]
|
||||
node_name = My Git Node
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
node_name = My Git Node
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
|
||||
[repositories]
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
|
||||
[access]
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
|
||||
|
||||
Release Management
|
||||
@@ -276,7 +684,8 @@ Release Management
|
||||
|
||||
In addition to hosting Git repositories, ``rngit`` provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the ``rngit release`` subcommand, and are also viewable through the Nomad Network page interface.
|
||||
|
||||
**The Release Workflow**
|
||||
The Release Workflow
|
||||
--------------------
|
||||
|
||||
Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The ``rngit`` client will open your configured ``$EDITOR`` to compose release notes, then upload all artifacts to the remote repository node.
|
||||
|
||||
@@ -295,7 +704,8 @@ This will:
|
||||
|
||||
If no ``$EDITOR`` environment variable is set, ``rngit`` will try to use ``nano``, ``vim`` or ``vi``. The editor will show a template with instructions. Lines starting with ``#`` will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.
|
||||
|
||||
**Release Storage & Structure**
|
||||
Release Storage & Structure
|
||||
---------------------------
|
||||
|
||||
Releases are stored on the node in a directory named ``repo_name.releases`` next to the bare repository. Each release is a subdirectory containing:
|
||||
|
||||
@@ -304,6 +714,10 @@ Releases are stored on the node in a directory named ``repo_name.releases`` next
|
||||
- ``artifacts/`` - All uploaded files
|
||||
- ``THANKS`` - Appreciation count from users
|
||||
|
||||
|
||||
Command-Line Interaction
|
||||
------------------------
|
||||
|
||||
**Listing Releases**
|
||||
|
||||
To view all releases for a repository:
|
||||
@@ -375,8 +789,6 @@ Release management requires the ``release`` permission, configured the same way
|
||||
|
||||
When the Nomad Network page node is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.
|
||||
|
||||
Only releases with ``published`` status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.
|
||||
|
||||
**All Command-Line Options (rngit release)**
|
||||
|
||||
.. code:: text
|
||||
@@ -411,6 +823,9 @@ Work Documents
|
||||
|
||||
In addition to releases, ``rngit`` provides a work document management system for tracking tasks, investigations, issues and progress related to repositories. Work documents are stored as structured msgpack data and support threaded updates and comments.
|
||||
|
||||
Working With Work Documents
|
||||
---------------------------
|
||||
|
||||
**Listing Work Documents**
|
||||
|
||||
To view work documents for a repository:
|
||||
@@ -427,7 +842,7 @@ To view work documents for a repository:
|
||||
1 Implemented new feature 9710b86ba12c4f2e… 2025-01-15 14:32 3
|
||||
2 Fixed bug in parser 8f3a21c9d84e927b… 2025-01-14 09:15 1
|
||||
|
||||
Use ``--scope completed`` to view completed work documents, or ``--scope all`` to see both active and completed.
|
||||
Use ``--scope completed`` to view completed work documents, ``--scope proposed`` to view proposed documents, or ``--scope all`` to see all scopes.
|
||||
|
||||
**Viewing a Work Document**
|
||||
|
||||
@@ -485,6 +900,34 @@ To add an update to a work document:
|
||||
|
||||
This opens your editor to compose the update.
|
||||
|
||||
Proposing Work Documents
|
||||
------------------------
|
||||
|
||||
Users with ``propose`` permission can create work document proposals without full ``write`` access. Proposals are created in a "proposed" state and must be activated by a user with appropriate permissions before becoming active.
|
||||
|
||||
To propose a work document:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo propose --title "Feature proposal"
|
||||
|
||||
This opens your editor to compose the proposal content. When saved, the document is created in the "proposed" scope. The creator automatically receives ``interact`` and ``write`` permissions on the proposed document.
|
||||
|
||||
Proposed documents are visible through ``--scope proposed`` or ``--scope all``:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list --scope proposed
|
||||
|
||||
**Permissions for Proposals**
|
||||
|
||||
- Creating proposals requires ``propose`` permission on the repository
|
||||
- The creator automatically gets ``interact`` and ``write`` on their proposed document
|
||||
- Activating a proposal requires ``write`` and ``interact`` permissions
|
||||
|
||||
State Management
|
||||
----------------
|
||||
|
||||
**Completing Work Documents**
|
||||
|
||||
To mark a work document as completed (moving it from ``active`` to ``completed``):
|
||||
@@ -516,20 +959,51 @@ To delete a work document and all its comments:
|
||||
Are you sure you want to delete active work document #1? [y/N]: y
|
||||
Work document #1 deleted
|
||||
|
||||
**Permissions**
|
||||
Managing Work Document Permissions
|
||||
----------------------------------
|
||||
|
||||
Users can view work documents and updates if the have ``read`` permission for the repository. If users have ``read`` and ``interact``, they can also post updates/comments on existing work documents. Work document management requires having ``write`` and ``interact`` permission to the repository. These permissions are configured the same way as any other repository permissions. In the config file or ``.allowed`` files, use ``i:target`` to grant work document interaction rights:
|
||||
Users with administrative access to a work document can manage its specific permissions. This allows fine-grained control over who can read, write, comment on, or administer individual work documents.
|
||||
|
||||
To view or edit permissions for a work document:
|
||||
|
||||
.. code:: text
|
||||
|
||||
# In .allowed file or config
|
||||
i:all # Allow everyone
|
||||
i:9710b86... # Allow specific identity
|
||||
i:none # Deny everyone
|
||||
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo perms -d 1
|
||||
|
||||
**Author Verification**
|
||||
This opens your editor with the current permission configuration:
|
||||
|
||||
Users can only edit or delete work documents and updates they created. The author is cryptographically verified from the interacting link's ``remote_identity``.
|
||||
.. code:: text
|
||||
|
||||
r:all
|
||||
i:9710b86ba12c42d1d8f30f74fe509286
|
||||
adm:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
Permission rules follow the same format as repository permissions:
|
||||
|
||||
- ``r:target`` - Grant read access
|
||||
- ``w:target`` - Grant write access
|
||||
- ``i:target`` - Grant interact (comment) access
|
||||
- ``adm:target`` - Grant admin access
|
||||
|
||||
Targets can be ``all``, ``none``, or a specific identity hash.
|
||||
|
||||
**Who Can Edit Permissions**
|
||||
|
||||
Document permissions can be edited by:
|
||||
|
||||
- The original author (if they also have ``interact`` and ``write`` on the repository)
|
||||
- Any user with ``admin`` permission on the document
|
||||
- Repository admins (through inherited permissions)
|
||||
|
||||
**Permission Precedence**
|
||||
|
||||
Document-specific permissions override repository-level permissions for that document. If document permissions exist, they are checked first; if access is not granted there, repository permissions are checked.
|
||||
|
||||
**Author Rights:**
|
||||
|
||||
- Users can only edit or delete work documents they created
|
||||
- The author is cryptographically verified from the interacting link's ``remote_identity``
|
||||
- Document creators automatically receive ``interact`` and ``write`` on their documents
|
||||
|
||||
**Storage Format**
|
||||
|
||||
@@ -537,6 +1011,7 @@ Work documents are stored in a ``repo_name.work`` directory next to the reposito
|
||||
|
||||
- ``active/`` - Active work documents
|
||||
- ``completed/`` - Completed work documents
|
||||
- ``proposed/`` - Proposed work documents
|
||||
|
||||
Each document is a numbered directory containing:
|
||||
|
||||
@@ -560,7 +1035,8 @@ When the Nomad Network page node is enabled, work documents are viewable through
|
||||
|
||||
positional arguments:
|
||||
repository URL of remote repository
|
||||
operation list, view, create, edit, delete, update or complete
|
||||
operation list, view, create, propose, edit, delete,
|
||||
update, complete, activate or perms
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -568,8 +1044,8 @@ When the Nomad Network page node is enabled, work documents are viewable through
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i, --identity PATH path to identity
|
||||
--scope SCOPE document scope: active, completed or all
|
||||
-t, --title TITLE document title for create
|
||||
--scope SCOPE document scope: active, completed, proposed or all
|
||||
-t, --title TITLE document title for create/propose
|
||||
-d, --id ID document ID
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
|
||||
Reference in New Issue
Block a user