1
0
Fork 0

Adding upstream version 3.10.8.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-18 09:37:23 +02:00
parent 37e9b6d587
commit 03bfe4079e
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
356 changed files with 28857 additions and 0 deletions

84
.deadcode-out Normal file
View file

@ -0,0 +1,84 @@
code.forgejo.org/f3/gof3/v3/api
TreeMirror
code.forgejo.org/f3/gof3/v3/f3
RepositoryDirname
code.forgejo.org/f3/gof3/v3/forges/forgejo
common.isContainer
code.forgejo.org/f3/gof3/v3/forges/forgejo/sdk
hasAgent
GetAgent
Version
NewClientWithHTTP
UseSSHCert
UseSSHPubkey
SetOTP
SetContext
SetSudo
SetUserAgent
SetDebugMode
OptionalBool
OptionalString
OptionalInt64
VerifyWebhookSignature
VerifyWebhookSignatureMiddleware
NewHTTPSignWithPubkey
NewHTTPSignWithCert
newHTTPSign
findCertSigner
findPubkeySigner
SetGiteaVersion
code.forgejo.org/f3/gof3/v3/forges/gitlab
common.getTree
common.getF3Tree
common.getChildDriver
common.isContainer
common.getURL
common.getPushURL
common.getNewMigrationHTTPClient
common.getIsAdmin
common.getVersion
treeDriver.maybeSudo
code.forgejo.org/f3/gof3/v3/forges/helpers/tests/repository
TestHelper.GetNode
TestHelper.RevList
TestHelper.AssertRepositoryNotFileExists
TestHelper.BranchRepositoryFeature
code.forgejo.org/f3/gof3/v3/internal/hoverfly
MainTest
GetSingleton
testSimulate.Run
code.forgejo.org/f3/gof3/v3/options/cli
OptionsCLI.FromFlags
OptionsCLI.GetFlags
code.forgejo.org/f3/gof3/v3/tree/f3
NewLabelReference
NewPullRequestLabelReference
NewMilestoneReference
pullRequestNode.GetPullRequestHead
pullRequestNode.GetPullRequestRef
pullRequestNode.GetPullRequestPushRefs
newPullRequestNode
NewRepositoryPath
NewTopicPath
NewTopicPathString
NewTopicReference
code.forgejo.org/f3/gof3/v3/tree/f3/objects
FuncReadURLAndSetSHA
code.forgejo.org/f3/gof3/v3/tree/generic
MirrorOptions.SetNoRemap
TreePartialMirror
code.forgejo.org/f3/gof3/v3/tree/tests/f3
Creator.GetDirectory
Creator.Generate

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
root = true
[*]
indent_style = space
indent_size = 2
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

View file

@ -0,0 +1,35 @@
on:
push:
tags: 'v*'
jobs:
publish:
runs-on: docker-bookworm
container:
image: 'data.forgejo.org/oci/ci:1'
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- uses: https://data.forgejo.org/actions/setup-go@v5
with:
go-version-file: "go.mod"
- run: |
make f3-cli
mkdir release
mv f3-cli release/
- name: publish release
uses: https://data.forgejo.org/actions/forgejo-release@v2.6.0
with:
url: "https://code.forgejo.org"
repo: "f3/gof3"
direction: upload
tag: "${{ github.ref_name }}"
sha: "${{ github.sha }}"
release-dir: release
token: ${{ secrets.RELEASE_NOTES_ASSISTANT_TOKEN }}
override: true
verbose: ${{ vars.VERBOSE || "false" }}
release-notes-assistant: true
hide-archive-link: true

View file

@ -0,0 +1,38 @@
#
# secrets.F3_READ_PRIVATE_MIRRORS_TOKEN
# https://code.forgejo.org/forgejo-mirror scope read:repository
#
on:
pull_request_target:
push:
branches:
- 'main'
- 'wip-gitea'
jobs:
compliance-gitea:
runs-on: lxc-bookworm
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
with:
submodules: true
- uses: https://data.forgejo.org/actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: install jq make
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get -q install -qq -y jq make
- run: make deps-backend lint
- name: install zstd for actions/cache@v4
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get -q install -y -qq zstd
- name: run tests
run: |
./tests/run.sh prepare_container
su forgejo -c "./tests/run.sh test_gitea ${{ secrets.F3_READ_PRIVATE_MIRRORS_TOKEN }}"

View file

