跳至主要內容

NIP-34: Git 相關事件

在 Nostr 上進行 Git 程式碼協作、議題討論和補丁提交

概述

NIP-34 定義了一系列用於 Git 協作的 Nostr 事件類型。開發者可以在 Nostr 上 建立程式碼倉庫、提交補丁、開設議題,實現去中心化的程式碼協作, 無需依賴 GitHub 等中心化平台。

事件類型

Kind 說明 類型
30617 倉庫公告 (Repository) 可定址
1617 補丁 (Patches) 一般
1621 議題 (Issues) 一般
1622 回覆 (Reply) 一般
1630-1633 狀態 (Status) 一般

倉庫公告 (Kind 30617)

倉庫公告事件宣布一個 Git 倉庫的存在,包含基本資訊和可選的克隆 URL。

標籤

標籤 說明 範例
d 倉庫識別符 ["d", "my-project"]
name 倉庫名稱 ["name", "My Project"]
description 倉庫描述 ["description", "一個很棒的專案"]
clone 克隆 URL ["clone", "https://git.example.com/repo.git"]
web 網頁瀏覽 URL ["web", "https://git.example.com/repo"]
relays 相關中繼器 ["relays", "wss://relay1.com", "wss://relay2.com"]
maintainers 維護者公鑰 ["maintainers", "pubkey1", "pubkey2"]

範例

{
  "kind": 30617,
  "content": "",
  "tags": [
    ["d", "nostr-client"],
    ["name", "Nostr 客戶端"],
    ["description", "一個開源的 Nostr 客戶端應用"],
    ["clone", "https://github.com/example/nostr-client.git"],
    ["web", "https://github.com/example/nostr-client"],
    ["relays", "wss://relay.damus.io", "wss://nos.lol"],
    ["maintainers", "npub1xxx...", "npub1yyy..."],
    ["t", "nostr"],
    ["t", "typescript"]
  ]
}

補丁 (Kind 1617)

補丁事件包含要合併到倉庫的程式碼變更,類似 Pull Request。

標籤

標籤 說明
a 目標倉庫的事件座標
p 倉庫維護者公鑰
t 主題標籤
commit 提交資訊
parent-commit 基於的父提交

Content 格式

Content 包含標準的 Git patch 格式:

From abc123 Mon Sep 17 00:00:00 2001
From: Author <[email protected]>
Date: Mon, 1 Jan 2024 12:00:00 +0800
Subject: [PATCH] 修復登入問題

修復了用戶無法登入的問題

---
 src/auth.ts | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/auth.ts b/src/auth.ts
index abc123..def456 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -10,7 +10,7 @@
-  const token = getToken();
+  const token = await getToken();

議題 (Kind 1621)

議題事件用於報告 bug、提出功能請求或進行討論。

標籤

標籤 說明
a 目標倉庫的事件座標
p 倉庫維護者公鑰
subject 議題標題
t 標籤(bug、feature 等)

範例

{
  "kind": 1621,
  "content": "## 問題描述\n\n在 iOS 上無法正常顯示圖片\n\n## 重現步驟\n\n1. 打開應用\n2. 瀏覽到圖片貼文\n3. 圖片無法載入\n\n## 預期行為\n\n圖片應該正常顯示",
  "tags": [
    ["a", "30617:pubkey:nostr-client", "wss://relay.example.com"],
    ["p", "maintainer-pubkey"],
    ["subject", "iOS 圖片無法顯示"],
    ["t", "bug"],
    ["t", "ios"]
  ]
}

狀態事件 (Kind 1630-1633)

用於標記補丁或議題的狀態:

Kind 狀態 說明
1630 Open 開啟/待處理
1631 Applied/Closed 已合併/已關閉
1632 Closed 已關閉(未合併)
1633 Draft 草稿

TypeScript 實作

建立倉庫公告

import { finalizeEvent } from 'nostr-tools';

interface Repository {
  id: string;
  name: string;
  description?: string;
  cloneUrls?: string[];
  webUrl?: string;
  relays?: string[];
  maintainers?: string[];
  tags?: string[];
}

function createRepositoryEvent(
  repo: Repository,
  secretKey: Uint8Array
) {
  const tags: string[][] = [
    ['d', repo.id],
    ['name', repo.name],
  ];

  if (repo.description) {
    tags.push(['description', repo.description]);
  }

  repo.cloneUrls?.forEach((url) => {
    tags.push(['clone', url]);
  });

  if (repo.webUrl) {
    tags.push(['web', repo.webUrl]);
  }

  if (repo.relays && repo.relays.length > 0) {
    tags.push(['relays', ...repo.relays]);
  }

  if (repo.maintainers && repo.maintainers.length > 0) {
    tags.push(['maintainers', ...repo.maintainers]);
  }

  repo.tags?.forEach((tag) => {
    tags.push(['t', tag]);
  });

  const event = {
    kind: 30617,
    content: '',
    tags,
    created_at: Math.floor(Date.now() / 1000),
  };

  return finalizeEvent(event, secretKey);
}

