Documentation
A merge request is just a ref
On most Git forges, a pull request is not really a Git object.
It is an application object that happens to point at Git commits.
There is usually a database row with an integer id, an author, a state, a target branch, timestamps, labels, review status, and a collection of comments. Somewhere inside that structure is a commit id.
That model is useful because a modern forge does much more than Git itself.
But it also hides a simpler question:
What is the smallest thing a merge request actually has to represent?
For Everlock, that question matters because repositories are not merely attached to the application. They are the durable state the application is built around.
If a proposal can live inside the repository too, there is a strong reason not to invent a second representation for it.
Strip the feature down to the proposal
A pull request can accumulate a lot of information.
Comments.
Approvals.
CI results.
Assignees.
Labels.
Review threads.
Those are all useful, but none of them are the proposed change itself.
The proposal is much smaller:
here is a commit
please merge it into this target
To make that proposal discussable, it also needs a stable name.
So the essential state becomes:
name
target
commit
Git already has a primitive that represents a name pointing at a commit.
A ref.
That is the entire starting point.
Branches are refs too
A normal branch is just a ref under a conventional namespace:
refs/heads/main
A tag is another kind of ref:
refs/tags/v1.0.0
Git does not say that every meaningful ref has to be a branch or a tag.
Applications can define their own namespaces.
Everlock therefore represents a pull request as:
refs/pull/{slug}/{target} -> <commit-oid>
For example:
refs/pull/fix-calendar-import/main
might point at:
8e13f7...
The ref name tells us the proposal name and the intended target.
The ref value tells us the proposed commit.
Nothing else is required to represent the proposal itself.
Opening a pull request becomes a push
Once the proposal is a ref, opening it is not an API operation.
It is a Git push:
Updating the proposal is the same command again.
The ref now points at the new commit.
Closing the proposal is deleting the ref:
Listing proposals is listing refs:
Fetching one for review is a normal fetch:
The interesting part is not that these commands are short.
It is that there is no parallel pull-request transport.
The thing a contributor already has, Git, is enough to create, update, list, fetch, and remove the proposal.
There is no separate state machine
Traditional forge pull requests usually have explicit states:
open
closed
merged
draft
Those states make sense because the pull request is an application object.
A ref-based proposal has a smaller lifecycle.
At the Git layer:
ref exists -> proposal exists
ref moves -> proposal changes
ref disappears -> proposal closes
That is intentionally sparse.
Whether the proposal is mergeable, whether it has already been integrated, or whether a UI wants to display another status can be computed above that representation.
The repository does not need a state column just to preserve the proposal.
The proposal now travels with the repository
This is where the design becomes especially useful for Everlock.
If proposals are stored in a database next to Git, then backing up the repository is not enough.
You need both:
Git repository
forge database
Lose either one and part of the collaboration state disappears.
If proposals are refs, a repository mirror can preserve them with the rest of the refs.
Conceptually:
repository
|
+--> branches
+--> tags
+--> pull-request refs
The proposal becomes part of the same replication and backup story as the repository itself.
That is a strong property in a system whose architecture already tries to keep durable state inside Git.
Access control gets a natural boundary
Refs also provide a useful authorization boundary.
A contributor should not need permission to modify:
refs/heads/*
just to propose a change.
They only need permission to write:
refs/pull/*
That makes "contribute without maintaining" expressible in terms Git already understands.
Everlock can distinguish between:
write branches
write pull-request refs
instead of treating both as generic repository write access.
The protocol remains a normal Git push.
The authorization policy decides which ref paths that push may update.
If a ref is denied, Git already has a way to report that failure to the client.
The contribution feature does not need a second permissions API.
There is no schema to migrate
A database-backed pull request feature inevitably acquires a schema.
That schema changes.
Fields appear.
Indexes change.
Migration code accumulates.
A ref has a name and an object id.
The semantics around it can evolve, but the durable representation remains extremely small.
This does not mean all collaboration data can avoid a database or some other metadata store.
It means the proposal itself can.
That is an important distinction.
The design deliberately gives things up
The cost is real.
A ref has no author field.
It does not remember who originally created it.
So if several people have permission to update pull-request refs, Git alone does not establish per-creator ownership.
Someone who can update that namespace may be able to update or delete another person's proposal.
If a project requires strict isolation between contributors, the permission model has to provide that isolation explicitly.
The ref cannot invent ownership metadata that does not exist.
Comments do not fit in a ref
The same is true for discussion.
A ref can tell us:
proposal X points at commit Y
It cannot naturally express:
Alice commented on line 17
Bob approved revision 3
CI failed on macOS
Carol requested another change
Those are different kinds of state.
Trying to force them into the ref would make the simple part complicated again.
So the useful architectural boundary is:
proposal -> Git ref
conversation -> separate layer
review metadata -> separate layer
The proposal does not need to become a database row merely because the conversation around it eventually might.
Discovery is intentionally thin
There is another tradeoff.
A forge can show a rich pull-request dashboard.
A raw Git remote cannot.
git ls-remote gives you names and object ids.
That is enough to discover proposals mechanically, but it is not a pleasant review interface by itself.
Everlock can add a shell or web view on top that resolves the refs and calculates useful status.
The repository representation stays small even if the presentation becomes richer.
That is the same separation Everlock uses elsewhere:
durable primitive
|
v
application interpretation
|
v
user interface
The UI does not have to become the storage format.
A merge request is not the conversation
The useful realization is that a modern pull request bundles two different things.
First there is the proposal:
this commit should become part of that branch
Then there is the social and operational machinery around it:
discussion
review
approval
automation
status
The second category is where forge software earns much of its complexity.
But that does not mean the first category needs the same complexity.
For Everlock, keeping the proposal as a ref means the most fundamental part remains a native Git object relationship.
It can be pushed with Git.
Fetched with Git.
Mirrored with Git.
Protected with ref-level permissions.
Removed with Git.
And understood even if the Everlock UI is gone.
Start with the smallest durable primitive
The point is not that every forge should throw away its pull-request database.
Large collaboration platforms have good reasons for storing rich metadata.
The more useful lesson is narrower:
Before creating a new application object, ask whether the durable core of that object already exists in the underlying system.
Here, it did.
Git already knew how to store:
a stable name -> a commit
That turned out to be enough to represent the proposal.
Everything else can remain layered on top.
For Everlock, that means a merge request starts life not as a row in a database, but as something much more ordinary:
a ref
And because the repository is the durable boundary of the system, ordinary is exactly what I want.
For the full semantics, see pull requests and contribution grants.