@ -0,0 +1,54 @@
on:
pull_request:
push:
branches:
- 'main'
jobs:
compliance:
runs-on: lxc-bookworm
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
with:
submodules: true
- uses: https://data.forgejo.org/actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: install jq make
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get -q install -qq -y jq make
- run: make deps-backend lint
- name: install hoverfly
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get -q install -qq -y unzip wget
version=$(./tests/run.sh hoverfly_version)
wget https://github.com/SpectoLabs/hoverfly/releases/download/v$version/hoverfly_bundle_linux_amd64.zip
unzip hoverfly_bundle_linux_amd64.zip
mv hoverfly hoverctl /usr/local/bin
- name: install zstd for actions/cache@v4
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get -q install -y -qq zstd
- name: get GitLab version
id: gitlab
run: |
echo "version=$(./tests/run.sh gitlab_version)" >> "$GITHUB_OUTPUT"
- name: cache GitLab OCI image
uses: https://data.forgejo.org/actions/cache@v4
with:
path: |
/srv/forgejo-binaries/gitlab
key: gitlab-${{ steps.gitlab.outputs.version }}
- name: run tests
run: |
./tests/run.sh prepare_container
su forgejo -c "./tests/run.sh run"

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
*~
f3-cli
coverage.out
coverage.html
tests/*.out
format/schemas/.gitignore
.cur-deadcode-out

6
.gitmodules vendored Normal file
View file

@ -0,0 +1,6 @@
[submodule "tests/setup-forgejo"]
path = tests/setup-forgejo
url = https://code.forgejo.org/actions/setup-forgejo
[submodule "tests/end-to-end"]
path = tests/end-to-end
url = https://code.forgejo.org/forgejo/end-to-end

81
.golangci.yml Normal file
View file

@ -0,0 +1,81 @@
#
# Copied from https://github.com/go-gitea/gitea/blob/cc649f0cb338a085373fd85a8b71e315701cbdc1/.golangci.yml
#
linters:
enable:
- gosimple
- typecheck
- govet
- errcheck
- staticcheck
#- unused # disabled because it gets it wrong with golangci-lint@v1.51.2 run & go 1.20.3
- gofmt
- misspell
- gocritic
- bidichk
- ineffassign
- revive
- gofumpt
- depguard
- nakedret
- unconvert
- wastedassign
- nolintlint
- stylecheck
enable-all: false
disable-all: true
fast: false
run:
go: 1.22
timeout: 10m
issues:
exclude-dirs:
- forges/forgejo/sdk
linters-settings:
stylecheck:
checks: ["all", "-ST1005", "-ST1003"]
nakedret:
max-func-lines: 0
gocritic:
disabled-checks:
- ifElseChain
- singleCaseSwitch # Every time this occurred in the code, there was no other way.
revive:
ignore-generated-header: false
severity: warning
confidence: 0.8
errorCode: 1
warningCode: 1
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: duplicated-imports
- name: modifies-value-receiver
depguard:
#list-type: denylist
# Check the list against standard lib.
#include-go-root: true
rules:
main:
deny:
- pkg: io/ioutil
desc: use os or io instead

20
LICENSE Normal file
View file

@ -0,0 +1,20 @@
Copyright Earl Warren <contact@earl-warren.org>
Copyright Loïc Dachary <loic@dachary.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

75
Makefile Normal file
View file

@ -0,0 +1,75 @@
GO ?= $(shell go env GOROOT)/bin/go
DIFF ?= diff --unified
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v2/cmd/editorconfig-checker@2.8.0 # renovate: datasource=go
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 # renovate: datasource=go
UNCOVER_PACKAGE ?= github.com/gregoryv/uncover/cmd/uncover@latest
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0 # renovate: datasource=go
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.24.0 # renovate: datasource=go
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.0 # renovate: datasource=go
SCHEMAS_VERSION ?= v3
DEADCODE_ARGS ?= -generated=false -test -f='{{println .Path}}{{range .Funcs}}{{printf "\t%s\n" .Name}}{{end}}{{println}}' code.forgejo.org/f3/gof3/v3/...
VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
LDFLAGS := $(LDFLAGS) -X "code.forgejo.org/f3/gof3/v3/cmd.Version=$(VERSION)"
EXECUTABLE := f3-cli
GO_DIRS = cmd f3 forges logger main options tree util
GO_SOURCES = $(shell find $(GO_DIRS) -type f -name "*.go")
$(EXECUTABLE): $(GO_SOURCES)
$(GO) build -tags 'netgo osusergo' -ldflags '-extldflags -static -s -w $(LDFLAGS)' -o $@ code.forgejo.org/f3/gof3/v3/main
.PHONY: deps-backend
deps-backend:
$(GO) mod download
$(GO) install $(GOFUMPT_PACKAGE)
$(GO) install $(GOLANGCI_LINT_PACKAGE)
$(GO) install $(UNCOVER_PACKAGE)
$(GO) install $(MISSPELL_PACKAGE)
$(GO) install $(DEADCODE_PACKAGE)
$(GO) install $(GOMOCK_PACKAGE)
.PHONY: lint
lint:
@if ! $(MAKE) lint-run ; then echo "Please run 'make lint-fix' and commit the result" ; exit 1 ; fi
.PHONY: lint-run
lint-run: lint-schemas
$(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS)
$(GO) run $(DEADCODE_PACKAGE) $(DEADCODE_ARGS) > .cur-deadcode-out
$(DIFF) .deadcode-out .cur-deadcode-out
.PHONY: lint-schemas
lint-schemas:
status=0 ; for schema in f3/schemas/*.json ; do if ! jq '.|empty' $$schema ; then status=1 ; echo $$schema error ; fi ; done ; exit $$status
d=`mktemp -d` ; trap "rm -fr $$d" EXIT ; git clone --branch $(SCHEMAS_VERSION) --quiet https://code.forgejo.org/f3/f3-schemas $$d/schemas ; diff --exclude '.*' -ru $$d/schemas f3/schemas
.PHONY: lint-schemas-fix
lint-schemas-fix:
d=`mktemp -d` ; trap "rm -fr $$d" EXIT ; git clone --branch $(SCHEMAS_VERSION) --quiet https://code.forgejo.org/f3/f3-schemas $$d/schemas ; cp $$d/schemas/* f3/schemas
.PHONY: lint-fix
lint-fix: lint-schemas-fix
$(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS) --fix
$(GO) run $(DEADCODE_PACKAGE) $(DEADCODE_ARGS) > .deadcode-out
.PHONY: fmt
fmt:
GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) gofumpt -extra -w .
SPELLCHECK_FILES = $(GO_DIRS)
.PHONY: lint-spell
lint-spell:
@$(GO) run $(MISSPELL_PACKAGE) -error $(SPELLCHECK_FILES)
.PHONY: lint-spell-fix
lint-spell-fix:
@$(GO) run $(MISSPELL_PACKAGE) -w $(SPELLCHECK_FILES)

226
README.md Normal file
View file

@ -0,0 +1,226 @@
## 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)

24
api/api.go Normal file
View file

@ -0,0 +1,24 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package api
import (
"context"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/util"
)
func TreeMirror(ctx context.Context, originTree, destinationTree generic.TreeInterface, p path.Path, options *generic.MirrorOptions) error {
err := util.PanicToError(func() {
generic.TreeMirror(ctx, originTree, destinationTree, p, options)
})
if err != nil {
originTree.Error(err.Error())
originTree.Debug(err.Stack())
}
return err
}

49
cmd/cli.go Normal file
View file

@ -0,0 +1,49 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package cmd
import (
"fmt"
"sort"
filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
"code.forgejo.org/f3/gof3/v3/options"
"github.com/urfave/cli/v3"
)
func ForgeTypeOption(direction string) string {
return direction + "-type"
}
func GetFlagsCommon(prefix, category string) []cli.Flag {
flags := make([]cli.Flag, 0, 10)
forgeTypes := make([]string, 0, 10)
for name := range options.GetFactories() {
forgeTypes = append(forgeTypes, name)
}
sort.Strings(forgeTypes)
values := &enumType{
Enum: forgeTypes,
Default: filesystem_options.Name,
}
flags = append(flags, &cli.GenericFlag{
Name: ForgeTypeOption(prefix),
Usage: fmt.Sprintf("`TYPE` of the %s forge", prefix),
Value: values,
DefaultText: values.GetDefaultText(),
Category: prefix,
})
flags = append(flags, &cli.StringFlag{
Name: BuildForgePrefix(prefix, "path"),
Usage: "resource to mirror (e.g. /forge/users/myuser/projects/myproject)",
Category: prefix,
})
return flags
}

48
cmd/enumtype.go Normal file
View file

@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
package cmd
import (
"fmt"
"strings"
)
type enumType struct {
Enum []string
Default string
selected string
}
func (o enumType) Join() string {
return strings.Join(o.Enum, ",")
}
func (o *enumType) Set(value string) error {
for _, enum := range o.Enum {
if strings.EqualFold(enum, value) {
o.selected = value
return nil
}
}
return fmt.Errorf("%v", o.Allowed())
}
func (o *enumType) Allowed() string {
return fmt.Sprintf("allowed values are %s", o.Join())
}
func (o *enumType) GetDefaultText() string {
return fmt.Sprintf("%s, %s", o.Default, o.Allowed())
}
func (o enumType) Get() any {
return o.String()
}
func (o enumType) String() string {
if o.selected == "" {
return o.Default
}
return o.selected
}

49
cmd/main.go Normal file
View file

@ -0,0 +1,49 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"code.forgejo.org/f3/gof3/v3/logger"
"github.com/urfave/cli/v3"
)
var Version = "development"
func SetVerbosity(ctx context.Context, verbosity int) {
l := logger.ContextGetLogger(ctx)
switch verbosity {
case 0:
l.SetLevel(logger.Info)
default:
l.SetLevel(logger.Trace)
}
}
func NewApp() *cli.Command {
return &cli.Command{
Name: "F3",
Usage: "Friendly Forge Format",
Description: `Friendly Forge Format`,
Version: Version,
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
SetVerbosity(ctx, c.Count("verbose"))
return nil, nil
},
Commands: []*cli.Command{
CreateCmdMirror(),
},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "verbose",
Usage: "increase the verbosity level",
},
cli.VersionFlag,
},
EnableShellCompletion: true,
}
}

129
cmd/mirror.go Normal file
View file

@ -0,0 +1,129 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"code.forgejo.org/f3/gof3/v3/logger"
"code.forgejo.org/f3/gof3/v3/options"
"code.forgejo.org/f3/gof3/v3/path"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/util"
"github.com/urfave/cli/v3"
)
var (
directionFrom = "from"
flagFrom = "--" + directionFrom
directionTo = "to"
flagTo = "--" + directionTo
)
func BuildForgePrefix(prefix, forge string) string {
return prefix + "-" + forge
}
func FlagsToTree(ctx context.Context, c *cli.Command, direction string) generic.TreeInterface {
forgeType := c.String(ForgeTypeOption(direction))
opts := options.GetFactory(forgeType)()
opts.(options.LoggerInterface).SetLogger(logger.ContextGetLogger(ctx))
if o, ok := opts.(options.CLIInterface); ok {
o.FromFlags(ctx, c, BuildForgePrefix(direction, forgeType))
} else {
panic("not implemented")
}
return generic.GetFactory("f3")(ctx, opts)
}
func CreateCmdMirror() *cli.Command {
flags := make([]cli.Flag, 0, 10)
for _, direction := range []string{"from", "to"} {
flags = append(flags, GetFlagsCommon(direction, "common")...)
for name, factory := range options.GetFactories() {
if opts, ok := factory().(options.CLIInterface); ok {
flags = append(flags, opts.GetFlags(BuildForgePrefix(direction, name), name)...)
}
}
}
flags = func(flags []cli.Flag) []cli.Flag {
dedup := make([]cli.Flag, 0, 10)
names := make(map[string]any, 10)
flagLoop:
for _, flag := range flags {
for _, name := range flag.Names() {
_, found := names[name]
if found {
continue flagLoop
}
}
dedup = append(dedup, flag)
for _, name := range flag.Names() {
names[name] = nil
}
}
return dedup
}(flags)
return &cli.Command{
Name: "mirror",
Usage: "Mirror",
Description: "Mirror",
Action: func(ctx context.Context, c *cli.Command) error {
return util.PanicToError(func() { runMirror(ctx, c) })
},
Flags: flags,
}
}
func runMirror(ctx context.Context, c *cli.Command) {
from := FlagsToTree(ctx, c, directionFrom)
to := FlagsToTree(ctx, c, directionTo)
fromPathString := c.String(BuildForgePrefix(directionFrom, "path"))
fromPath := generic.NewPathFromString(fromPathString)
toPathString := c.String(BuildForgePrefix(directionTo, "path"))
toPath := generic.NewPathFromString(toPathString)
log := from.GetLogger()
fromURL := "(unset)"
if url, ok := from.GetOptions().(options.URLInterface); ok {
fromURL = url.GetURL()
}
toURL := "(unset)"
if url, ok := to.GetOptions().(options.URLInterface); ok {
toURL = url.GetURL()
}
log.Info("mirror %s (%s at %s) to %s (%s at %s)",
fromPath, c.String(ForgeTypeOption(directionFrom)), fromURL,
toPath, c.String(ForgeTypeOption(directionTo)), toURL,
)
log.Debug("read %s from %T", fromPath, from)
var fromNode generic.NodeInterface
fromNode = generic.NilNode
walkAndGet := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
node.WalkAndGet(ctx, parent, generic.NewWalkOptions(nil))
fromNode = node
}
from.ApplyAndGet(ctx, fromPath, generic.NewApplyOptions(walkAndGet))
if fromNode == generic.NilNode {
panic(fmt.Errorf("from %s not found", fromPath))
}
from.Debug("copy %s from %T to %T", fromPath, from, to)
if toPathString == "" {
generic.TreeMirror(ctx, from, to, fromPath, generic.NewMirrorOptions())
} else {
toNode := to.FindAndGet(ctx, toPath)
if toNode == generic.NilNode {
panic(fmt.Errorf("to %s not found", toPath))
}
generic.NodeMirror(ctx, fromNode, toNode, generic.NewMirrorOptions())
}
}

139
cmd/mirror_test.go Normal file
View file

@ -0,0 +1,139 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"testing"
filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
forgejo_options "code.forgejo.org/f3/gof3/v3/forges/forgejo/options"
"code.forgejo.org/f3/gof3/v3/options"
"code.forgejo.org/f3/gof3/v3/tree/generic"
f3_tests "code.forgejo.org/f3/gof3/v3/tree/tests/f3"
tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_CmdMirrorArguments(t *testing.T) {
ctx := context.Background()
output, err := runApp(ctx, "f3", "mirror", "--from-type", "garbage")
assert.ErrorContains(t, err, `allowed values are `)
assert.Contains(t, output, "Incorrect Usage:")
}
func Test_CmdMirrorIntegrationDefaultToPath(t *testing.T) {
ctx := context.Background()
fixtureOptions := tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t)
fixtureTree := generic.GetFactory("f3")(ctx, fixtureOptions)
log := fixtureTree.GetLogger()
log.Trace("======= build fixture")
f3_tests.TreeBuild(t, "CmdMirrorDefault", fixtureOptions, fixtureTree)
log.Trace("======= create mirror")
mirrorOptions := tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t)
mirrorTree := generic.GetFactory("f3")(ctx, mirrorOptions)
p := "/forge/users/10111"
log.Trace("======= mirror %s", p)
output, err := runApp(ctx, "f3", "--verbose", "mirror",
"--from-filesystem-directory", fixtureOptions.(options.URLInterface).GetURL(),
"--from-path", p,
"--to-filesystem-directory", mirrorOptions.(options.URLInterface).GetURL(),
)
log.Trace("======= assert")
assert.NoError(t, err)
require.Contains(t, output, fmt.Sprintf("mirror %s (filesystem", p))
mirrorTree.WalkAndGet(ctx, generic.NewWalkOptions(nil))
found := mirrorTree.Find(generic.NewPathFromString(p))
require.NotEqualValues(t, found, generic.NilNode)
assert.EqualValues(t, p, found.GetCurrentPath().String())
}
func Test_CmdMirrorIntegrationSpecificToPath(t *testing.T) {
ctx := context.Background()
mirrorOptions := tests_forge.GetFactory(forgejo_options.Name)().NewOptions(t)
mirrorTree := generic.GetFactory("f3")(ctx, mirrorOptions)
fixtureOptions := tests_forge.GetFactory(filesystem_options.Name)().NewOptions(t)
fixtureTree := generic.GetFactory("f3")(ctx, fixtureOptions)
log := fixtureTree.GetLogger()
creator := f3_tests.NewCreator(t, "CmdMirrorSpecific", log)
log.Trace("======= build fixture")
var fromPath string
{
fixtureUserID := "userID01"
fixtureProjectID := "projectID01"
userFormat := creator.GenerateUser()
userFormat.SetID(fixtureUserID)
users := fixtureTree.MustFind(generic.NewPathFromString("/forge/users"))
user := users.CreateChild(ctx)
user.FromFormat(userFormat)
user.Upsert(ctx)
require.EqualValues(t, user.GetID(), users.GetIDFromName(ctx, userFormat.UserName))
projectFormat := creator.GenerateProject()
projectFormat.SetID(fixtureProjectID)
projects := user.MustFind(generic.NewPathFromString("projects"))
project := projects.CreateChild(ctx)
project.FromFormat(projectFormat)
project.Upsert(ctx)
require.EqualValues(t, project.GetID(), projects.GetIDFromName(ctx, projectFormat.Name))
fromPath = fmt.Sprintf("/forge/users/%s/projects/%s", userFormat.UserName, projectFormat.Name)
}
log.Trace("======= create mirror")
var toPath string
var projects generic.NodeInterface
{
userFormat := creator.GenerateUser()
users := mirrorTree.MustFind(generic.NewPathFromString("/forge/users"))
user := users.CreateChild(ctx)
user.FromFormat(userFormat)
user.Upsert(ctx)
require.EqualValues(t, user.GetID(), users.GetIDFromName(ctx, userFormat.UserName))
projectFormat := creator.GenerateProject()
projects = user.MustFind(generic.NewPathFromString("projects"))
project := projects.CreateChild(ctx)
project.FromFormat(projectFormat)
project.Upsert(ctx)
require.EqualValues(t, project.GetID(), projects.GetIDFromName(ctx, projectFormat.Name))
toPath = fmt.Sprintf("/forge/users/%s/projects/%s", userFormat.UserName, projectFormat.Name)
}
log.Trace("======= mirror %s", fromPath)
output, err := runApp(ctx, "f3", "--verbose", "mirror",
"--from-type", filesystem_options.Name,
"--from-path", fromPath,
"--from-filesystem-directory", fixtureOptions.(options.URLInterface).GetURL(),
"--to-type", forgejo_options.Name,
"--to-path", toPath,
"--to-forgejo-user", mirrorOptions.(options.AuthInterface).GetUsername(),
"--to-forgejo-password", mirrorOptions.(options.AuthInterface).GetPassword(),
"--to-forgejo-url", mirrorOptions.(options.URLInterface).GetURL(),
)
assert.NoError(t, err)
log.Trace("======= assert")
require.Contains(t, output, fmt.Sprintf("mirror %s", fromPath))
projects.List(ctx)
require.NotEmpty(t, projects.GetChildren())
log.Trace("======= project %s", projects.GetChildren()[0])
}

33
cmd/signal.go Normal file
View file

@ -0,0 +1,33 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"os"
"os/signal"
"syscall"
)
func InstallSignals() (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
signalChannel := make(chan os.Signal, 1)
signal.Notify(
signalChannel,
syscall.SIGINT,
syscall.SIGTERM,
)
select {
case <-signalChannel:
case <-ctx.Done():
}
cancel()
signal.Reset()
}()
return ctx, cancel
}

38
cmd/testhelpers.go Normal file
View file

@ -0,0 +1,38 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"code.forgejo.org/f3/gof3/v3/logger"
// allow forges to register
_ "code.forgejo.org/f3/gof3/v3/forges/filesystem"
_ "code.forgejo.org/f3/gof3/v3/forges/forgejo"
)
func runApp(ctx context.Context, args ...string) (string, error) {
l := logger.NewCaptureLogger()
ctx = logger.ContextSetLogger(ctx, l)
app := NewApp()
app.Writer = l.GetBuffer()
app.ErrWriter = l.GetBuffer()
defer func() {
if r := recover(); r != nil {
fmt.Println(l.String())
panic(r)
}
}()
err := app.Run(ctx, args)
fmt.Println(l.String())
return l.String(), err
}

20
f3/ci.go Normal file
View file

@ -0,0 +1,20 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// Copyright twenty-panda <twenty-panda@posteo.com>
// SPDX-License-Identifier: MIT
package f3
type CI struct {
Common
}
func (o CI) Equal(other CI) bool {
return o.Common.Equal(other.Common)
}
func (o *CI) Clone() Interface {
clone := &CI{}
*clone = *o
return clone
}

35
f3/comment.go Normal file
View file

@ -0,0 +1,35 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import "time"
type Comment struct {
Common
PosterID *Reference `json:"poster_id"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Content string `json:"content"`
}
func (o *Comment) GetReferences() References {
references := o.Common.GetReferences()
if !o.PosterID.IsNil() {
references = append(references, o.PosterID)
}
return references
}
func (o Comment) Equal(other Comment) bool {
return o.Common.Equal(other.Common) &&
nilOrEqual(o.PosterID, other.PosterID) &&
o.Content == other.Content
}
func (o *Comment) Clone() Interface {
clone := &Comment{}
*clone = *o
return clone
}

37
f3/equal.go Normal file
View file

@ -0,0 +1,37 @@
// Copyright limiting-factor <limiting-factor@posteo.com>
// SPDX-License-Identifier: MIT
package f3
import (
"time"
)
type equalConstraint[T any] interface {
Equal(T) bool
*T
}
func nilOrEqual[P any, T equalConstraint[P]](a, b T) bool {
return (a == nil && b == nil) ||
(a != nil && b != nil && a.Equal(*b))
}
func arrayEqual[P any, T equalConstraint[P]](a, b []T) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if !nilOrEqual(a[i], b[i]) {
return false
}
}
return true
}
func nilOrEqualTimeToDate(a, b *time.Time) bool {
return (a == nil && b == nil) ||
(a != nil && b != nil && a.Format(time.DateOnly) == b.Format(time.DateOnly))
}

53
f3/equal_test.go Normal file
View file

@ -0,0 +1,53 @@
// Copyright limiting-factor <limiting-factor@posteo.com>
// SPDX-License-Identifier: MIT
package f3
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type s struct {
v int
}
func (o s) Equal(other s) bool {
return o.v == other.v
}
func TestNilOrEqual(t *testing.T) {
s1 := &s{1}
s2 := &s{1}
s3 := &s{2}
assert.True(t, nilOrEqual[s](nil, nil))
assert.False(t, nilOrEqual(s1, nil))
assert.False(t, nilOrEqual(nil, s2))
assert.True(t, nilOrEqual(s1, s2))
assert.False(t, nilOrEqual(s1, s3))
}
func TestArrayEqual(t *testing.T) {
s1 := []*s{{1}, {2}}
s2 := []*s{{1}, {2}}
s3 := []*s{{1}, {2}, {3}}
assert.True(t, arrayEqual(s1, s2))
assert.False(t, arrayEqual(s1, s3))
}
func TestNilOrEqualTimeToDate(t *testing.T) {
t1, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
require.NoError(t, err)
t2, err := time.Parse(time.RFC3339, "2006-01-02T00:01:00Z")
require.NoError(t, err)
t3, err := time.Parse(time.RFC3339, "2026-01-02T11:01:00Z")
require.NoError(t, err)
assert.True(t, nilOrEqualTimeToDate(nil, nil))
assert.False(t, nilOrEqualTimeToDate(&t1, nil))
assert.False(t, nilOrEqualTimeToDate(nil, &t2))
assert.True(t, nilOrEqualTimeToDate(&t1, &t2))
assert.False(t, nilOrEqualTimeToDate(&t1, &t3))
}

109
f3/file_format.go Normal file
View file

@ -0,0 +1,109 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"encoding/json"
"fmt"
"os"
"github.com/santhosh-tekuri/jsonschema/v6"
)
// Load project data from file, with optional validation
func Load(filename string, data any, validation bool) error {
bs, err := os.ReadFile(filename)
if err != nil {
return err
}
if validation {
err := validate(bs, data)
if err != nil {
return err
}
}
return unmarshal(bs, data)
}
func Store(filename string, data any) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
bs, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
if _, err := f.Write(bs); err != nil {
return err
}
if _, err := f.Write([]byte("\n")); err != nil {
return err
}
return nil
}
func unmarshal(bs []byte, data any) error {
return json.Unmarshal(bs, data)
}
func getSchema(filename string) (*jsonschema.Schema, error) {
c := jsonschema.NewCompiler()
return c.Compile(filename)
}
func validate(bs []byte, datatype any) error {
var v any
err := unmarshal(bs, &v)
if err != nil {
return err
}
var schemaFilename string
switch datatype := datatype.(type) {
case *User:
schemaFilename = "schemas/user.json"
case *Organization:
schemaFilename = "schemas/organization.json"
case *Project:
schemaFilename = "schemas/project.json"
case *Topic:
schemaFilename = "schemas/topic.json"
case *Issue:
schemaFilename = "schemas/issue.json"
case *PullRequest:
schemaFilename = "schemas/pullrequest.json"
case *Label:
schemaFilename = "schemas/label.json"
case *Milestone:
schemaFilename = "schemas/milestone.json"
case *Release:
schemaFilename = "schemas/release.json"
case *ReleaseAsset:
schemaFilename = "schemas/releaseasset.json"
case *Comment:
schemaFilename = "schemas/comment.json"
case *Reaction:
schemaFilename = "schemas/reaction.json"
case *Repository:
schemaFilename = "schemas/repository.json"
case *Review:
schemaFilename = "schemas/review.json"
case *ReviewComment:
schemaFilename = "schemas/reviewcomment.json"
case *CI:
schemaFilename = "schemas/ci.json"
default:
return fmt.Errorf("file_format:validate: %T does not have a schema that could be used for validation", datatype)
}
sch, err := getSchema(schemaFilename)
if err != nil {
return err
}
return sch.Validate(v)
}

166
f3/file_format_test.go Normal file
View file

@ -0,0 +1,166 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"path/filepath"
"strings"
"testing"
"github.com/santhosh-tekuri/jsonschema/v6"
"github.com/stretchr/testify/assert"
)
func TestStoreLoad(t *testing.T) {
tmpDir := t.TempDir()
type S struct {
A int
B string
}
original := S{A: 1, B: "B"}
p := filepath.Join(tmpDir, "s.json")
assert.NoError(t, Store(p, original))
var loaded S
assert.NoError(t, Load(p, &loaded, false))
assert.EqualValues(t, original, loaded)
}
func TestF3_CI(t *testing.T) {
var ci CI
err := Load("file_format_testdata/ci/good.json", &ci, true)
assert.NoError(t, err)
err = Load("file_format_testdata/ci/bad.json", &ci, true)
assert.ErrorContains(t, err, "'/index': value must be one of")
}
func TestF3_User(t *testing.T) {
var user User
err := Load("file_format_testdata/user/good.json", &user, true)
assert.NoError(t, err)
err = Load("file_format_testdata/user/bad.json", &user, true)
assert.ErrorContains(t, err, "missing properties 'index'")
}
func TestF3_Organization(t *testing.T) {
var organization Organization
err := Load("file_format_testdata/organization/good.json", &organization, true)
assert.NoError(t, err)
err = Load("file_format_testdata/organization/bad.json", &organization, true)
assert.ErrorContains(t, err, "missing properties 'index'")
}
func TestF3_Project(t *testing.T) {
var project Project
err := Load("file_format_testdata/project/good.json", &project, true)
assert.NoError(t, err)
err = Load("file_format_testdata/project/bad.json", &project, true)
assert.ErrorContains(t, err, "'/stars': got string, want number")
}
func TestF3_Issue(t *testing.T) {
var issue Issue
err := Load("file_format_testdata/issue/good.json", &issue, true)
assert.NoError(t, err)
err = Load("file_format_testdata/issue/bad.json", &issue, true)
assert.ErrorContains(t, err, "missing property 'index'")
}
func TestF3_PullRequest(t *testing.T) {
var pullRequest PullRequest
err := Load("file_format_testdata/pullrequest/good.json", &pullRequest, true)
assert.NoError(t, err)
err = Load("file_format_testdata/pullrequest/bad.json", &pullRequest, true)
assert.ErrorContains(t, err, "missing properties 'index'")
}
func TestF3_Release(t *testing.T) {
var release Release
err := Load("file_format_testdata/release/good.json", &release, true)
assert.NoError(t, err)
err = Load("file_format_testdata/release/bad.json", &release, true)
assert.ErrorContains(t, err, "missing properties 'index'")
}
func TestF3_ReleaseAsset(t *testing.T) {
var releaseAsset ReleaseAsset
err := Load("file_format_testdata/releaseasset/good.json", &releaseAsset, true)
assert.NoError(t, err)
err = Load("file_format_testdata/releaseasset/bad.json", &releaseAsset, true)
assert.ErrorContains(t, err, "missing properties 'index'")
}
func TestF3_Comment(t *testing.T) {
var comment Comment
err := Load("file_format_testdata/comment/good.json", &comment, true)
assert.NoError(t, err)
err = Load("file_format_testdata/comment/bad.json", &comment, true)
assert.ErrorContains(t, err, "'/created': 'AAAAAAAAA' is not valid date-time")
}
func TestF3_Label(t *testing.T) {
var label Label
err := Load("file_format_testdata/label/good.json", &label, true)
assert.NoError(t, err)
err = Load("file_format_testdata/label/bad.json", &label, true)
assert.ErrorContains(t, err, "'/exclusive': got string, want boolean")
}
func TestF3_Milestone(t *testing.T) {
var milestone Milestone
err := Load("file_format_testdata/milestone/good.json", &milestone, true)
assert.NoError(t, err)
err = Load("file_format_testdata/milestone/bad.json", &milestone, true)
assert.ErrorContains(t, err, "missing properties 'index'")
}
func TestF3_Review(t *testing.T) {
var review Review
err := Load("file_format_testdata/review/good.json", &review, true)
assert.NoError(t, err)
err = Load("file_format_testdata/review/bad.json", &review, true)
assert.ErrorContains(t, err, "missing properties 'index'")
}
func TestF3_Reaction(t *testing.T) {
var reaction Reaction
err := Load("file_format_testdata/reaction/good.json", &reaction, true)
assert.NoError(t, err)
err = Load("file_format_testdata/reaction/bad.json", &reaction, true)
assert.ErrorContains(t, err, "missing properties 'index'")
}
func TestF3_Repository(t *testing.T) {
var repository Repository
err := Load("file_format_testdata/repository/good.json", &repository, true)
assert.NoError(t, err)
err = Load("file_format_testdata/repository/bad.json", &repository, true)
assert.ErrorContains(t, err, "missing property 'name'")
}
func TestF3_ReviewComment(t *testing.T) {
var reviewComment ReviewComment
err := Load("file_format_testdata/reviewcomment/good.json", &reviewComment, true)
assert.NoError(t, err)
err = Load("file_format_testdata/reviewcomment/bad.json", &reviewComment, true)
assert.ErrorContains(t, err, "missing properties 'index'")
}
func TestF3_Topic(t *testing.T) {
var topic Topic
err := Load("file_format_testdata/topic.json", &topic, true)
assert.NoError(t, err)
}
func TestF3_ValidationFail(t *testing.T) {
var issue Issue
err := Load("file_format_testdata/issue/bad.json", &issue, true)
if _, ok := err.(*jsonschema.ValidationError); ok {
errors := strings.Split(err.(*jsonschema.ValidationError).GoString(), "\n")
assert.Contains(t, errors[1], "missing property")
} else {
t.Fatalf("got: type %T with value %s, want: *jsonschema.ValidationError", err, err)
}
}

View file

@ -0,0 +1,3 @@
{
"index": "Unknown"
}

View file

@ -0,0 +1,3 @@
{
"index": "Forgejo Actions"
}

View file

@ -0,0 +1,7 @@
{
"index": "5",
"poster_id": "1",
"created": "AAAAAAAAA",
"updated": "1986-04-12T23:20:50.52Z",
"content": "comment_content_5"
}

View file

@ -0,0 +1,14 @@
{
"index": "5",
"poster_id": "/user/1",
"created": "1985-04-12T23:20:50.52Z",
"updated": "1986-04-12T23:20:50.52Z",
"content": "comment_content_5",
"reactions": [
{
"index": "8",
"user_id": "/user/23",
"content": "laugh"
}
]
}

View file

@ -0,0 +1,20 @@
{
"poster_id": "/forge/users/1",
"title": "title_a",
"content": "content_a",
"milestone": "../../milestones/23",
"state": "closed",
"is_locked": false,
"created": "1985-04-12T23:20:50.52Z",
"updated": "1986-04-12T23:20:50.52Z",
"closed": null,
"due": "1986-04-12",
"labels": [
"../../labels/435"
],
"reactions": null,
"assignees": [
"/forge/users/1",
"/forge/users/2"
]
}

View file

@ -0,0 +1,21 @@
{
"index": "1",
"poster_id": "/forge/users/1",
"title": "title_a",
"content": "content_a",
"milestone": "../../milestones/23",
"state": "closed",
"is_locked": false,
"created": "1985-04-12T23:20:50.52Z",
"updated": "1986-04-12T23:20:50.52Z",
"closed": "1986-04-12T23:20:50.52Z",
"due": "1986-04-12",
"labels": [
"../../labels/435"
],
"reactions": [],
"assignees": [
"/forge/users/1",
"/forge/users/2"
]
}

View file

@ -0,0 +1,7 @@
{
"index": "1",
"name": "label1",
"description": "label1 description",
"color": "ffffff",
"exclusive": "CCCCCCCC"
}

View file

@ -0,0 +1,7 @@
{
"index": "1",
"name": "label1",
"description": "label1 description",
"color": "ffffff",
"exclusive": false
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,10 @@
{
"index": "1",
"title": "title_a",
"description": "description_a",
"deadline": "1988-04-12T23:20:50.52Z",
"created": "1985-04-12T23:20:50.52Z",
"updated": "1986-04-12T23:20:50.52Z",
"closed": "1987-04-12T23:20:50.52Z",
"state": "closed"
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,5 @@
{
"index": "9",
"name": "orgunique",
"full_name": "Org Unique"
}

View file

@ -0,0 +1,28 @@
{
"index": "1",
"name": "projectname",
"is_private": false,
"is_mirror": false,
"description": "project description",
"default_branch": "main",
"repositories": [
{
"name": "vcs",
"vcs": "hg"
},
{
"name": "vcs.wiki"
}
],
"archived": true,
"archived_at": "1987-04-12T23:20:50.52Z",
"created": "1985-04-12T23:20:50.52Z",
"updated": "1986-04-12T23:20:50.52Z",
"url": "https://example.com",
"stars": "BBBBBB",
"has_ci": true,
"has_issues": true,
"has_packages": true,
"has_pull_requests": true,
"has_wiki": true
}

View file

@ -0,0 +1,28 @@
{
"index": "1",
"name": "projectname",
"is_private": false,
"is_mirror": false,
"description": "project description",
"default_branch": "main",
"repositories": [
{
"name": "vcs",
"vcs": "hg"
},
{
"name": "vcs.wiki"
}
],
"archived": true,
"archived_at": "1987-04-12T23:20:50.52Z",
"created": "1985-04-12T23:20:50.52Z",
"updated": "1986-04-12T23:20:50.52Z",
"url": "https://example.com",
"stars": 20,
"has_ci": true,
"has_issues": true,
"has_packages": true,
"has_pull_requests": true,
"has_wiki": true
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,32 @@
{
"index": "1",
"poster_id": "/forgejo/users/1",
"title": "title_a",
"content": "content_a",
"milestone": "../../milestones/5",
"state": "closed",
"is_locked": false,
"created": "1985-04-12T23:20:50.52Z",
"updated": "1986-04-12T23:20:50.52Z",
"closed": "1986-04-12T23:20:50.52Z",
"labels": [
"../../labels/435"
],
"reactions": [],
"assignees": [],
"merged": false,
"merged_time": "1986-04-12T23:20:50.52Z",
"merged_commit_sha": "shashasha",
"head": {
"clone_url": "head_clone_url",
"ref": "head_branch",
"sha": "head_sha",
"repository": "/forge/user/1/projects/2/repositories/vcs"
},
"base": {
"clone_url": "base_clone_url",
"ref": "base_branch",
"sha": "base _sha",
"repository": "/forge/user/3/projects/4/repositories/vcs"
}
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,5 @@
{
"index": "8",
"user_id": "/forge/users/912",
"content": "laugh"
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,11 @@
{
"index": "123",
"tag_name": "v12",
"target_commitish": "stable",
"name": "v12 name",
"body": "v12 body",
"draft": false,
"prerelease": false,
"publisher_id": "/forgejo/user/1",
"created": "1985-04-12T23:20:50.52Z"
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,11 @@
{
"index": "5",
"name": "asset_5",
"content_type": "application/zip",
"size": 50,
"download_count": 10,
"download_url": "http://example.com/something",
"created": "1985-04-12T23:20:50.52Z",
"updated": "1986-04-12T23:20:50.52Z",
"sha256": "4c5b2f412017de78124ae3a063d08e76566eea0cba6deb2533fb176d816a54fc"
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,4 @@
{
"name": "vcs.wiki",
"vcs": "fossil"
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -0,0 +1,12 @@
{
"index": "1",
"reviewer_id": "/forge/user/50",
"official": false,
"commit_id": "shashashasha",
"content": "cover review comment",
"created_at": "1985-04-12T23:20:50.52Z",
"state": "PENDING",
"dissmissed": false,
"stale": false
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,14 @@
{
"index": "100",
"content": "review comment",
"tree_path": "dir/file1.txt",
"diff_hunk": "@@hunkhunk",
"line": 1,
"lines_count": 1,
"commit_id": "shashashasha",
"poster_id": "/forge/users/10",
"reactions": [],
"created_at": "1985-04-12T23:20:50.52Z",
"updated_at": "1985-04-12T23:20:50.52Z",
"resolver": "/forge/users/10"
}

View file

@ -0,0 +1,4 @@
{
"index": "1",
"name": "category1"
}

View file

@ -0,0 +1,2 @@
{
}

View file

@ -0,0 +1,8 @@
{
"index": "7",
"name": "User Name One",
"email": "user1@example.com",
"username": "user1",
"password": "gloxKainlo",
"admin": false
}

20
f3/forge.go Normal file
View file

@ -0,0 +1,20 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
type Forge struct {
Common
URL string `json:"url"`
}
func (o Forge) Equal(other Forge) bool {
return o.Common.Equal(other.Common)
}
func (o *Forge) Clone() Interface {
clone := &Forge{}
*clone = *o
return clone
}

99
f3/formatbase.go Normal file
View file

@ -0,0 +1,99 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"encoding/json"
"strings"
"code.forgejo.org/f3/gof3/v3/util"
)
type Interface interface {
GetID() string
GetName() string
SetID(id string)
IsNil() bool
GetReferences() References
ToReference() *Reference
Clone() Interface
}
type ReferenceInterface interface {
Get() string
Set(reference string)
GetIDAsString() string
GetIDAsInt() int64
}
type Reference struct {
ID string
}
func (r *Reference) Get() string {
return r.ID
}
func (r *Reference) Set(reference string) {
r.ID = reference
}
func (r *Reference) GetIDAsInt() int64 {
return util.ParseInt(r.GetIDAsString())
}
func (r *Reference) GetIDAsString() string {
s := strings.Split(r.ID, "/")
return s[len(s)-1]
}
type References []ReferenceInterface
func (r *Reference) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, &r.ID)
}
func (r Reference) MarshalJSON() ([]byte, error) {
return json.Marshal(r.ID)
}
func (r *Reference) GetID() string { return r.ID }
func (r *Reference) SetID(id string) { r.ID = id }
func (r *Reference) IsNil() bool { return r == nil || r.ID == "0" || r.ID == "" }
func (r Reference) Equal(other Reference) bool { return r.ID == other.ID }
func NewReferences() References {
return make([]ReferenceInterface, 0, 1)
}
func NewReference(id string) *Reference {
r := &Reference{}
r.SetID(id)
return r
}
type Common struct {
Index Reference `json:"index"`
}
func (c *Common) GetID() string { return c.Index.GetID() }
func (c *Common) GetName() string { return c.GetID() }
func (c *Common) SetID(id string) { c.Index.SetID(id) }
func (c *Common) IsNil() bool { return c == nil || c.Index.IsNil() }
func (c *Common) GetReferences() References { return NewReferences() }
func (c *Common) ToReference() *Reference { return &c.Index }
func (c Common) Equal(other Common) bool { return true }
var Nil = &Common{}
func NewCommon(id string) Common {
return Common{Index: *NewReference(id)}
}
func (c *Common) Clone() Interface {
clone := &Common{}
*clone = *c
return clone
}

57
f3/formatbase_test.go Normal file
View file

@ -0,0 +1,57 @@
// Copyright limiting-factor <limiting-factor@posteo.com>
// SPDX-License-Identifier: MIT
package f3
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestF3Reference(t *testing.T) {
ref := "reference"
r := NewReference(ref)
assert.Equal(t, ref, r.GetID())
otherRef := "other"
r.SetID(otherRef)
assert.Equal(t, otherRef, r.GetID())
r.Set(otherRef)
assert.Equal(t, otherRef, r.Get())
assert.True(t, r.Equal(*r))
m, err := r.MarshalJSON()
require.NoError(t, err)
u := NewReference("???")
require.NoError(t, u.UnmarshalJSON(m))
assert.True(t, r.Equal(*u))
assert.False(t, r.IsNil())
r.SetID("")
assert.True(t, r.IsNil())
r.SetID("0")
assert.True(t, r.IsNil())
var nilRef *Reference
assert.True(t, nilRef.IsNil())
}
func TestF3Common(t *testing.T) {
id := "ID"
c := NewCommon(id)
assert.Equal(t, id, c.GetID())
assert.Equal(t, id, c.GetName())
otherID := "otherID"
c.SetID(otherID)
assert.Equal(t, otherID, c.GetID())
assert.False(t, c.IsNil())
c.SetID("")
assert.True(t, c.IsNil())
c.SetID("0")
assert.True(t, c.IsNil())
var nilCommon *Common
assert.True(t, nilCommon.IsNil())
}

58
f3/issue.go Normal file
View file

@ -0,0 +1,58 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import "time"
const (
IssueStateOpen = "open"
IssueStateClosed = "closed"
)
type Issue struct {
Common
PosterID *Reference `json:"poster_id"`
Assignees []*Reference `json:"assignees"`
Labels []*Reference `json:"labels"`
Title string `json:"title"`
Content string `json:"content"`
Milestone *Reference `json:"milestone"`
State string `json:"state"` // open, closed
IsLocked bool `json:"is_locked"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Closed *time.Time `json:"closed"`
}
func (o *Issue) GetReferences() References {
references := o.Common.GetReferences()
for _, assignee := range o.Assignees {
references = append(references, assignee)
}
for _, label := range o.Labels {
references = append(references, label)
}
if !o.Milestone.IsNil() {
references = append(references, o.Milestone)
}
return append(references, o.PosterID)
}
func (o Issue) Equal(other Issue) bool {
return o.Common.Equal(other.Common) &&
nilOrEqual(o.PosterID, other.PosterID) &&
arrayEqual(o.Assignees, other.Assignees) &&
arrayEqual(o.Labels, other.Labels) &&
o.Title == other.Title &&
nilOrEqual(o.Milestone, other.Milestone) &&
o.State == other.State &&
o.IsLocked == other.IsLocked
}
func (o *Issue) Clone() Interface {
clone := &Issue{}
*clone = *o
return clone
}

25
f3/label.go Normal file
View file

@ -0,0 +1,25 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
type Label struct {
Common
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
}
func (o Label) Equal(other Label) bool {
return o.Common.Equal(other.Common) &&
o.Name == other.Name &&
o.Color == other.Color &&
o.Description == other.Description
}
func (o *Label) Clone() Interface {
clone := &Label{}
*clone = *o
return clone
}

37
f3/milestone.go Normal file
View file

@ -0,0 +1,37 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import "time"
const (
MilestoneStateOpen = "open"
MilestoneStateClosed = "closed"
)
type Milestone struct {
Common
Title string `json:"title"`
Description string `json:"description"`
Deadline *time.Time `json:"deadline"`
Created time.Time `json:"created"`
Updated *time.Time `json:"updated"`
Closed *time.Time `json:"closed"`
State string `json:"state"` // open, closed
}
func (o Milestone) Equal(other Milestone) bool {
return o.Common.Equal(other.Common) &&
o.Title == other.Title &&
o.Description == other.Description &&
nilOrEqualTimeToDate(o.Deadline, other.Deadline) &&
o.State == other.State
}
func (o *Milestone) Clone() Interface {
clone := &Milestone{}
*clone = *o
return clone
}

81
f3/new.go Normal file
View file

@ -0,0 +1,81 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"fmt"
)
func New(name string) Interface {
common := NewCommon(name)
switch name {
case "<root>":
return &common
case ResourceAsset:
return &ReleaseAsset{}
case ResourceAssets:
return &common
case ResourceComment:
return &Comment{}
case ResourceComments:
return &common
case ResourceIssue:
return &Issue{}
case ResourceIssues:
return &common
case ResourceLabel:
return &Label{}
case ResourceLabels:
return &common
case ResourceMilestone:
return &Milestone{}
case ResourceMilestones:
return &common
case ResourceOrganization:
return &Organization{}
case ResourceOrganizations:
return &common
case ResourceProject:
return &Project{}
case ResourceProjects:
return &common
case ResourcePullRequest:
return &PullRequest{}
case ResourcePullRequests:
return &common
case ResourceReaction:
return &Reaction{}
case ResourceReactions:
return &common
case ResourceRelease:
return &Release{}
case ResourceReleases:
return &common
case ResourceRepository:
return &Repository{}
case ResourceRepositories:
return &common
case ResourceReview:
return &Review{}
case ResourceReviews:
return &common
case ResourceReviewComment:
return &ReviewComment{}
case ResourceReviewComments:
return &common
case ResourceTopic:
return &Topic{}
case ResourceTopics:
return &common
case ResourceUser:
return &User{}
case ResourceUsers:
return &common
case ResourceForge:
return &Forge{}
default:
panic(fmt.Errorf("unknown %s", name))
}
}

27
f3/organization.go Normal file
View file

@ -0,0 +1,27 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
type Organization struct {
Common
FullName string `json:"full_name"`
Name string `json:"name"`
}
func (o Organization) Equal(other Organization) bool {
return o.Common.Equal(other.Common) &&
o.FullName == other.FullName &&
o.Name == other.Name
}
func (o *Organization) GetName() string {
return o.Name
}
func (o *Organization) Clone() Interface {
clone := &Organization{}
*clone = *o
return clone
}

50
f3/project.go Normal file
View file

@ -0,0 +1,50 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
type Project struct {
Common
Name string `json:"name"`
IsPrivate bool `json:"is_private"`
IsMirror bool `json:"is_mirror"`
Description string `json:"description"`
DefaultBranch string `json:"default_branch"`
Forked *Reference `json:"forked"`
HasWiki bool `json:"has_wiki"`
Topics []*Reference `json:"topics"`
}
func (o Project) Project(other Project) bool {
return o.Common.Equal(other.Common) &&
o.Name == other.Name &&
o.IsPrivate == other.IsPrivate &&
o.IsMirror == other.IsMirror &&
o.Description == other.Description &&
o.DefaultBranch == other.DefaultBranch &&
nilOrEqual(o.Forked, other.Forked) &&
o.HasWiki == other.HasWiki &&
arrayEqual(o.Topics, other.Topics)
}
func (o *Project) GetName() string {
return o.Name
}
func (o *Project) GetReferences() References {
references := o.Common.GetReferences()
if !o.Forked.IsNil() {
references = append(references, o.Forked)
}
for _, topic := range o.Topics {
references = append(references, topic)
}
return references
}
func (o *Project) Clone() Interface {
clone := &Project{}
*clone = *o
return clone
}

72
f3/pullrequest.go Normal file
View file

@ -0,0 +1,72 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
"time"
)
type PullRequestFetchFunc func(ctx context.Context, url, ref string)
const (
PullRequestStateOpen = "open"
PullRequestStateClosed = "closed"
)
type PullRequest struct {
Common
PosterID *Reference `json:"poster_id"`
Title string `json:"title"`
Content string `json:"content"`
Milestone *Reference `json:"milestone"`
State string `json:"state"` // open, closed
IsLocked bool `json:"is_locked"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Closed *time.Time `json:"closed"`
Merged bool `json:"merged"`
MergedTime *time.Time `json:"merged_time"`
MergeCommitSHA string `json:"merged_commit_sha"`
Head PullRequestBranch `json:"head"`
Base PullRequestBranch `json:"base"`
FetchFunc PullRequestFetchFunc `json:"-"`
}
func (o PullRequest) Equal(other PullRequest) bool {
return o.Common.Equal(other.Common) &&
nilOrEqual(o.PosterID, other.PosterID) &&
o.Title == other.Title &&
o.Content == other.Content &&
nilOrEqual(o.Milestone, other.Milestone) &&
o.State == other.State &&
o.IsLocked == other.IsLocked &&
o.Merged == other.Merged &&
nilOrEqual(o.MergedTime, other.MergedTime) &&
o.MergeCommitSHA == other.MergeCommitSHA &&
o.Head.Equal(other.Head) &&
o.Base.Equal(other.Base)
}
func (o *PullRequest) GetReferences() References {
references := o.Common.GetReferences()
if !o.Milestone.IsNil() {
references = append(references, o.Milestone)
}
references = append(references, o.Base.GetReferences()...)
references = append(references, o.Head.GetReferences()...)
return append(references, o.PosterID)
}
func (o *PullRequest) IsForkPullRequest() bool {
return o.Head.Repository != o.Base.Repository
}
func (o *PullRequest) Clone() Interface {
clone := &PullRequest{}
*clone = *o
return clone
}

20
f3/pullrequestbranch.go Normal file
View file

@ -0,0 +1,20 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
type PullRequestBranch struct {
Ref string `json:"ref"`
SHA string `json:"sha"`
Repository *Reference `json:"repository"`
}
func (o PullRequestBranch) Equal(other PullRequestBranch) bool {
return o.Ref == other.Ref &&
o.SHA == other.SHA
}
func (o *PullRequestBranch) GetReferences() References {
return References{o.Repository}
}

31
f3/reaction.go Normal file
View file

@ -0,0 +1,31 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
type Reaction struct {
Common
UserID *Reference `json:"user_id"`
Content string `json:"content"`
}
func (o Reaction) Equal(other Reaction) bool {
return o.Common.Equal(other.Common) &&
nilOrEqual(o.UserID, other.UserID) &&
o.Content == other.Content
}
func (o *Reaction) GetReferences() References {
references := o.Common.GetReferences()
if !o.UserID.IsNil() {
references = append(references, o.UserID)
}
return references
}
func (o *Reaction) Clone() Interface {
clone := &Reaction{}
*clone = *o
return clone
}

48
f3/release.go Normal file
View file

@ -0,0 +1,48 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"time"
)
type Release struct {
Common
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
Body string `json:"body"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
PublisherID *Reference `json:"publisher_id"`
Assets []*ReleaseAsset `json:"assets"`
Created time.Time `json:"created"`
}
func (o Release) Equal(other Release) bool {
return o.Common.Equal(other.Common) &&
o.TagName == other.TagName &&
o.TargetCommitish == other.TargetCommitish &&
o.Name == other.Name &&
o.Body == other.Body &&
o.Draft == other.Draft &&
o.Prerelease == other.Prerelease &&
nilOrEqual(o.PublisherID, other.PublisherID) &&
arrayEqual(o.Assets, other.Assets)
}
func (o *Release) GetReferences() References {
references := o.Common.GetReferences()
if !o.PublisherID.IsNil() {
references = append(references, o.PublisherID)
}
return references
}
func (o *Release) Clone() Interface {
clone := &Release{}
*clone = *o
return clone
}

38
f3/releaseasset.go Normal file
View file

@ -0,0 +1,38 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"io"
"time"
)
type DownloadFuncType func() io.ReadCloser
type ReleaseAsset struct {
Common
Name string `json:"name"`
ContentType string `json:"content_type"`
Size int64 `json:"size"`
DownloadCount int64 `json:"download_count"`
Created time.Time `json:"created"`
SHA256 string `json:"sha256"`
DownloadURL string `json:"download_url"`
DownloadFunc DownloadFuncType `json:"-"`
}
func (o ReleaseAsset) Equal(other ReleaseAsset) bool {
return o.Common.Equal(other.Common) &&
o.Name == other.Name &&
o.ContentType == other.ContentType &&
o.Size == other.Size &&
o.SHA256 == other.SHA256
}
func (o *ReleaseAsset) Clone() Interface {
clone := &ReleaseAsset{}
*clone = *o
return clone
}

45
f3/repository.go Normal file
View file

@ -0,0 +1,45 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"context"
)
const (
RepositoryNameDefault = "vcs"
RepositoryNameWiki = "vcs.wiki"
)
var nameToID = map[string]int64{
RepositoryNameDefault: 1,
RepositoryNameWiki: 2,
}
// var RepositoryNames = []string{RepositoryNameDefault, RepositoryNameWiki}
var RepositoryNames = []string{RepositoryNameDefault}
type Repository struct {
Common
Name string
FetchFunc func(ctx context.Context, destination string, internalRefs []string) `json:"-"`
}
func (o Repository) Equal(other Repository) bool {
return o.Common.Equal(other.Common) &&
o.Name == other.Name
}
func (o *Repository) Clone() Interface {
clone := &Repository{}
*clone = *o
return clone
}
func RepositoryDirname(name string) string {
return "git" + name
}

39
f3/resources.go Normal file
View file

@ -0,0 +1,39 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
const (
ResourceAsset = "asset"
ResourceAssets = "assets"
ResourceComment = "comment"
ResourceComments = "comments"
ResourceIssue = "issue"
ResourceIssues = "issues"
ResourceLabel = "label"
ResourceLabels = "labels"
ResourceMilestone = "milestone"
ResourceMilestones = "milestones"
ResourceOrganization = "organization"
ResourceOrganizations = "organizations"
ResourceProject = "project"
ResourceProjects = "projects"
ResourcePullRequest = "pull_request"
ResourcePullRequests = "pull_requests"
ResourceReaction = "reaction"
ResourceReactions = "reactions"
ResourceRelease = "release"
ResourceReleases = "releases"
ResourceRepository = "repository"
ResourceRepositories = "repositories"
ResourceReview = "review"
ResourceReviews = "reviews"
ResourceReviewComment = "reviewcomment"
ResourceReviewComments = "reviewcomments"
ResourceTopic = "topic"
ResourceTopics = "topics"
ResourceUser = "user"
ResourceUsers = "users"
ResourceForge = "forge"
)

48
f3/review.go Normal file
View file

@ -0,0 +1,48 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import "time"
const (
ReviewStatePending = "PENDING"
ReviewStateApproved = "APPROVED"
ReviewStateChangesRequested = "CHANGES_REQUESTED"
ReviewStateCommented = "COMMENTED"
ReviewStateRequestReview = "REQUEST_REVIEW"
ReviewStateUnknown = ""
)
type Review struct {
Common
ReviewerID *Reference `json:"reviewer_id"`
Official bool `json:"official"`
CommitID string `json:"commit_id"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
State string `json:"state"`
}
func (o Review) Equal(other Review) bool {
return o.Common.Equal(other.Common) &&
nilOrEqual(o.ReviewerID, other.ReviewerID) &&
o.CommitID == other.CommitID &&
o.Content == other.Content &&
o.State == other.State
}
func (o *Review) GetReferences() References {
references := o.Common.GetReferences()
if !o.ReviewerID.IsNil() {
references = append(references, o.ReviewerID)
}
return references
}
func (o *Review) Clone() Interface {
clone := &Review{}
*clone = *o
return clone
}

46
f3/reviewcomment.go Normal file
View file

@ -0,0 +1,46 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
import (
"time"
)
type ReviewComment struct {
Common
Content string `json:"content"`
TreePath string `json:"tree_path"`
DiffHunk string `json:"diff_hunk"`
Line int `json:"line"`
LinesCount int `json:"lines_count"`
CommitID string `json:"commit_id"`
PosterID *Reference `json:"poster_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (o ReviewComment) Equal(other ReviewComment) bool {
return o.Common.Equal(other.Common) &&
o.Content == other.Content &&
o.TreePath == other.TreePath &&
o.Line == other.Line &&
o.LinesCount == other.LinesCount &&
o.CommitID == other.CommitID &&
nilOrEqual(o.PosterID, other.PosterID)
}
func (o *ReviewComment) GetReferences() References {
references := o.Common.GetReferences()
if !o.PosterID.IsNil() {
references = append(references, o.PosterID)
}
return references
}
func (o *ReviewComment) Clone() Interface {
clone := &ReviewComment{}
*clone = *o
return clone
}

42
f3/schemas/ci.json Normal file
View file

@ -0,0 +1,42 @@
{
"title": "CI",
"description": "The Continuous Integration supported by the project. The configuration files are found in the repository itself, under a path that depends on the CI system.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "The type of continuous integration",
"enum": [
"Apache Gump",
"Azure DevOps Server",
"Bamboo",
"Buddy",
"Buildbot",
"BuildMaster",
"CircleCI",
"Drone",
"Forgejo Actions",
"Gitea Actions",
"GitHub Actions",
"GitLab",
"GoCD",
"Jenkins",
"OpenMake",
"Semaphore",
"TeamCity",
"tekton",
"Travis CI",
"Vexor",
"Woodpecker CI"
]
}
},
"required": [
"index"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/ci.json",
"$$target": "ci.json"
}

49
f3/schemas/comment.json Normal file
View file

@ -0,0 +1,49 @@
{
"title": "Comment",
"description": "Comment associated to a commentable object (i.e. issue, review, etc.). Forge users add a comment to an object to create a non-threaded conversation.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the comment.",
"type": "string"
},
"poster_id": {
"description": "Unique identifier of the comment author.",
"type": "string"
},
"created": {
"description": "Creation time.",
"type": "string",
"format": "date-time"
},
"updated": {
"description": "Last update time.",
"type": "string",
"format": "date-time"
},
"content": {
"description": "Markdown content of the comment.",
"type": "string"
},
"reactions": {
"description": "List of reactions.",
"type": "array",
"items": {
"$ref": "reaction.json"
}
}
},
"required": [
"index",
"poster_id",
"created",
"updated",
"content"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/comment.json",
"$$target": "comment.json"
}

22
f3/schemas/index.rst Normal file
View file

@ -0,0 +1,22 @@
.. toctree::
:maxdepth: 2
.. jsonschema:: ci.json
.. jsonschema:: comment.json
.. jsonschema:: issue.json
.. jsonschema:: label.json
.. jsonschema:: milestone.json
.. jsonschema:: object.json
.. jsonschema:: organization.json
.. jsonschema:: project.json
.. jsonschema:: pullrequest.json
.. jsonschema:: pullrequestbranch.json
.. jsonschema:: reaction.json
.. jsonschema:: release.json
.. jsonschema:: releaseasset.json
.. jsonschema:: repository.json
.. jsonschema:: review.json
.. jsonschema:: reviewcomment.json
.. jsonschema:: topic.json
.. jsonschema:: user.json

96
f3/schemas/issue.json Normal file
View file

@ -0,0 +1,96 @@
{
"title": "Issue",
"description": "An issue within an issue tracking system, relative to a project.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the issue.",
"type": "string"
},
"poster_id": {
"description": "Unique identifier of the user who authored the issue.",
"type": "string"
},
"title": {
"description": "Short description displayed as the title.",
"type": "string"
},
"content": {
"description": "Description of the issue.",
"type": "string"
},
"milestone": {
"description": "Unique identifier of the milestone.",
"type": "string"
},
"state": {
"description": "An issue is 'closed' when it is resolved, 'open' otherwise. Issues that do not relate to a topic that needs to be resolved, such as an open conversation, may never be closed.",
"enum": [
"closed",
"open"
]
},
"is_locked": {
"description": "A locked issue can only be modified by privileged users. It is commonly used for moderation purposes when comments associated with the issue are too heated.",
"type": "boolean"
},
"created": {
"description": "Creation time.",
"type": "string",
"format": "date-time"
},
"updated": {
"description": "Last update time.",
"type": "string",
"format": "date-time"
},
"closed": {
"description": "The last time 'state' changed to 'closed'.",
"type": "string",
"format": "date-time"
},
"due": {
"description": "Due date.",
"type": "string",
"format": "date"
},
"labels": {
"description": "List of label unique identifiers.",
"type": "array",
"items": {
"type": "string"
}
},
"reactions": {
"description": "List of reactions.",
"type": "array",
"items": {
"$ref": "reaction.json"
}
},
"assignees": {
"description": "List of assignees.",
"type": "array",
"items": {
"description": "Name of a user assigned to the issue.",
"type": "string"
}
}
},
"required": [
"index",
"poster_id",
"title",
"content",
"state",
"is_locked",
"created",
"updated"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/issue.json",
"$$target": "issue.json"
}

37
f3/schemas/label.json Normal file
View file

@ -0,0 +1,37 @@
{
"title": "Label",
"description": "Label associated to an issue.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier.",
"type": "string"
},
"name": {
"description": "Name of the label, unique within the repository.",
"type": "string"
},
"color": {
"description": "Color code of the label in RGB notation 'xxx' or 'xxxxxx'.",
"type": "string"
},
"description": {
"description": "Long description.",
"type": "string"
},
"exclusive": {
"description": "There can only be one label with the prefix found before the first slash (/).",
"type": "boolean"
}
},
"required": [
"index",
"name"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/label.json",
"$$target": "label.json"
}

62
f3/schemas/milestone.json Normal file
View file

@ -0,0 +1,62 @@
{
"title": "Milestone",
"description": "Milestone relative to a project, for the purpose of grouping objects due to a given date (issues, etc.).",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier.",
"type": "string"
},
"title": {
"description": "Short description.",
"type": "string"
},
"description": {
"description": "Long description.",
"type": "string"
},
"deadline": {
"description": "Deadline after which the milestone is overdue.",
"type": "string",
"format": "date-time"
},
"created": {
"description": "Creation time.",
"type": "string",
"format": "date-time"
},
"updated": {
"description": "Last update time.",
"type": "string",
"format": "date-time"
},
"closed": {
"description": "The last time 'state' changed to 'closed'.",
"type": "string",
"format": "date-time"
},
"state": {
"description": "A 'closed' milestone will not see any activity in the future, otherwise it is 'open'.",
"enum": [
"closed",
"open"
]
}
},
"required": [
"index",
"title",
"description",
"deadline",
"created",
"updated",
"closed",
"state"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/milestone.json",
"$$target": "milestone.json"
}

35
f3/schemas/object.json Normal file
View file

@ -0,0 +1,35 @@
{
"title": "Object",
"description": "Meta information and reference to an opaque content such as an image. The unique identifier is the SHA-256 of the content of the object.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier.",
"type": "string"
},
"mime": {
"description": "Mime type of the object.",
"type": "string"
},
"name": {
"description": "Human readable file name.",
"type": "string"
},
"description": {
"description": "Description.",
"type": "string"
}
},
"required": [
"index",
"mime",
"name",
"description"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/object.json",
"$$target": "object.json"
}

View file

@ -0,0 +1,29 @@
{
"title": "Organization",
"description": "A forge organization.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the organization.",
"type": "string"
},
"name": {
"description": "Unique name of the organization.",
"type": "string"
},
"full_name": {
"description": "Readable name of the organization.",
"type": "string"
}
},
"required": [
"index",
"name"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/organization.json",
"$$target": "organization.json"
}

117
f3/schemas/project.json Normal file
View file

@ -0,0 +1,117 @@
{
"title": "Project",
"description": "A software project contains a code repository, an issue tracker, etc.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the project.",
"type": "string"
},
"name": {
"description": "Name of the project, relative to the owner.",
"type": "string"
},
"is_private": {
"description": "True if the visibility of the project is not public.",
"type": "boolean"
},
"is_mirror": {
"description": "True if it is a mirror of a project residing on another forge.",
"type": "boolean"
},
"description": {
"description": "Long description of the project.",
"type": "string"
},
"default_branch": {
"description": "Name of the default branch in the code repository.",
"type": "string"
},
"repositories": {
"type": "array",
"items": {
"$ref": "repository.json"
}
},
"forked": {
"description": "Unique identifier of the project from which this one was forked.",
"type": "string"
},
"ci": {
"type": "array",
"items": {
"$ref": "ci.json"
}
},
"archived": {
"description": "True if archived and read only.",
"type": "boolean"
},
"archived_at": {
"description": "Time of archival.",
"type": "string",
"format": "date-time"
},
"created": {
"description": "Creation time.",
"type": "string",
"format": "date-time"
},
"updated": {
"description": "Last update time.",
"type": "string",
"format": "date-time"
},
"url": {
"description": "URL associated with the project, for instance the project home page.",
"type": "string"
},
"stars": {
"description": "Number of stars.",
"type": "number"
},
"has_ci": {
"description": "True if CI is enabled.",
"type": "boolean"
},
"has_issues": {
"description": "True if the issue tracker is enabled.",
"type": "boolean"
},
"has_packages": {
"description": "True if the software packages are enabled.",
"type": "boolean"
},
"has_kanban": {
"description": "True if the kanban is enabled.",
"type": "boolean"
},
"has_pull_requests": {
"description": "True if pull requests are enabled.",
"type": "boolean"
},
"has_releases": {
"description": "True if releases are enabled.",
"type": "boolean"
},
"has_wiki": {
"description": "True if the wiki is enabled.",
"type": "boolean"
}
},
"required": [
"index",
"name",
"is_private",
"is_mirror",
"description",
"default_branch",
"repositories"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/project.json",
"$$target": "project.json"
}

134
f3/schemas/pullrequest.json Normal file
View file

@ -0,0 +1,134 @@
{
"title": "Pull request",
"description": "A pull requests to merge a commit from a 'head' that may be another branch in the same repository or a branch in a forked repository.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the pull request.",
"type": "string"
},
"poster_id": {
"description": "Unique identifier of the user who authored the pull request.",
"type": "string"
},
"title": {
"description": "Short description displayed as the title.",
"type": "string"
},
"content": {
"description": "Long description.",
"type": "string"
},
"milestone": {
"description": "Unique identifier of the milestone.",
"type": "string"
},
"state": {
"description": "A 'closed' pull request will not see any activity in the future, otherwise it is 'open'.",
"enum": [
"closed",
"open"
]
},
"is_locked": {
"description": "A locked pull request can only be modified by privileged users.",
"type": "boolean"
},
"created": {
"description": "Creation time.",
"type": "string",
"format": "date-time"
},
"updated": {
"description": "Last update time.",
"type": "string",
"format": "date-time"
},
"closed": {
"description": "The last time 'state' changed to 'closed'.",
"type": "string",
"format": "date-time"
},
"labels": {
"description": "List of labels unique identifiers.",
"type": "array",
"items": {
"type": "string"
}
},
"reactions": {
"description": "List of reactions.",
"type": "array",
"items": {
"$ref": "reaction.json"
}
},
"assignees": {
"description": "List of assignees.",
"type": "array",
"items": {
"description": "Name of a user assigned to the issue.",
"type": "string"
}
},
"merged": {
"description": "True if the pull request was merged.",
"type": "boolean"
},
"merged_time": {
"description": "The time when the pull request was merged.",
"type": "string",
"format": "date-time"
},
"merged_commit_sha": {
"description": "The SHA of the merge commit.",
"type": "string"
},
"head": {
"description": "The changes proposed in the pull request.",
"type": "object",
"items": {
"$ref": "pullrequestbranch.json"
}
},
"base": {
"description": "The branch where the pull request changes in the head are to be merged.",
"type": "object",
"items": {
"$ref": "pullrequestbranch.json"
}
},
"merged_by": {
"description": "Unique identifier of the user who merged the pull request.",
"type": "string"
},
"due": {
"description": "Due date.",
"type": "string",
"format": "date"
},
"allow_edit": {
"description": "True when the author of the pull request allows pushing new commits to its branch.",
"type": "boolean"
}
},
"required": [
"index",
"poster_id",
"title",
"content",
"state",
"is_locked",
"created",
"updated",
"merged",
"head",
"base"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/pullrequest.json",
"$$target": "pullrequest.json"
}

View file

@ -0,0 +1,30 @@
{
"title": "Pull request reference to a commit",
"description": "The location of a commit and the repository where it can be found.",
"type": "object",
"additionalProperties": false,
"properties": {
"ref": {
"description": "Repository reference of the commit (branch, tag, etc.).",
"type": "string"
},
"sha": {
"description": "SHA of the commit.",
"type": "string"
},
"repository": {
"description": "Unique identifier of the repository.",
"type": "string"
}
},
"required": [
"ref",
"sha",
"repository"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/pullrequestbranch.json",
"$$target": "pullrequestbranch.json"
}

30
f3/schemas/reaction.json Normal file
View file

@ -0,0 +1,30 @@
{
"title": "Reaction",
"description": "Reaction associated to a comment that is displayed as a single emoji.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the reaction.",
"type": "string"
},
"user_id": {
"description": "Unique identifier of the user who authored the reaction.",
"type": "string"
},
"content": {
"description": "Representation of the reaction. The rendering of the reaction depends on the forge displaying it.",
"type": "string"
}
},
"required": [
"index",
"user_id",
"content"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/reaction.json",
"$$target": "reaction.json"
}

60
f3/schemas/release.json Normal file
View file

@ -0,0 +1,60 @@
{
"title": "Release",
"description": "A release is associated with a tag in a repository and contains of a set of files (release assets).",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the release.",
"type": "string"
},
"tag_name": {
"description": "Tag name of the release.",
"type": "string"
},
"target_commitish": {
"description": "Specifies the commitish value that determines where the tag is created from. Can be any branch or commit SHA. Unused if the tag already exists.",
"type": "string"
},
"name": {
"description": "The name of the release.",
"type": "string"
},
"body": {
"description": "Text describing the contents of the release, usually the release notes.",
"type": "string"
},
"draft": {
"description": "True if the release is a draft.",
"type": "boolean"
},
"prerelease": {
"description": "True if the release is a pre-release.",
"type": "boolean"
},
"publisher_id": {
"description": "Unique identifier of the user who authored the release.",
"type": "string"
},
"created": {
"description": "Creation time.",
"type": "string",
"format": "date-time"
}
},
"required": [
"index",
"tag_name",
"name",
"body",
"draft",
"prerelease",
"publisher_id",
"created"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/release.json",
"$$target": "release.json"
}

View file

@ -0,0 +1,61 @@
{
"title": "Release asset",
"description": "A file associated with a release. The content of the file is opaque.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the release asset.",
"type": "string"
},
"name": {
"description": "The name of the release asset.",
"type": "string"
},
"content_type": {
"description": "The content type of the release asset (application/zip, etc.).",
"type": "string"
},
"size": {
"description": "Size in bytes of the release asset.",
"type": "number"
},
"download_count": {
"description": "The number of times the release asset was downloaded.",
"type": "number"
},
"download_url": {
"description": "The URL from which the release asset can be downloaded.",
"type": "string"
},
"created": {
"description": "Creation time.",
"type": "string",
"format": "date-time"
},
"updated": {
"description": "Last update time.",
"type": "string",
"format": "date-time"
},
"sha256": {
"description": "SHA256 of the cnotent of the asset.",
"type": "string"
}
},
"required": [
"index",
"name",
"content_type",
"size",
"download_count",
"created",
"updated",
"sha256"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/releaseasset.json",
"$$target": "releaseasset.json"
}

View file

@ -0,0 +1,31 @@
{
"title": "Repository",
"description": "VCS repository relative to a project. The actual content of the repository is found in the sibling 'repository' directory.",
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"description": "Unique name of the repository relative to the project (e.g. vcs or vcs.wiki).",
"type": "string"
},
"vcs": {
"description": "The type of the repository, defaults to 'git'",
"enum": [
"git",
"hg",
"bazaar",
"darcs",
"fossil",
"svn"
]
}
},
"required": [
"name"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/repository.json",
"$$target": "repository.json"
}

63
f3/schemas/review.json Normal file
View file

@ -0,0 +1,63 @@
{
"title": "Review",
"description": "A set of review comments on a pull/merge request.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the review.",
"type": "string"
},
"reviewer_id": {
"description": "Unique identifier of review author.",
"type": "string"
},
"official": {
"description": "True if a positive review counts to reach the required threshold.",
"type": "boolean"
},
"commit_id": {
"description": "SHA of the commit targeted by the review.",
"type": "string"
},
"content": {
"description": "Cover message of the review.",
"type": "string"
},
"created_at": {
"description": "Creation time.",
"type": "string",
"format": "date-time"
},
"state": {
"description": "State of the review.",
"enum": [
"PENDING",
"APPROVED",
"CHANGES_REQUESTED",
"COMMENTED"
]
},
"dissmissed": {
"description": "True if the review was dismissed.",
"type": "boolean"
},
"stale": {
"description": "True if the review is stale because the pull request content changed after it was published.",
"type": "boolean"
}
},
"required": [
"index",
"reviewer_id",
"commit_id",
"content",
"created_at",
"state"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/review.json",
"$$target": "review.json"
}

View file

@ -0,0 +1,77 @@
{
"title": "Review comment",
"description": "A comment in the context of a review.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the review comment.",
"type": "string"
},
"content": {
"description": "The text of the review comment.",
"type": "string"
},
"tree_path": {
"description": "The relative path to the file commented on.",
"type": "string"
},
"diff_hunk": {
"description": "The hunk commented on.",
"type": "string"
},
"line": {
"description": "The line number of the comment relative to the tree_path.",
"type": "number"
},
"lines_count": {
"description": "The range of lines that are commented on. If absent it defaults to one and is a single line comment. If specified it must be a positive number. If line is N and lines_count is C, the range of lines commented on is ]N-C,N]. In other words, the range starts lines_count before line, which is the last of the range",
"type": "number"
},
"commit_id": {
"description": "The SHA of the tree_path commented on.",
"type": "string"
},
"poster_id": {
"description": "Unique identifier of the user who authored the comment.",
"type": "string"
},
"reactions": {
"description": "List of reactions.",
"type": "array",
"items": {
"$ref": "reaction.json"
}
},
"created_at": {
"description": "Creation time.",
"type": "string",
"format": "date-time"
},
"updated_at": {
"description": "Last update time.",
"type": "string",
"format": "date-time"
},
"resolver": {
"description": "Unique identifier of the user who resolved the comment.",
"type": "string"
}
},
"required": [
"index",
"content",
"tree_path",
"diff_hunk",
"line",
"commit_id",
"poster_id",
"created_at",
"updated_at"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/reviewcomment.json",
"$$target": "reviewcomment.json"
}

25
f3/schemas/topic.json Normal file
View file

@ -0,0 +1,25 @@
{
"title": "Topic",
"description": "A category associated with a project. There can be multiple topics/categories for a given project.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier.",
"type": "string"
},
"name": {
"description": "The name of the category the project belongs to.",
"type": "string"
}
},
"required": [
"index",
"name"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/topic.json",
"$$target": "topic.json"
}

42
f3/schemas/user.json Normal file
View file

@ -0,0 +1,42 @@
{
"title": "User",
"description": "A forge user.",
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"description": "Unique identifier of the user.",
"type": "string"
},
"name": {
"description": "User readable name of the user.",
"type": "string"
},
"email": {
"description": "Mail of the user.",
"type": "string"
},
"username": {
"description": "Unique name of the user.",
"type": "string"
},
"password": {
"description": "Password of the user.",
"type": "string"
},
"admin": {
"description": "True if the user has administrative permissions on the forge.",
"type": "boolean"
}
},
"required": [
"index",
"name",
"username"
],
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://code.forgejo.org/f3/f3-schemas/src/branch/main/user.json",
"$$target": "user.json"
}

21
f3/topic.go Normal file
View file

@ -0,0 +1,21 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
type Topic struct {
Common
Name string `json:"name"`
}
func (o Topic) Equal(other Topic) bool {
return o.Common.Equal(other.Common) &&
o.Name == other.Name
}
func (o *Topic) Clone() Interface {
clone := &Topic{}
*clone = *o
return clone
}

32
f3/user.go Normal file
View file

@ -0,0 +1,32 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package f3
type User struct {
Common
Name string `json:"name"`
Email string `json:"email"`
UserName string `json:"username"`
Password string `json:"password"`
IsAdmin bool `json:"admin"`
}
func (o User) Equal(other User) bool {
return o.Common.Equal(other.Common) &&
o.Name == other.Name &&
o.Email == other.Email &&
o.UserName == other.UserName &&
o.IsAdmin == other.IsAdmin
}
func (o *User) GetName() string {
return o.UserName
}
func (o *User) Clone() Interface {
clone := &User{}
*clone = *o
return clone
}

View file

@ -0,0 +1,85 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package filesystem
import (
"context"
"io"
"os"
"path/filepath"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/tree/generic"
)
type assetDriver struct {
nodeDriver
}
func newAssetDriver(content f3.Interface) generic.NodeDriverInterface {
n := newNodeDriver(content).(*nodeDriver)
a := &assetDriver{
nodeDriver: *n,
}
return a
}
func (o *assetDriver) getPath() string {
f := o.nodeDriver.content.(*f3.ReleaseAsset)
options := o.GetTreeDriver().(*treeDriver).options
return filepath.Join(options.Directory, "objects", f.SHA256[0:2], f.SHA256[2:4], f.SHA256)
}
func (o *assetDriver) save(ctx context.Context) {
assetFormat := o.nodeDriver.content.(*f3.ReleaseAsset)
objectsHelper := o.getF3Tree().GetObjectsHelper()
sha, tmpPath := objectsHelper.Save(assetFormat.DownloadFunc())
assetFormat.SHA256 = sha
path := o.getPath()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
panic(err)
}
if err := os.Rename(tmpPath, path); err != nil {
panic(err)
}
o.GetNode().Trace("%s %s", assetFormat.SHA256, path)
}
func (o *assetDriver) setDownloadFunc() {
f := o.nodeDriver.content.(*f3.ReleaseAsset)
f.DownloadFunc = func() io.ReadCloser {
f, err := os.Open(o.getPath())
if err != nil {
panic(err)
}
return f
}
}
func (o *assetDriver) Put(ctx context.Context) id.NodeID {
return o.upsert(ctx)
}
func (o *assetDriver) Patch(ctx context.Context) {
o.upsert(ctx)
}
func (o *assetDriver) upsert(ctx context.Context) id.NodeID {
assetFormat := o.nodeDriver.content.(*f3.ReleaseAsset)
if assetFormat.SHA256 != "" {
o.save(ctx)
}
o.setDownloadFunc()
return o.nodeDriver.upsert(ctx)
}
func (o *assetDriver) Get(ctx context.Context) bool {
if o.nodeDriver.Get(ctx) {
o.setDownloadFunc()
return true
}
return false
}

37
forges/filesystem/json.go Normal file
View file

@ -0,0 +1,37 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package filesystem
import (
"encoding/json"
"fmt"
"os"
)
func loadJSON(filename string, data any) {
bs, err := os.ReadFile(filename)
if err != nil {
panic(fmt.Errorf("ReadFile %w", err))
}
if err := json.Unmarshal(bs, data); err != nil {
panic(fmt.Errorf("Unmarshal %s %s %w", filename, string(bs), err))
}
}
func saveJSON(filename string, data any) {
f, err := os.Create(filename)
if err != nil {
panic(fmt.Errorf("Create %w", err))
}
defer f.Close()
bs, err := json.MarshalIndent(data, "", " ")
if err != nil {
panic(fmt.Errorf("MarshalIndent %w", err))
}
if _, err := f.Write(bs); err != nil {
panic(fmt.Errorf("Write %w", err))
}
}

16
forges/filesystem/main.go Normal file
View file

@ -0,0 +1,16 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package filesystem
import (
filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
"code.forgejo.org/f3/gof3/v3/options"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
)
func init() {
f3_tree.RegisterForgeFactory(filesystem_options.Name, newTreeDriver)
options.RegisterFactory(filesystem_options.Name, newOptions)
}

249
forges/filesystem/node.go Normal file
View file

@ -0,0 +1,249 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package filesystem
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"code.forgejo.org/f3/gof3/v3/f3"
"code.forgejo.org/f3/gof3/v3/id"
"code.forgejo.org/f3/gof3/v3/kind"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
"code.forgejo.org/f3/gof3/v3/util"
)
type nodeDriver struct {
generic.NullDriver
content f3.Interface
}
func newNodeDriver(content f3.Interface) generic.NodeDriverInterface {
return &nodeDriver{
content: content.Clone(),
}
}
func (o *nodeDriver) SetNative(any) {}
func (o *nodeDriver) GetNativeID() string {
return o.GetNode().GetID().String()
}
func (o *nodeDriver) getBasePath() string {
options := o.GetTreeDriver().(*treeDriver).options
return options.Directory + o.GetNode().GetCurrentPath().String()
}
func (o *nodeDriver) getTree() generic.TreeInterface {
return o.GetNode().GetTree()
}
func (o *nodeDriver) getF3Tree() f3_tree.TreeInterface {
return o.getTree().(f3_tree.TreeInterface)
}
func (o *nodeDriver) isContainer() bool {
return o.getF3Tree().IsContainer(o.getKind())
}
func (o *nodeDriver) getKind() kind.Kind {
return o.GetNode().GetKind()
}
func (o *nodeDriver) IsNull() bool { return false }
func (o *nodeDriver) GetIDFromName(ctx context.Context, name string) id.NodeID {
switch o.getKind() {
case kind.KindRoot, f3_tree.KindProjects, f3_tree.KindUsers, f3_tree.KindOrganizations, f3_tree.KindRepositories:
default:
panic(fmt.Errorf("unxpected kind %s", o.getKind()))
}
for _, child := range o.GetNode().List(ctx) {
child.Get(ctx)
if child.ToFormat().GetName() == name {
return child.GetID()
}
}
return id.NilID
}
func (o *nodeDriver) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
node := o.GetNode()
node.Trace("%s '%s'", o.getKind(), node.GetID())
children := generic.NewChildrenSlice(0)
if o.getKind() == kind.KindRoot || page > 1 {
return children
}
basePath := o.getBasePath()
if !util.FileExists(basePath) {
return children
}
f3Tree := o.getF3Tree()
if !f3Tree.IsContainer(o.getKind()) {
return children
}
node.Trace("%d '%s'", page, basePath)
dirEntries, err := os.ReadDir(basePath)
if err != nil {
panic(fmt.Errorf("ReadDir %s %w", basePath, err))
}
for _, dirEntry := range dirEntries {
if !strings.HasSuffix(dirEntry.Name(), ".json") {
continue
}
node.Trace(" add %s", dirEntry.Name())
child := node.CreateChild(ctx)
i := strings.TrimSuffix(dirEntry.Name(), ".json")
childID := id.NewNodeID(i)
child.SetID(childID)
children = append(children, child)
}
return children
}
func (o *nodeDriver) Equals(context.Context, generic.NodeInterface) bool { panic("") }
func (o *nodeDriver) LookupMappedID(id id.NodeID) id.NodeID {
o.GetNode().Trace("%s", id)
return id
}
func (o *nodeDriver) hasJSON() bool {
kind := o.getKind()
if kind == f3_tree.KindForge {
return true
}
return !o.isContainer()
}
func (o *nodeDriver) Get(context.Context) bool {
o.GetNode().Trace("'%s' '%s'", o.getKind(), o.GetNode().GetID())
if !o.hasJSON() || o.GetNode().GetID() == id.NilID {
return true
}
filename := o.getBasePath() + ".json"
o.GetNode().Trace("'%s'", filename)
if !util.FileExists(filename) {
return false
}
f := o.NewFormat()
loadJSON(filename, f)
o.content = f
o.GetNode().Trace("%s %s id=%s", o.getKind(), filename, o.content.GetID())
idFilename := o.getBasePath() + ".id"
if !util.FileExists(idFilename) {
return true
}
mappedID, err := os.ReadFile(idFilename)
if err != nil {
panic(fmt.Errorf("Get %s %w", idFilename, err))
}
o.NullDriver.SetMappedID(id.NewNodeID(string(mappedID)))
return true
}
func (o *nodeDriver) SetMappedID(mapped id.NodeID) {
o.NullDriver.SetMappedID(mapped)
o.saveMappedID()
}
func (o *nodeDriver) saveMappedID() {
k := o.getKind()
switch k {
case kind.KindRoot, f3_tree.KindForge:
return
}
if o.isContainer() {
return
}
mappedID := o.GetMappedID()
if mappedID == id.NilID {
return
}
basePath := o.getBasePath()
idFilename := basePath + ".id"
o.Trace("%s", idFilename)
if err := os.WriteFile(idFilename, []byte(o.GetMappedID().String()), 0o644); err != nil {
panic(fmt.Errorf("%s %w", idFilename, err))
}
}
func (o *nodeDriver) Put(ctx context.Context) id.NodeID {
return o.upsert(ctx)
}
func (o *nodeDriver) Patch(ctx context.Context) {
o.upsert(ctx)
}
func (o *nodeDriver) upsert(context.Context) id.NodeID {
i := o.GetNode().GetID()
o.GetNode().Trace("%s %s", o.getKind(), i)
o.content.SetID(i.String())
if !o.hasJSON() || i == id.NilID {
return i
}
basePath := o.getBasePath()
dirname := filepath.Dir(basePath)
if !util.FileExists(dirname) {
if err := os.MkdirAll(dirname, 0o777); err != nil {
panic(fmt.Errorf("MakeDirAll %s %w", dirname, err))
}
}
saveJSON(basePath+".json", o.content)
o.saveMappedID()
return i
}
func (o *nodeDriver) Delete(context.Context) {
if o.isContainer() {
return
}
basePath := o.getBasePath()
if util.FileExists(basePath) {
if err := os.RemoveAll(basePath); err != nil {
panic(fmt.Errorf("RemoveAll %s %w", basePath, err))
}
}
for _, ext := range []string{".id", ".json"} {
jsonFilename := basePath + ext
if util.FileExists(jsonFilename) {
if err := os.Remove(jsonFilename); err != nil {
panic(fmt.Errorf("RemoveAll %s %w", basePath, err))
}
}
}
o.content = o.NewFormat()
}
func (o *nodeDriver) NewFormat() f3.Interface {
return o.getTree().(f3_tree.TreeInterface).NewFormat(o.getKind())
}
func (o *nodeDriver) FromFormat(content f3.Interface) {
o.content = content
}
func (o *nodeDriver) ToFormat() f3.Interface {
return o.content.Clone()
}
func (o *nodeDriver) String() string {
return o.content.GetID()
}

View file

@ -0,0 +1,16 @@
// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT
package filesystem
import (
filesystem_options "code.forgejo.org/f3/gof3/v3/forges/filesystem/options"
"code.forgejo.org/f3/gof3/v3/options"
)
func newOptions() options.Interface {
o := &filesystem_options.Options{}
o.SetName(filesystem_options.Name)
return o
}

Some files were not shown because too many files have changed in this diff Show more