// 使用範例
const repoEvent = createRepositoryEvent(
  {
    id: 'my-nostr-app',
    name: 'My Nostr App',
    description: '一個去中心化的社交應用',
    cloneUrls: ['https://github.com/user/my-nostr-app.git'],
    webUrl: 'https://github.com/user/my-nostr-app',
    relays: ['wss://relay.damus.io'],
    maintainers: ['npub1...'],
    tags: ['nostr', 'typescript', 'react'],
  },
  secretKey
);

建立議題

interface Issue {
  repoCoordinate: string; // "30617:pubkey:repo-id"
  repoRelay?: string;
  maintainerPubkey: string;
  subject: string;
  content: string;
  labels?: string[];
}

function createIssueEvent(
  issue: Issue,
  secretKey: Uint8Array
) {
  const tags: string[][] = [
    ['a', issue.repoCoordinate, issue.repoRelay || ''],
    ['p', issue.maintainerPubkey],
    ['subject', issue.subject],
  ];

  issue.labels?.forEach((label) => {
    tags.push(['t', label]);
  });

  const event = {
    kind: 1621,
    content: issue.content,
    tags,
    created_at: Math.floor(Date.now() / 1000),
  };

  return finalizeEvent(event, secretKey);
}

// 使用範例
const issueEvent = createIssueEvent(
  {
    repoCoordinate: '30617:abc123:my-nostr-app',
    repoRelay: 'wss://relay.damus.io',
    maintainerPubkey: 'maintainer-pubkey',
    subject: '新增深色模式支援',
    content: '## 功能請求\n\n希望能新增深色模式,保護眼睛。',
    labels: ['feature', 'ui'],
  },
  secretKey
);

建立補丁

interface Patch {
  repoCoordinate: string;
  repoRelay?: string;
  maintainerPubkey: string;
  patchContent: string; // Git patch 格式
  commit?: string;
  parentCommit?: string;
  subject?: string;
}

function createPatchEvent(
  patch: Patch,
  secretKey: Uint8Array
) {
  const tags: string[][] = [
    ['a', patch.repoCoordinate, patch.repoRelay || ''],
    ['p', patch.maintainerPubkey],
  ];

  if (patch.commit) {
    tags.push(['commit', patch.commit]);
  }

  if (patch.parentCommit) {
    tags.push(['parent-commit', patch.parentCommit]);
  }

  if (patch.subject) {
    tags.push(['subject', patch.subject]);
  }

  const event = {
    kind: 1617,
    content: patch.patchContent,
    tags,
    created_at: Math.floor(Date.now() / 1000),
  };

  return finalizeEvent(event, secretKey);
}

更新狀態

type StatusKind = 1630 | 1631 | 1632 | 1633;

interface StatusUpdate {
  targetEventId: string;
  kind: StatusKind;
  reason?: string;
}

function createStatusEvent(
  status: StatusUpdate,
  secretKey: Uint8Array
) {
  const tags: string[][] = [
    ['e', status.targetEventId],
  ];

  const event = {
    kind: status.kind,
    content: status.reason || '',
    tags,
    created_at: Math.floor(Date.now() / 1000),
  };

  return finalizeEvent(event, secretKey);
}

// 使用範例:關閉議題
const closeStatus = createStatusEvent(
  {
    targetEventId: 'issue-event-id',
    kind: 1631,
    reason: '已在 commit abc123 中修復',
  },
  secretKey
);

查詢倉庫相關事件

import { SimplePool } from 'nostr-tools';

async function getRepositoryActivity(
  pool: SimplePool,
  relays: string[],
  repoCoordinate: string
) {
  // 查詢議題
  const issues = await pool.querySync(relays, {
    kinds: [1621],
    '#a': [repoCoordinate],
  });

  // 查詢補丁
  const patches = await pool.querySync(relays, {
    kinds: [1617],
    '#a': [repoCoordinate],
  });

  // 查詢狀態更新
  const issueIds = issues.map((i) => i.id);
  const patchIds = patches.map((p) => p.id);
  const allIds = [...issueIds, ...patchIds];

  const statuses = await pool.querySync(relays, {
    kinds: [1630, 1631, 1632, 1633],
    '#e': allIds,
  });

  return {
    issues,
    patches,
    statuses,
  };
}

使用場景

去中心化程式碼協作

  • 在 Nostr 上宣布開源專案
  • 接收社群貢獻的補丁
  • 討論功能和 bug

抗審查的程式碼托管

  • 不依賴 GitHub 等中心化平台
  • 程式碼相關討論不會被刪除
  • 全球開發者協作

整合 Git 工作流程

  • 透過 Nostr 發送和接收補丁
  • 程式碼審查和討論
  • 追蹤議題狀態

最佳實踐

  • 完整的倉庫資訊:提供克隆 URL 和網頁連結
  • 標準補丁格式:使用 git format-patch 產生的格式
  • 清晰的議題描述:包含重現步驟和預期行為
  • 及時更新狀態:合併或關閉時更新狀態事件
  • 標籤分類:使用標籤幫助分類和搜尋
  • NIP-01:基本協議 - 事件格式
  • NIP-10:回覆與標記 - 對話串結構
  • NIP-22:評論 - 結構化評論

參考資源

已複製連結
已複製到剪貼簿