func CloseChange
crossing ActionCloseChange withdraws or rejects a change (author or maintainer).
Package forge is an on-chain software forge: repos, an append-only reference log, issues, change requests and reviews...
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; 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.
| 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.
1gnokey maketx call -pkgpath gno.land/r/moul/forge/v0 \
2 -func CreateRepo -args "moul/forge" -args "an on-chain forge" -args "" \
3 -gas-fee 1000000ugnot -gas-wanted 3000000 \
4 -broadcast -chainid <chain> -remote <rpc> <key>
1# move a branch: the fourth argument is the tip you expect to replace
2gnokey maketx call -pkgpath gno.land/r/moul/forge/v0 \
3 -func SetRef -args "moul/forge" -args "refs/heads/main" \
4 -args "<expected-oid>" -args "<new-oid>" -args "ship it" \
5 -gas-fee 1000000ugnot -gas-wanted 3000000 \
6 -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.
Read paths are free. Browse the routes above, or query:
1gnokey query vm/qeval -data 'gno.land/r/moul/forge/v0.RefOID("moul/forge","refs/heads/main")' -remote <rpc>
2gnokey 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.
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.
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.
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.
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.
Part of 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.
Package forge is an on-chain software forge: repos, an append-only reference log, issues, change requests and reviews, browsable through gnoweb and writable only through signed transactions.
It is the realm half of gno.land/p/moul/forge/v0, which holds the whole domain model; this file is wiring. Every exported mutation is a crossing function that resolves the caller, forwards to the library, aborts on error (the only way to revert state in gno) and emits an event for indexers.
What the chain stores is NOT the code. Git objects stay in git (a mirror, an IPFS CID, a peer) and the realm records what a forge is actually trusted for: which object a ref points at, in what order, on whose authority, under what review policy, and what got merged. See the package README for the design, the threat model and what this deliberately does not do.
A caller may be a user or another realm: a repo whose owner is a DAO realm is a repo whose merge button is a governance vote.
CloseChange withdraws or rejects a change (author or maintainer).
CloseIssue closes an issue (author or maintainer).
CommentChange appends a reply to a change request.
CommentIssue appends a reply.
CreateRepo registers "<namespace>/<name>" with the caller as owner. defaultRef may be empty for refs/heads/main.
The namespace must be one the caller owns: an r/sys/users name registered to their address, or their own bech32 address. Nobody squats anybody.
DeleteRef removes a ref by compare-and-swap (maintainer). The default branch is not deletable.
ForceSetRef moves a ref with no expectation (maintainer). It is not forbidden, it is recorded as a force, in a log nobody can rewrite.
Fork registers newID as a fork of srcID, snapshotting the parent's current refs into the fork's log so the lineage records exactly what was forked.
HasRepo reports whether a repo id is registered.
LogHead returns the repo's chain digest: pin it off chain and the whole history of every ref becomes falsifiable.
1func MergeChange(cur realm, repoID string, changeID int64, expectedTargetOID, mergedOID, note string)MergeChange moves the target ref to mergedOID and records the move as a merge entry naming the change (maintainer). expectedTargetOID is a compare-and-swap on the target: a change approved against a base that has moved is refused, not silently rebased.
1func OpenChange(cur realm, repoID, title, body, sourceRepo, sourceRef, headOID, targetRef string) int64OpenChange proposes moving targetRef to include headOID. sourceRepo may be another forge repo id, a mirror locator, or empty for this repo. Returns the change id.
OpenIssue files an issue. Permissionless: the author pays for their bytes. labels is comma-separated. Returns the issue id.
RefOID returns the object a ref points at, or "" if there is no such ref.
Render routes gnoweb paths:
1/ the forge: every repo
2/<ns>/<name> repo overview
3/<ns>/<name>/log the full reference log (?page=N)
4/<ns>/<name>/issues issues (?page=N)
5/<ns>/<name>/issues/<id> one issue and its replies
6/<ns>/<name>/changes change requests (?page=N)
7/<ns>/<name>/changes/<id> one change, its reviews and its replies
8/help what this realm is and how to call it
ReopenIssue reopens an issue (author or maintainer).
RepoCount is the number of repos on this forge.
ReviewChange records a verdict: approve, request-changes, comment: against the change's current head. Anyone may review; a writer's approval counts.
SetArchived freezes or unfreezes a repo (admin). The log stays readable.
SetDefaultRef points the repo at another default branch (maintainer).
SetDescription updates the repo description (admin).
SetIssueLabels replaces an issue's labels, comma-separated (maintainer).
SetMember grants a role: none, reader, writer, maintainer, admin, owner.
SetMirrors replaces the fetch locators, comma-separated, first one canonical (maintainer). The chain records them; it never fetches.
SetPolicy sets how many writer approvals a change needs, and whether the author's own approval counts (admin).
SetRef moves a ref by compare-and-swap (writer). expectedOID is the tip the caller last saw, empty to create. A stale expectation aborts instead of overwriting: git's --force-with-lease, with consensus holding the lease.
UpdateChangeHead repoints an open change at a new object (author or writer). Every approval of the previous head stops counting, by construction.