# forge
An on-chain software forge, browsable in gnoweb and writable only through signed
transactions. The domain model lives in
[`gno.land/p/moul/forge/v0`](../../../../p/moul/forge/v0); this realm is the
wiring, the routes and the events.
The code itself is not here. Git objects stay in git, behind whatever mirror a
repo declares. What the chain keeps is the part a forge is trusted for: which
object a branch points at, in what order, on whose authority, and what was
reviewed before it moved.
## Routes
| path | page |
| --- | --- |
| `/` | every repo, with counts |
| `/<ns>/<name>` | repo overview: refs, recent log, open changes and issues |
| `/<ns>/<name>/log` | the full reference log, paginated with `?page=N` |
| `/<ns>/<name>/issues` | issues, `?page=N` |
| `/<ns>/<name>/issues/<id>` | one issue and its replies |
| `/<ns>/<name>/changes` | change requests, `?page=N` |
| `/<ns>/<name>/changes/<id>` | one change, its reviews and its replies |
| `/help` | what the realm is and how to call it |
Every action link on those pages is a `txlink` into the matching function, so
the whole forge is usable from gnoweb without a client.
## Writing
```sh
gnokey maketx call -pkgpath gno.land/r/moul/forge/v0 \
-func CreateRepo -args "moul/forge" -args "an on-chain forge" -args "" \
-gas-fee 1000000ugnot -gas-wanted 3000000 \
-broadcast -chainid <chain> -remote <rpc> <key>
```
```sh
# move a branch: the fourth argument is the tip you expect to replace
gnokey maketx call -pkgpath gno.land/r/moul/forge/v0 \
-func SetRef -args "moul/forge" -args "refs/heads/main" \
-args "<expected-oid>" -args "<new-oid>" -args "ship it" \
-gas-fee 1000000ugnot -gas-wanted 3000000 \
-broadcast -chainid <chain> -remote <rpc> <key>
```
A stale expectation aborts the transaction instead of overwriting the branch. A
move with no expectation is `ForceSetRef`, needs `maintainer`, and is recorded
as a force forever.
Arguments that would be slices are comma-separated strings (`SetMirrors`,
`OpenIssue` labels): a transaction can only carry strings, so a `[]string`
parameter would not be callable from gnokey.
## Reading
Read paths are free. Browse the routes above, or query:
```sh
gnokey query vm/qeval -data 'gno.land/r/moul/forge/v0.RefOID("moul/forge","refs/heads/main")' -remote <rpc>
gnokey query vm/qeval -data 'gno.land/r/moul/forge/v0.LogHead("moul/forge")' -remote <rpc>
```
`LogHead` is the digest of the last log entry, committing to every entry before
it. Pin it in a release note or a package manifest and the repo's whole history
becomes falsifiable by anyone who can read the chain.
## Namespaces
A repo id is `<namespace>/<name>`. The namespace is either a name the caller
holds in `r/sys/users` or the caller's own bech32 address, checked on every
`CreateRepo` and `Fork`:
- `g1.../forge` works for any account with nothing to register and nothing to
lose, including a realm, whose address is its namespace. That is what makes a
DAO-owned repo work.
- `moul/forge` requires the `moul` name to resolve to the caller, renames
included, since the registry resolves aliases to the same record.
Nobody can claim a namespace they do not own, so there is no squatting to
arbitrate and no reservation list to maintain.
## Roles
`reader < writer < maintainer < admin < owner`. Writers move refs, maintainers
force and merge, admins manage members and the merge policy, and the last owner
cannot be demoted. Opening an issue or a change request needs no role at all:
the author pays gas and locks the storage deposit for their own bytes, which is
the spam gate.
A role can be held by **another realm**, because the realm resolves its caller
without requiring an end user. A repo whose owner is a DAO realm is a repo whose
merge button is a governance vote, which is the thing a centralized forge
structurally cannot offer.
## Events
Every mutation emits one: `RepoCreated`, `RepoForked`, `RepoUpdated`,
`MemberSet`, `RefLog`, `IssueOpened`, `IssueComment`, `IssueClosed`,
`IssueReopened`, `IssueLabeled`, `ChangeOpened`, `ChangeUpdated`,
`ChangeReviewed`, `ChangeComment`, `ChangeClosed`, `ChangeMerged`. `RefLog`
carries the sequence number and the chain digest, so an indexer can mirror the
log without replaying state.
## Costs
A realm write locks 100ugnot per byte, refundable when the data is deleted. A
log entry is roughly 200 bytes including the digest, so a push costs about
0.02 GNOT of deposit plus gas: the reason this design anchors refs instead of
storing objects. An issue with a 500-byte body is about 0.05 GNOT.
<!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) -->
---
Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
**Dependency graph:**

> ⚠️ **Disclaimer:** provided as-is, without warranty; not security-audited. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md).
<!-- END GNOCONTRACTS FOOTER -->
CloseChange(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, changeID int64)
CloseIssue(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, issueID int64)
CommentChange(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, changeID int64, body string)
CommentIssue(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, issueID int64, body string)
CreateRepo(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, description string, defaultRef string)
DeleteRef(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, name string, expectedOID string, note string)
ForceSetRef(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, name string, newOID string, note string)
Fork(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, srcID string, newID string)
HasRepo(repoID string) bool
LogHead(repoID string) string
MergeChange(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, changeID int64, expectedTargetOID string, mergedOID string, note string)
OpenChange(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, title string, body string, sourceRepo string, sourceRef string, headOID string, targetRef string) int64
OpenIssue(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, title string, body string, labels string) int64
RefOID(repoID string, name string) string
Render(path string) string
ReopenIssue(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, issueID int64)
RepoCount() int
ReviewChange(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, changeID int64, verdict string, body string)
SetArchived(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, archived bool)
SetDefaultRef(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, name string)
SetDescription(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, description string)
SetIssueLabels(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, issueID int64, labels string)
SetMember(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, member string, role string)
SetMirrors(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, mirrors string)
SetPolicy(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, requiredApprovals int, allowSelfApproval bool)
SetRef(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, name string, expectedOID string, newOID string, note string)
UpdateChangeHead(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, repoID string, changeID int64, headOID string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.