NAME
Net::Nostr::Git - NIP-34 git collaboration over Nostr
SYNOPSIS
use Net::Nostr::Git;
# Announce a repository
my $repo = Net::Nostr::Git->repository(
pubkey => $my_pubkey,
id => 'my-project',
name => 'My Project',
description => 'A Nostr library',
clone => ['https://github.com/user/repo.git'],
relays => ['wss://relay.example.com'],
);
# Announce repository state
my $state = Net::Nostr::Git->repository_state(
pubkey => $my_pubkey,
id => 'my-project',
refs => [
['refs/heads/main', $commit_id],
['refs/tags/v1.0', $tag_commit_id],
],
head => 'main',
);
# Submit a patch
my $patch = Net::Nostr::Git->patch(
pubkey => $my_pubkey,
content => $git_format_patch_output,
repository => "30617:$owner_pk:my-project",
repo_owner => $owner_pk,
root => 1,
);
# Open a pull request
my $pr = Net::Nostr::Git->pull_request(
pubkey => $my_pubkey,
content => 'Please review these changes.',
repository => "30617:$owner_pk:my-project",
subject => 'Add feature X',
commit => $tip_commit_id,
clone => ['https://github.com/user/fork.git'],
);
# File an issue
my $issue = Net::Nostr::Git->issue(
pubkey => $my_pubkey,
content => 'Found a bug in parsing.',
repository => "30617:$owner_pk:my-project",
repo_owner => $owner_pk,
subject => 'Crash on startup',
labels => ['bug'],
);
# Set status
my $status = Net::Nostr::Git->status(
pubkey => $my_pubkey,
status => 'applied',
target => $patch_event_id,
repo_owner => $my_pubkey,
target_author => $author_pk,
);
# Update a pull request
my $update = Net::Nostr::Git->pull_request_update(
pubkey => $my_pubkey,
repository => "30617:$owner_pk:my-project",
pr_event => $pr_event_id,
pr_author => $pr_author_pk,
commit => $new_tip_commit_id,
clone => ['https://github.com/user/fork.git'],
);
# User grasp list
my $list = Net::Nostr::Git->grasp_list(
pubkey => $my_pubkey,
servers => ['wss://grasp.example.com'],
);
# Parse any NIP-34 event
my $info = Net::Nostr::Git->from_event($event);
say $info->event_type; # 'repository', 'patch', 'issue', etc.
# Validate a NIP-34 event
Net::Nostr::Git->validate($event);
DESCRIPTION
Implements NIP-34 git collaboration over Nostr. Provides methods to create all NIP-34 event kinds: repository announcements, repository state, patches, pull requests, pull request updates, issues, status events, and user grasp lists.
Replies to patches, PRs, and issues follow NIP-22 comment threading (see Net::Nostr::Comment).
CONSTRUCTOR
new
my $info = Net::Nostr::Git->new(%fields);
Creates a new Net::Nostr::Git object. Typically returned by "from_event"; calling new directly is useful for testing or manual construction.
my $info = Net::Nostr::Git->new(
event_type => 'repository',
repo_id => 'my-project',
);
Accepted fields: event_type, repo_id, repo_name, repo_description, web, clone_urls, relay_urls, earliest_unique_commit, maintainer_pubkeys, repository_address, subject, status_name, grasp_servers. Croaks on unknown arguments.
CLASS METHODS
repository
my $event = Net::Nostr::Git->repository(
pubkey => $hex_pubkey,
id => 'repo-id', # required, becomes d tag
name => 'Human Name', # optional
description => 'Description', # optional
web => ['https://...'], # optional, multiple values
clone => ['https://...git'], # optional, multiple values
relays => ['wss://...'], # optional, multiple values
earliest_unique_commit => $commit_hex, # optional, r tag with euc
maintainers => [$pubkey, ...], # optional, multiple values
personal_fork => 1, # optional, adds t:personal-fork
hashtags => ['tag1', 'tag2'], # optional
);
Creates a kind 30617 (addressable) repository announcement event. Only pubkey and id are required; all other tags are optional per spec.
The web, clone, relays, and maintainers tags support multiple values passed as arrayrefs.
repository_state
my $event = Net::Nostr::Git->repository_state(
pubkey => $hex_pubkey,
id => 'repo-id',
refs => [
['refs/heads/main', $commit_id],
['refs/heads/dev', $commit_id, $parent_short, $grandparent_short],
],
head => 'main', # optional, becomes HEAD tag
);
Creates a kind 30618 (addressable) repository state event. The d tag matches the corresponding repository announcement.
Each ref is an arrayref of [ref-path, commit-id, ...]. Additional elements after the commit ID are optional shorthand ancestor commits for client use.
If no refs are provided, the author signals they are no longer tracking state.
patch
my $event = Net::Nostr::Git->patch(
pubkey => $hex_pubkey,
content => $git_format_patch_output,
repository => '30617:<owner-pk>:<repo-id>',
repo_owner => $owner_pk, # optional p tag
notify => [$other_pk], # optional additional p tags
earliest_unique_commit => $commit_id, # optional r tag
root => 1, # optional t:root for first patch
root_revision => 1, # optional t:root-revision
previous_patch => $event_id, # optional NIP-10 e reply tag
previous_patch_relay => 'wss://...', # optional relay hint for e tag
commit => $commit_id, # optional stable commit id
parent_commit => $commit_id, # optional
commit_pgp_sig => '...', # optional
committer => [$name, $email, $ts, $tz], # optional
);
Creates a kind 1617 patch event. Content should be the output of git format-patch. The first patch in a series MAY be a cover letter in the format produced by git format-patch --cover-letter.
Set root for the first patch in a series. Set root_revision for the first patch in a revision. Use previous_patch (with optional previous_patch_relay) for NIP-10 e reply threading within a patch series.
pull_request
my $event = Net::Nostr::Git->pull_request(
pubkey => $hex_pubkey,
content => 'Markdown description',
repository => '30617:<owner-pk>:<repo-id>',
subject => 'PR title', # optional
commit => $tip_commit_id, # required, c tag
clone => ['https://...git'], # required, at least one
repo_owner => $owner_pk, # optional p tag
notify => [$other_pk], # optional
labels => ['enhancement'], # optional t tags
branch_name => 'feature-x', # optional
revises => $root_patch_event_id, # optional e tag
merge_base => $commit_id, # optional
earliest_unique_commit => $commit_id, # optional r tag
);
Creates a kind 1618 pull request event. commit and clone are required.
pull_request_update
my $event = Net::Nostr::Git->pull_request_update(
pubkey => $hex_pubkey,
repository => '30617:<owner-pk>:<repo-id>',
pr_event => $pr_event_id, # E tag
pr_author => $pr_author_pk, # P tag
commit => $new_tip_commit_id, # c tag
clone => ['https://...git'], # clone tag
repo_owner => $owner_pk, # optional
notify => [$other_pk], # optional
merge_base => $commit_id, # optional
earliest_unique_commit => $cid, # optional r tag
);
Creates a kind 1619 pull request update event with NIP-22 E and P tags pointing to the original PR.
issue
my $event = Net::Nostr::Git->issue(
pubkey => $hex_pubkey,
content => 'Markdown bug report',
repository => '30617:<owner-pk>:<repo-id>',
repo_owner => $owner_pk, # optional
subject => 'Issue title', # optional
labels => ['bug', 'critical'], # optional t tags
);
Creates a kind 1621 issue event with Markdown content. Issues may optionally include a subject tag and one or more t label tags.
status
my $event = Net::Nostr::Git->status(
pubkey => $hex_pubkey,
status => 'open', # open|applied|merged|resolved|closed|draft
target => $event_id, # e root tag
repo_owner => $owner_pk, # p tag
target_author => $author_pk, # p tag
content => 'Optional note', # optional, markdown
accepted_revision => $event_id, # optional, e reply tag
revision_author => $pk, # optional, p tag
repository => $repo_coord, # optional, a tag
repository_relay => 'wss://...', # optional, relay hint
earliest_unique_commit => $commit_id, # optional, r tag
applied_patches => [{id => $eid, relay_url => $r, pubkey => $pk}], # optional (1631)
merge_commit => $commit_id, # optional (1631)
applied_as_commits => [$cid1, $cid2], # optional (1631)
);
Creates a status event: kind 1630 (Open), 1631 (Applied/Merged/Resolved), 1632 (Closed), or 1633 (Draft).
Per spec, the most recent status event (by created_at) from either the issue/patch author or a maintainer is considered the current status. The status of a patch-revision inherits from its root-patch, or becomes Closed (1632) if the root-patch is Applied/Merged (1631) and the revision is not tagged in the Applied event.
grasp_list
my $event = Net::Nostr::Git->grasp_list(
pubkey => $hex_pubkey,
servers => ['wss://grasp1.com', 'wss://grasp2.com'],
);
Creates a kind 10317 (replaceable) user grasp list event with g tags for grasp server URLs in order of preference. servers is optional; zero or more grasp server URLs may be provided.
nostr_clone_url
my $url = Net::Nostr::Git->nostr_clone_url(naddr => $naddr_bech32);
my $url = Net::Nostr::Git->nostr_clone_url(
owner => $npub_or_nip05,
identifier => 'repo-id',
);
my $url = Net::Nostr::Git->nostr_clone_url(
owner => $npub_or_nip05,
relay_hint => 'wss://relay.example.com',
identifier => 'repo-id',
);
Builds a nostr:// clone URL compatible with git clone when a git-remote-nostr helper is installed. Three forms are supported:
nostr://<naddr>- an naddr bech32 referencing a kind 30617 eventnostr://<npub|nip05>/<identifier>- owner and repositorydtagnostr://<npub|nip05>/<relay-hint>/<identifier>- with relay hint
relay_hint and identifier are automatically percent-encoded per RFC 3986. The wss:// scheme is stripped from relay hints for brevity; other schemes (e.g. ws://) are kept and percent-encoded.
Croaks if required arguments are missing or if the naddr is invalid.
parse_nostr_clone_url
my $result = Net::Nostr::Git->parse_nostr_clone_url($url);
Parses a nostr:// clone URL. For the naddr form, returns the decoded naddr data (same structure as decode_naddr from Net::Nostr::Bech32):
# { identifier => '...', pubkey => '...', kind => 30617, relays => [...] }
For owner forms, returns:
# { owner => '...', identifier => '...' }
# { owner => '...', relay_hint => 'wss://...', identifier => '...' }
Relay hints without a scheme get wss:// prepended. Percent-encoded components are decoded automatically.
Croaks if the URL is missing, does not start with nostr://, or lacks required path segments.
# Spec examples:
Net::Nostr::Git->parse_nostr_clone_url(
'nostr://npub1.../relay.ngit.dev/ngit'
);
# { owner => 'npub1...', relay_hint => 'wss://relay.ngit.dev', identifier => 'ngit' }
Net::Nostr::Git->parse_nostr_clone_url(
'nostr://danconwaydev.com/ws%3A%2F%2Flocalhost%3A7334/my-local-only-repo'
);
# { owner => 'danconwaydev.com', relay_hint => 'ws://localhost:7334', identifier => 'my-local-only-repo' }
from_event
my $info = Net::Nostr::Git->from_event($event);
Parses a NIP-34 event and returns a Net::Nostr::Git object with appropriate accessors populated, or undef if the event is not a NIP-34 kind.
say $info->event_type; # 'repository', 'patch', 'issue', etc.
say $info->repo_id; # for repository/state events
say $info->subject; # for PR/issue events
validate
Net::Nostr::Git->validate($event);
Validates that an event is a well-formed NIP-34 event. Croaks if required tags are missing for the given kind. Returns 1 on success.
eval { Net::Nostr::Git->validate($event) };
warn "Invalid: $@" if $@;
ACCESSORS
Available on objects returned by "from_event".
event_type
my $type = $info->event_type;
# 'repository', 'repository_state', 'patch', 'pull_request',
# 'pull_request_update', 'issue', 'status', 'grasp_list'
repo_id
my $id = $info->repo_id; # d tag value
repo_name
my $name = $info->repo_name;
repo_description
my $desc = $info->repo_description;
web
my $urls = $info->web; # arrayref
clone_urls
my $urls = $info->clone_urls; # arrayref
relay_urls
my $urls = $info->relay_urls; # arrayref
earliest_unique_commit
my $commit = $info->earliest_unique_commit;
maintainer_pubkeys
my $pks = $info->maintainer_pubkeys; # arrayref
repository_address
my $addr = $info->repository_address; # a tag value
subject
my $subj = $info->subject;
status_name
my $name = $info->status_name; # 'open', 'applied', 'closed', 'draft'
grasp_servers
my $servers = $info->grasp_servers; # arrayref of URLs