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 event
nostr://<npub|nip05>/<identifier> - owner and repository d tag
nostr://<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

SEE ALSO

NIP-34, Net::Nostr::Comment, Net::Nostr, Net::Nostr::Event