## gof3 As a CLI or as a library, GoF3 provides a single operation: mirroring. The origin and destination are designated by the URL of a forge and a path to the resource. For instance, `mirror --from-type forgejo --from https://code.forgejo.org/forgejo/lxc-helpers --to-type F3 --to /some/directory` will mirror a project in a local directory using the F3 format. ## Building * Install go >= v1.21 * make f3-cli * ./f3-cli mirror -h ## Example ### To F3 Login to https://code.forgejo.org and obtain an application token with read permissions at https://code.forgejo.org/user/settings/applications. ```sh f3-cli mirror \ --from-type forgejo --from-forgejo-url https://code.forgejo.org \ --from-forgejo-token $codetoken \ --from-path /forge/organizations/actions/projects/cascading-pr \ --to-type filesystem --to-filesystem-directory /tmp/cascading-pr ``` ### From F3 Run a local Forgejo instance with `serials=1 tests/setup-forgejo.sh` and obtain an application token with: ```sh docker exec --user 1000 forgejo1 forgejo admin user generate-access-token -u root --raw --scopes 'all,sudo' ``` Mirror issues ```sh f3-cli mirror \ --from-type filesystem --from-filesystem-directory /tmp/cascading-pr \ --from-path /forge/organizations/actions/projects/cascading-pr/issues \ --to-type forgejo --to-forgejo-url http://0.0.0.0:3001 \ --to-forgejo-token $localtoken ``` Visit them at http://0.0.0.0:3001/actions/cascading-pr/issues ## Testing ### Requirements The tests require a live GitLab instance as well as a live Forgejo instance and will use up to 16GB of RAM. * Install docker * `./test/run.sh` ## License This project is [MIT licensed](LICENSE). ## Architecture [F3](https://f3.forgefriends.org/) is a hierarchy designed to be stored in a file system. It is represented in memory with the [tree/generic](tree/generic) abstract data structure that can be saved and loaded from disk by the [forges/filesystem](forges/filesystem) driver. Each forge (e.g. [forges/forgejo](forges/forgejo)) is supported by a driver that is responsible for the interactions of each resource (e.g `issues`, `asset`, etc.). ### Tree [tree/f3](tree/f3) implements a [F3](https://f3.forgefriends.org/) hierarchy based on the [tree/generic](tree/generic) data structure. The [tree](tree/generic/tree.go) has a [logger](logger) for messages, [options](options) defining which forge it relates to and how and a pointer to the root [node](tree/generic/node.go) of the hierarchy (i.e. the `forge` F3 resource). The node ([tree/generic/node.go](tree/generic/node.go)) has: * a unique id (e.g. the numerical id of an `issue`) * a parent * chidren (e.g. `issues` children are `issues`, `issue` children are `comments` and `reactions`) * a kind that maps to a F3 resource (e.g. `issue`, etc.) * a driver for its concrete implementation for a given forge It relies on a forge driver for the concrete implemenation of a F3 resource (issue, reaction, repository, etc.). For instance the `issues` driver for Forgejo is responsible for listing the existing issues and the `issue` driver is responsible for creating, updating or deleting a Forgejo issue. ### F3 archive The [F3 JSON schemas](https://code.forgejo.org/f3/f3-schemas/-/tree/main) are copied in [f3/schemas](f3/schemas). Their internal representation and validation is found in a source file named after the resource (e.g. an `issue` represented by [f3/schemas/issue.json](f3/schemas/issue.json) is implemented by [f3/issue.go](f3/issue.go)). When a F3 resource includes data external to the JSON file (i.e. a Git repository or an asset file), the internal representation has a function to copy the data to the destination given in argument. For instance: * [f3/repository.go](f3/repository.go) `FetchFunc(destination)` will `git fetch --mirror` the repository to the `destination` directory. * [f3/releaseasset.go](f3/releaseasset.go) `DownloadFunc()` returns a `io.ReadCloser` that will be used by the caller to copy the asset to its destination. ### Options The Forge options at [options/interface.go](options/interface.go) define the parameters given when a forge is created: Each forge driver is responsible for registering the options (e.g. [Forgejo options](forges/forgejo/options/options.go)) and for registering a factory that will create these options (e.g. [Forgejo options registration](forgejo/main.go)). In addition to the options that are shared by all forges such as the logger, it may define additional options. ### Driver interface For each [F3](https://f3.forgefriends.org/) resource, the driver is responsible for: * copying the [f3](f3) argument to `FromFormat` to the forge * `ToFormat` reads from the forge and convert the data into an [f3/resources.go](f3/resources.go) A driver must have a unique name (e.g. `forgejo`) and [register](forges/forgejo/main.go): * an [options factory](options/factory.go) * a [forge factory](tree/f3/forge_factory.go) #### Tree driver The [tree driver](tree/generic/driver_tree.go) functions (e.g. [forges/forgejo/tree.go](forges/forgejo/tree.go)) specialize [NullTreeDriver](tree/generic/driver_tree.go). * **Factory(ctx context.Context, kind generic.Kind) generic.NodeDriverInterface** creates a new node driver for a given [`Kind`](tree/f3/kind.go). * **GetPageSize() int** returns the default page size. #### Node driver The [node driver](tree/generic/driver_node.go) functions for [each `Kind`](tree/f3/kind.go) (e.g. `issues`, `issue`, etc.) specialize [NullNodeDriver](tree/generic/driver_node.go). The examples are given for the Forgejo [`issue`](forges/forgejo/issue.go) and [`issues`](forges/forgejo/issues.go) drivers, matching the REST API endpoint to the driver function. * **ListPage(context.Context, page int) ChildrenSlice** returns children of the node paginated [GET /repos/{owner}/{repo}/issues](https://code.forgejo.org/api/swagger/#/issue/issueListIssues) * **Get(context.Context)** get the content of the resource (e.g. [GET /repos/{owner}/{repo}/issues/{index}](https://code.forgejo.org/api/swagger/#/issue/issueGetIssue)) * **Put(context.Context) NodeID** create a new resource and return the identifier (e.g. [POST /repos/{owner}/{repo}/issues](https://code.forgejo.org/api/swagger/#/issue/issueCreateIssue)) * **Patch(context.Context)** modify an existing resource (e.g. [PATCH /repos/{owner}/{repo}/issues/{index}](https://code.forgejo.org/api/swagger/#/issue/issueEditIssue)) * **Delete(context.Context)** delete an existing resource (e.g. [DELETE /repos/{owner}/{repo}/issues/{index}](https://code.forgejo.org/api/swagger/#/issue/issueDelete)) * **NewFormat() f3.Interface** create a new `issue` F3 object * **FromFormat(f3.Interface)** set the internal representation from the given F3 resource * **ToFormat() f3.Interface** convert the internal representation into the corresponding F3 resource. For instance the internal representation of an `issue` for the Forgejo driver is the `Issue` struct of the Forgejo SDK. #### Options The [options](options) created by the factory are expected to provide the [options interfaces](options/interface.go): * Required * LoggerInterface * URLInterface * Optional * CLIInterface if additional CLI arguments specific to the forge are supported For instance [forges/forgejo/options/options.go](forges/forgejo/options/options.go) is created by [forges/forgejo/options.go](forges/forgejo/options.go). ### Driver implementation A driver for a forge must be self contained in a directory (e.g. [forges/forgejo](forges/forgejo)). Functions shared by multiple forges are grouped in the [forges/helpers](forges/helpers) directory and split into one directory per `Kind` (e.g. [forges/helpers/pullrequest](forges/helpers/pullrequest)). * [options.go](forges/forgejo/options.go) defines the name of the forge in the Name variable (e.g. Name = "forgejo") * [options/options.go](forges/forgejo/options/options.go) defines the options specific to the forge and the corresponding CLI flags * [main.go](forges/forgejo/main.go) calls f3_tree.RegisterForgeFactory to create the forge given its name * [tree.go](forges/forgejo/tree.go) has the `Factory()` function that maps a node kind (`issue`, `reaction`, etc.) into an object that is capable of interacting with it (CRUD). * one file per `Kind` (e.g. [forges/forgejo/issues.go](forges/forgejo/issues.go)). ### Idempotency Mirroring is idempotent: it will produce the same result if repeated multiple times. The drivers functions are not required to be idempotent. * The `Put` function will only be called if the resource does not already exist. * The `Patch` and `Delete` functions will only be called if the resource exists. ### Identifiers mapping When a forge (e.g. Forgejo) is mirrored on the filesystem, the identifiers are preserved verbatim (e.g. the `issue` identifier). When the filesystem is mirrored to a forge, the identifiers cannot always be preserved. For instance if an `issue` with the identifier 1234 is downloaded from Forgejo and created on another Forgejo instance, it will be allocated an identifier by the Forgejo instance. It cannot request to be given a specific identifier. ### References A F3 resource may reference another F3 resource by a path. For instance the user that authored an issue is represented by `/forge/users/1234` where `1234` is the unique identifier of the user. The reference is relative to the forge. The mirroring of a forge to another is responsible for converting the references using the identifier mapping stored in the origin forge. For instance if `/forge/users/1234` stored in the filesystem is created in Forgejo as `/forge/users/58`, the `issue` stored in the filesystem with its authored as `/forge/users/1234` will be created in Forgejo to be authored by `/forge/users/58` instead. ### Logger The [tree/generic](tree/generic) has a pointer to a logger implementing [logger.Interface](logger/interface.go) which is made available to the nodes and the drivers. ### Context All functions except for setters and getters have a `context.Context` argument which is checked (using [util/terminate.go](util/terminate.go)) to not be `Done` before performing a long lasting operation (e.g. a REST API call or a call to the Git CLI). It is not used otherwise. ### Error model When an error that cannot be recovered from happens, `panic` is called, otherwise an `Error` is logged. ### CLI The CLI is in [cmd](cmd) and relies on [options](options) to figure out which options are to be implemented for each supported forge. ## Hacking ### Local tests The forge instance is deleted before each run and left running for forensic analysis when the run completes. ```sh ./tests/run.sh test_forgejo # http://0.0.0.0:3001 user root, password admin1234 ./tests/run.sh test_gitlab # http://0.0.0.0:8181 user root, password Wrobyak4 ./tests/run.sh test_gitea # http://0.0.0.0:3001 user root, password admin1234 ``` Restart a new forge with: ```sh ./tests/run.sh run_forgejo # http://0.0.0.0:3001 user root, password admin1234 ./tests/run.sh run_gitlab # http://0.0.0.0:8181 user root, password Wrobyak4 ./tests/run.sh run_gitea # http://0.0.0.0:3001 user root, password admin1234 ``` The compliance test resources are deleted, except if the environment variable `GOF3_TEST_COMPLIANCE_CLEANUP=false`. ```sh GOF3_TEST_COMPLIANCE_CLEANUP=false GOF3_FORGEJO_HOST_PORT=0.0.0.0:3001 go test -run=TestF3Forge/forgejo -v code.forgejo.org/f3/gof3/... ``` ### Code coverage ```sh export SCRATCHDIR=/tmp/gof3 ./tests/run.sh # collect coverage for every test ./tests/run.sh run_forgejo # update coverage for forgejo ./tests/run.sh test_merge_coverage # merge coverage from every test go tool cover -func /tmp/gof3/merged.out # show coverage per function uncover /tmp/gof3/merged.out GeneratorSetReviewComment # show which lines of the GeneratorSetReviewComment function are not covered ``` ### F3 schemas The JSON schemas come from [the f3-schemas repository](https://code.forgejo.org/f3/f3-schemas) and should be updated as follows: ``` cd f3 ; rm -fr schemas ; git --work-tree schemas clone https://code.forgejo.org/f3/f3-schemas ; rm -fr f3-schemas schemas/.gitignore schemas/.forgejo ``` ## Funding See the page dedicated to funding in the [F3 documentation](https://f3.forgefriends.org/funding.html)