Federated via graft
4.2 kB
128 lines
1// Package state persists sync bookkeeping so the daemon can tell, across
2// restarts, what has already been mirrored and avoid duplicating issues,
3// patches or commits on every poll.
4package state
5
6import (
7 "database/sql"
8 "fmt"
9
10 _ "modernc.org/sqlite"
11)
12
13// Store wraps the local SQLite database used to track sync state.
14type Store struct {
15 db *sql.DB
16}
17
18// Open creates (if needed) and opens the state database at path.
19func Open(path string) (*Store, error) {
20 db, err := sql.Open("sqlite", path)
21 if err != nil {
22 return nil, fmt.Errorf("open state db: %w", err)
23 }
24 if err := migrate(db); err != nil {
25 db.Close()
26 return nil, fmt.Errorf("migrate state db: %w", err)
27 }
28 return &Store{db: db}, nil
29}
30
31func (s *Store) Close() error {
32 return s.db.Close()
33}
34
35func migrate(db *sql.DB) error {
36 _, err := db.Exec(`
37CREATE TABLE IF NOT EXISTS git_cursor (
38 repo_pair TEXT PRIMARY KEY,
39 forgejo_sha TEXT NOT NULL DEFAULT '',
40 radicle_sha TEXT NOT NULL DEFAULT ''
41);
42
43CREATE TABLE IF NOT EXISTS item_mapping (
44 repo_pair TEXT NOT NULL,
45 kind TEXT NOT NULL, -- 'issue' or 'patch'
46 forgejo_id INTEGER NOT NULL DEFAULT 0,
47 radicle_id TEXT NOT NULL DEFAULT '',
48 content_hash TEXT NOT NULL DEFAULT '',
49 synced_at TEXT NOT NULL,
50 PRIMARY KEY (repo_pair, kind, forgejo_id, radicle_id)
51);
52
53CREATE INDEX IF NOT EXISTS idx_item_mapping_forgejo
54 ON item_mapping (repo_pair, kind, forgejo_id);
55CREATE INDEX IF NOT EXISTS idx_item_mapping_radicle
56 ON item_mapping (repo_pair, kind, radicle_id);
57`)
58 return err
59}
60
61// GitCursor returns the last-synced commit SHAs on each side for a repo pair.
62func (s *Store) GitCursor(repoPair string) (forgejoSHA, radicleSHA string, err error) {
63 row := s.db.QueryRow(`SELECT forgejo_sha, radicle_sha FROM git_cursor WHERE repo_pair = ?`, repoPair)
64 err = row.Scan(&forgejoSHA, &radicleSHA)
65 if err == sql.ErrNoRows {
66 return "", "", nil
67 }
68 return forgejoSHA, radicleSHA, err
69}
70
71// SetGitCursor records the commit SHAs last observed on each side.
72func (s *Store) SetGitCursor(repoPair, forgejoSHA, radicleSHA string) error {
73 _, err := s.db.Exec(`
74INSERT INTO git_cursor (repo_pair, forgejo_sha, radicle_sha) VALUES (?, ?, ?)
75ON CONFLICT (repo_pair) DO UPDATE SET forgejo_sha = excluded.forgejo_sha, radicle_sha = excluded.radicle_sha
76`, repoPair, forgejoSHA, radicleSHA)
77 return err
78}
79
80// ItemMapping records the correspondence between a Forgejo issue/PR and its
81// mirrored Radicle issue/patch, plus a content hash used to detect edits
82// that need re-syncing.
83type ItemMapping struct {
84 Kind string // "issue" or "patch"
85 ForgejoID int64
86 RadicleID string
87 ContentHash string
88}
89
90// FindByForgejoID looks up an existing mapping created for a Forgejo item.
91func (s *Store) FindByForgejoID(repoPair, kind string, forgejoID int64) (*ItemMapping, error) {
92 row := s.db.QueryRow(`
93SELECT radicle_id, content_hash FROM item_mapping
94WHERE repo_pair = ? AND kind = ? AND forgejo_id = ?
95`, repoPair, kind, forgejoID)
96 m := &ItemMapping{Kind: kind, ForgejoID: forgejoID}
97 err := row.Scan(&m.RadicleID, &m.ContentHash)
98 if err == sql.ErrNoRows {
99 return nil, nil
100 }
101 return m, err
102}
103
104// FindByRadicleID looks up an existing mapping created for a Radicle item.
105func (s *Store) FindByRadicleID(repoPair, kind, radicleID string) (*ItemMapping, error) {
106 row := s.db.QueryRow(`
107SELECT forgejo_id, content_hash FROM item_mapping
108WHERE repo_pair = ? AND kind = ? AND radicle_id = ?
109`, repoPair, kind, radicleID)
110 m := &ItemMapping{Kind: kind, RadicleID: radicleID}
111 err := row.Scan(&m.ForgejoID, &m.ContentHash)
112 if err == sql.ErrNoRows {
113 return nil, nil
114 }
115 return m, err
116}
117
118// Upsert records or updates a mapping between a Forgejo item and its
119// Radicle counterpart.
120func (s *Store) Upsert(repoPair string, m ItemMapping) error {
121 _, err := s.db.Exec(`
122INSERT INTO item_mapping (repo_pair, kind, forgejo_id, radicle_id, content_hash, synced_at)
123VALUES (?, ?, ?, ?, ?, datetime('now'))
124ON CONFLICT (repo_pair, kind, forgejo_id, radicle_id)
125DO UPDATE SET content_hash = excluded.content_hash, synced_at = excluded.synced_at
126`, repoPair, m.Kind, m.ForgejoID, m.RadicleID, m.ContentHash)
127 return err
128}