Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview/db,models: add repo_did columns and update model structs

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (May 11, 2026, 11:59 AM +0300) commit 15218e05 parent b1ccdcbd change-id kypomost
+637 -101
+414 -14
appview/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 + "fmt" 6 7 "log/slog" 7 8 "strings" 8 9 ··· 115 116 issue_at text, 116 117 unique(repo_at, issue_id), 117 118 foreign key (repo_at) references repos(at_uri) on delete cascade 118 - ); 119 - create table if not exists comments ( 120 - id integer primary key autoincrement, 121 - owner_did text not null, 122 - issue_id integer not null, 123 - repo_at text not null, 124 - comment_id integer not null, 125 - comment_at text not null, 126 - body text not null, 127 - created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 128 - unique(issue_id, comment_id), 129 - foreign key (repo_at, issue_id) references issues(repo_at, issue_id) on delete cascade 130 119 ); 131 120 create table if not exists pulls ( 132 121 -- identifiers ··· 693 682 create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read); 694 683 create index if not exists idx_references_from_at on reference_links(from_at); 695 684 create index if not exists idx_references_to_at on reference_links(to_at); 696 - create index if not exists idx_webhooks_repo_at on webhooks(repo_at); 697 685 create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id); 698 - create index if not exists idx_site_deploys_repo_at on site_deploys(repo_at); 699 686 create index if not exists idx_newsletter_prefs_user_did on newsletter_preferences(user_did); 700 687 `) 701 688 if err != nil { ··· 1544 1531 1545 1532 drop table pipeline_statuses; 1546 1533 alter table pipeline_statuses_new rename to pipeline_statuses; 1534 + `) 1535 + return err 1536 + }) 1537 + conn.ExecContext(ctx, "pragma foreign_keys = on;") 1538 + 1539 + orm.RunMigration(conn, logger, "add-repo-renames", func(tx *sql.Tx) error { 1540 + res, err := tx.Exec(` 1541 + update repos 1542 + set name = name || '-renamed-' || id || '-' || lower(hex(randomblob(4))) 1543 + where id in ( 1544 + select id from ( 1545 + select id, row_number() over ( 1546 + partition by did, knot, name 1547 + order by created desc, id desc 1548 + ) as rn 1549 + from repos 1550 + ) where rn > 1 1551 + ); 1552 + `) 1553 + if err != nil { 1554 + return err 1555 + } 1556 + if n, _ := res.RowsAffected(); n > 0 { 1557 + logger.Warn("suffixed legacy duplicate repo names before adding unique index", "rows", n) 1558 + } 1559 + 1560 + var remaining int 1561 + if err := tx.QueryRow(` 1562 + select count(*) from ( 1563 + select 1 from repos group by did, knot, name having count(*) > 1 1564 + ) 1565 + `).Scan(&remaining); err != nil { 1566 + return fmt.Errorf("checking for residual duplicate (did, knot, name) groups: %w", err) 1567 + } 1568 + if remaining > 0 { 1569 + return fmt.Errorf("add-repo-renames: %d duplicate (did, knot, name) groups remain after suffix pass; manual cleanup required before unique index can be created", remaining) 1570 + } 1571 + 1572 + _, err = tx.Exec(` 1573 + create table if not exists repo_renames ( 1574 + owner_did text not null, 1575 + old_rkey text not null, 1576 + repo_did text not null, 1577 + renamed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1578 + primary key (owner_did, old_rkey) 1579 + ); 1580 + create unique index if not exists idx_repos_owner_knot_name 1581 + on repos(did, knot, name); 1582 + `) 1583 + return err 1584 + }) 1585 + 1586 + orm.RunMigration(conn, logger, "repos-canonical-rkey-uniqueness", func(tx *sql.Tx) error { 1587 + _, err := tx.Exec(` 1588 + drop index if exists idx_repos_owner_knot_name; 1589 + create unique index if not exists idx_repos_did_rkey 1590 + on repos(did, rkey); 1591 + `) 1592 + return err 1593 + }) 1594 + 1595 + orm.RunMigration(conn, logger, "repo-did-references", func(tx *sql.Tx) error { 1596 + tables := []struct{ table, oldCol, newCol string }{ 1597 + {"issues", "repo_at", "repo_did"}, 1598 + {"pulls", "repo_at", "repo_did"}, 1599 + {"pull_comments", "repo_at", "repo_did"}, 1600 + {"stars", "subject_at", "subject_did"}, 1601 + {"artifacts", "repo_at", "repo_did"}, 1602 + {"webhooks", "repo_at", "repo_did"}, 1603 + {"repo_sites", "repo_at", "repo_did"}, 1604 + {"site_deploys", "repo_at", "repo_did"}, 1605 + {"collaborators", "repo_at", "repo_did"}, 1606 + {"repo_issue_seqs", "repo_at", "repo_did"}, 1607 + {"repo_pull_seqs", "repo_at", "repo_did"}, 1608 + {"repo_languages", "repo_at", "repo_did"}, 1609 + {"repo_labels", "repo_at", "repo_did"}, 1610 + } 1611 + 1612 + stmts := "" 1613 + for _, t := range tables { 1614 + stmts += fmt.Sprintf( 1615 + `ALTER TABLE %s ADD COLUMN %s TEXT; 1616 + UPDATE %s SET %s = (SELECT repos.repo_did FROM repos WHERE repos.at_uri = %s.%s); 1617 + CREATE INDEX IF NOT EXISTS idx_%s_%s ON %s(%s); 1618 + `, t.table, t.newCol, t.table, t.newCol, t.table, t.oldCol, t.table, t.newCol, t.table, t.newCol) 1619 + } 1620 + 1621 + stmts += `ALTER TABLE pulls ADD COLUMN source_repo_did TEXT; 1622 + UPDATE pulls SET source_repo_did = (SELECT repos.repo_did FROM repos WHERE repos.at_uri = pulls.source_repo_at); 1623 + 1624 + UPDATE profile_pinned_repositories SET pin = ( 1625 + SELECT repos.repo_did FROM repos WHERE repos.at_uri = profile_pinned_repositories.pin 1626 + ) WHERE pin LIKE 'at://%' 1627 + AND EXISTS (SELECT 1 FROM repos WHERE repos.at_uri = profile_pinned_repositories.pin AND repos.repo_did IS NOT NULL AND repos.repo_did != ''); 1628 + ` 1629 + 1630 + _, err := tx.Exec(stmts) 1631 + return err 1632 + }) 1633 + 1634 + orm.RunMigration(conn, logger, "backfill-pds-rewrites-star-issue-pull-collab", func(tx *sql.Tx) error { 1635 + type source struct { 1636 + userDidCol string 1637 + table string 1638 + nsid string 1639 + fkCol string 1640 + } 1641 + sources := []source{ 1642 + {"did", "stars", "sh.tangled.feed.star", "subject_at"}, 1643 + {"did", "issues", "sh.tangled.repo.issue", "repo_at"}, 1644 + {"owner_did", "pulls", "sh.tangled.repo.pull", "repo_at"}, 1645 + {"did", "collaborators", "sh.tangled.repo.collaborator", "repo_at"}, 1646 + } 1647 + 1648 + for _, src := range sources { 1649 + _, err := tx.Exec(fmt.Sprintf(` 1650 + INSERT INTO pds_migration (name, did, collection, rkey, status) 1651 + SELECT 'add-repo-did', t.%s, '%s', t.rkey, 'pending' 1652 + FROM %s t 1653 + JOIN repos r ON r.at_uri = t.%s 1654 + WHERE r.repo_did IS NOT NULL AND r.repo_did != '' 1655 + ON CONFLICT(name, did, collection, rkey) DO NOTHING 1656 + `, src.userDidCol, src.nsid, src.table, src.fkCol)) 1657 + if err != nil { 1658 + return fmt.Errorf("backfill pds rewrites for %s: %w", src.table, err) 1659 + } 1660 + } 1661 + 1662 + return nil 1663 + }) 1664 + 1665 + orm.RunMigration(conn, logger, "backfill-pds-rewrites-profiles", func(tx *sql.Tx) error { 1666 + _, err := tx.Exec(` 1667 + INSERT INTO pds_migration (name, did, collection, rkey, status) 1668 + SELECT DISTINCT 'add-repo-did', pp.did, 'sh.tangled.actor.profile', 'self', 'pending' 1669 + FROM profile_pinned_repositories pp 1670 + JOIN repos r ON r.at_uri = pp.pin 1671 + WHERE pp.pin LIKE 'at://%' 1672 + AND r.repo_did IS NOT NULL AND r.repo_did != '' 1673 + ON CONFLICT(name, did, collection, rkey) DO NOTHING 1674 + `) 1675 + if err != nil { 1676 + return fmt.Errorf("backfill pds rewrites for profiles: %w", err) 1677 + } 1678 + return nil 1679 + }) 1680 + 1681 + conn.ExecContext(ctx, "pragma foreign_keys = off;") 1682 + orm.RunMigration(conn, logger, "drop-old-at-uri-columns", func(tx *sql.Tx) error { 1683 + _, err := tx.Exec(` 1684 + CREATE TABLE repos_new ( 1685 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1686 + did TEXT NOT NULL, 1687 + name TEXT NOT NULL, 1688 + knot TEXT NOT NULL, 1689 + rkey TEXT NOT NULL, 1690 + at_uri TEXT NOT NULL UNIQUE, 1691 + created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1692 + description TEXT CHECK (length(description) <= 200), 1693 + source TEXT, 1694 + spindle TEXT, 1695 + website TEXT, 1696 + topics TEXT, 1697 + repo_did TEXT, 1698 + UNIQUE(did, rkey) 1699 + ); 1700 + INSERT INTO repos_new (id, did, name, knot, rkey, at_uri, created, description, source, spindle, website, topics, repo_did) 1701 + SELECT id, did, name, knot, rkey, at_uri, created, description, source, spindle, website, topics, repo_did 1702 + FROM repos; 1703 + DROP TABLE repos; 1704 + ALTER TABLE repos_new RENAME TO repos; 1705 + CREATE UNIQUE INDEX idx_repos_repo_did ON repos(repo_did); 1706 + CREATE UNIQUE INDEX idx_repos_did_rkey ON repos(did, rkey); 1707 + 1708 + CREATE TABLE issues_new ( 1709 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1710 + did TEXT NOT NULL, 1711 + rkey TEXT NOT NULL, 1712 + at_uri TEXT GENERATED ALWAYS AS ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) STORED, 1713 + repo_did TEXT NOT NULL, 1714 + issue_id INTEGER NOT NULL, 1715 + title TEXT NOT NULL, 1716 + body TEXT NOT NULL, 1717 + open INTEGER NOT NULL DEFAULT 1, 1718 + created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1719 + edited TEXT, 1720 + deleted TEXT, 1721 + UNIQUE(did, rkey), 1722 + UNIQUE(repo_did, issue_id), 1723 + UNIQUE(at_uri), 1724 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1725 + ); 1726 + INSERT INTO issues_new (id, did, rkey, repo_did, issue_id, title, body, open, created, edited, deleted) 1727 + SELECT id, did, rkey, repo_did, issue_id, title, body, open, created, edited, deleted 1728 + FROM issues WHERE repo_did IS NOT NULL AND repo_did != ''; 1729 + DROP TABLE issues; 1730 + ALTER TABLE issues_new RENAME TO issues; 1731 + CREATE INDEX idx_issues_repo_did ON issues(repo_did); 1732 + 1733 + CREATE TABLE pulls_new ( 1734 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1735 + pull_id INTEGER NOT NULL, 1736 + at_uri TEXT GENERATED ALWAYS AS ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) STORED, 1737 + repo_did TEXT NOT NULL, 1738 + owner_did TEXT NOT NULL, 1739 + rkey TEXT NOT NULL, 1740 + title TEXT NOT NULL, 1741 + body TEXT NOT NULL, 1742 + target_branch TEXT NOT NULL, 1743 + state INTEGER NOT NULL DEFAULT 0 CHECK (state IN (0, 1, 2, 3)), 1744 + source_branch TEXT, 1745 + source_repo_did TEXT, 1746 + change_id TEXT, 1747 + dependent_on TEXT, 1748 + created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1749 + UNIQUE(repo_did, pull_id), 1750 + UNIQUE(at_uri), 1751 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1752 + ); 1753 + INSERT INTO pulls_new (id, pull_id, repo_did, owner_did, rkey, title, body, target_branch, state, source_branch, source_repo_did, change_id, dependent_on, created) 1754 + SELECT id, pull_id, repo_did, owner_did, rkey, title, body, target_branch, state, source_branch, source_repo_did, change_id, dependent_on, created 1755 + FROM pulls WHERE repo_did IS NOT NULL AND repo_did != ''; 1756 + DROP TABLE pulls; 1757 + ALTER TABLE pulls_new RENAME TO pulls; 1758 + CREATE INDEX idx_pulls_repo_did ON pulls(repo_did); 1759 + CREATE INDEX idx_pulls_source_repo_did ON pulls(source_repo_did); 1760 + 1761 + CREATE TABLE pull_comments_new ( 1762 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1763 + pull_id INTEGER NOT NULL, 1764 + submission_id INTEGER NOT NULL, 1765 + repo_did TEXT NOT NULL, 1766 + owner_did TEXT NOT NULL, 1767 + comment_at TEXT NOT NULL, 1768 + body TEXT NOT NULL, 1769 + created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1770 + FOREIGN KEY (repo_did, pull_id) REFERENCES pulls(repo_did, pull_id) ON DELETE CASCADE, 1771 + FOREIGN KEY (submission_id) REFERENCES pull_submissions(id) ON DELETE CASCADE 1772 + ); 1773 + INSERT INTO pull_comments_new (id, pull_id, submission_id, repo_did, owner_did, comment_at, body, created) 1774 + SELECT id, pull_id, submission_id, repo_did, owner_did, comment_at, body, created 1775 + FROM pull_comments WHERE repo_did IS NOT NULL AND repo_did != ''; 1776 + DROP TABLE pull_comments; 1777 + ALTER TABLE pull_comments_new RENAME TO pull_comments; 1778 + CREATE INDEX idx_pull_comments_repo_did ON pull_comments(repo_did); 1779 + 1780 + CREATE TABLE stars_new ( 1781 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1782 + did TEXT NOT NULL, 1783 + rkey TEXT NOT NULL, 1784 + subject_type TEXT NOT NULL CHECK (subject_type IN ('repo', 'string')), 1785 + subject TEXT NOT NULL, 1786 + created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1787 + UNIQUE(did, rkey), 1788 + UNIQUE(did, subject) 1789 + ); 1790 + INSERT INTO stars_new (id, did, rkey, subject_type, subject, created) 1791 + SELECT id, did, rkey, 'repo', subject_did, created 1792 + FROM stars 1793 + WHERE subject_did IS NOT NULL AND subject_did != ''; 1794 + INSERT OR IGNORE INTO stars_new (id, did, rkey, subject_type, subject, created) 1795 + SELECT id, did, rkey, 'string', subject_at, created 1796 + FROM stars 1797 + WHERE (subject_did IS NULL OR subject_did = '') 1798 + AND subject_at LIKE 'at://%/sh.tangled.string/%'; 1799 + DROP TABLE stars; 1800 + ALTER TABLE stars_new RENAME TO stars; 1801 + CREATE INDEX idx_stars_subject ON stars(subject); 1802 + CREATE INDEX idx_stars_subject_type ON stars(subject_type); 1803 + CREATE INDEX idx_stars_created ON stars(created); 1804 + 1805 + CREATE TABLE collaborators_new ( 1806 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1807 + did TEXT NOT NULL, 1808 + rkey TEXT, 1809 + subject_did TEXT NOT NULL, 1810 + repo_did TEXT NOT NULL, 1811 + created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1812 + UNIQUE(did, rkey), 1813 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1814 + ); 1815 + INSERT INTO collaborators_new (id, did, rkey, subject_did, repo_did, created) 1816 + SELECT id, did, NULLIF(rkey, ''), subject_did, repo_did, created 1817 + FROM collaborators WHERE repo_did IS NOT NULL AND repo_did != ''; 1818 + DROP TABLE collaborators; 1819 + ALTER TABLE collaborators_new RENAME TO collaborators; 1820 + CREATE INDEX idx_collaborators_repo_did ON collaborators(repo_did); 1821 + 1822 + CREATE TABLE artifacts_new ( 1823 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1824 + did TEXT NOT NULL, 1825 + rkey TEXT NOT NULL, 1826 + repo_did TEXT NOT NULL, 1827 + tag BINARY(20) NOT NULL, 1828 + created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1829 + blob_cid TEXT NOT NULL, 1830 + name TEXT NOT NULL, 1831 + size INTEGER NOT NULL DEFAULT 0, 1832 + mimetype TEXT NOT NULL DEFAULT '*/*', 1833 + UNIQUE(did, rkey), 1834 + UNIQUE(repo_did, tag, name), 1835 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1836 + ); 1837 + INSERT INTO artifacts_new (id, did, rkey, repo_did, tag, created, blob_cid, name, size, mimetype) 1838 + SELECT id, did, rkey, repo_did, tag, created, blob_cid, name, size, mimetype 1839 + FROM artifacts WHERE repo_did IS NOT NULL AND repo_did != ''; 1840 + DROP TABLE artifacts; 1841 + ALTER TABLE artifacts_new RENAME TO artifacts; 1842 + CREATE INDEX idx_artifacts_repo_did ON artifacts(repo_did); 1843 + 1844 + CREATE TABLE webhooks_new ( 1845 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1846 + repo_did TEXT NOT NULL, 1847 + url TEXT NOT NULL, 1848 + secret TEXT, 1849 + active INTEGER NOT NULL DEFAULT 1, 1850 + events TEXT NOT NULL, 1851 + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1852 + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1853 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1854 + ); 1855 + INSERT INTO webhooks_new (id, repo_did, url, secret, active, events, created_at, updated_at) 1856 + SELECT id, repo_did, url, secret, active, events, created_at, updated_at 1857 + FROM webhooks WHERE repo_did IS NOT NULL AND repo_did != ''; 1858 + DROP TABLE webhooks; 1859 + ALTER TABLE webhooks_new RENAME TO webhooks; 1860 + CREATE INDEX idx_webhooks_repo_did ON webhooks(repo_did); 1861 + 1862 + CREATE TABLE repo_sites_new ( 1863 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1864 + repo_did TEXT NOT NULL UNIQUE, 1865 + branch TEXT NOT NULL, 1866 + dir TEXT NOT NULL DEFAULT '/', 1867 + is_index INTEGER NOT NULL DEFAULT 0, 1868 + created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1869 + updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1870 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1871 + ); 1872 + INSERT INTO repo_sites_new (id, repo_did, branch, dir, is_index, created, updated) 1873 + SELECT id, repo_did, branch, dir, is_index, created, updated 1874 + FROM repo_sites WHERE repo_did IS NOT NULL AND repo_did != ''; 1875 + DROP TABLE repo_sites; 1876 + ALTER TABLE repo_sites_new RENAME TO repo_sites; 1877 + 1878 + CREATE TABLE site_deploys_new ( 1879 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1880 + repo_did TEXT NOT NULL, 1881 + branch TEXT NOT NULL, 1882 + dir TEXT NOT NULL DEFAULT '/', 1883 + commit_sha TEXT NOT NULL DEFAULT '', 1884 + status TEXT NOT NULL CHECK (status IN ('success', 'failure')), 1885 + trigger TEXT NOT NULL CHECK (trigger IN ('config_change', 'push')), 1886 + error TEXT NOT NULL DEFAULT '', 1887 + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1888 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1889 + ); 1890 + INSERT INTO site_deploys_new (id, repo_did, branch, dir, commit_sha, status, trigger, error, created_at) 1891 + SELECT id, repo_did, branch, dir, commit_sha, status, trigger, error, created_at 1892 + FROM site_deploys WHERE repo_did IS NOT NULL AND repo_did != ''; 1893 + DROP TABLE site_deploys; 1894 + ALTER TABLE site_deploys_new RENAME TO site_deploys; 1895 + CREATE INDEX idx_site_deploys_repo_did ON site_deploys(repo_did); 1896 + 1897 + CREATE TABLE repo_issue_seqs_new ( 1898 + repo_did TEXT PRIMARY KEY, 1899 + next_issue_id INTEGER NOT NULL DEFAULT 1, 1900 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1901 + ); 1902 + INSERT INTO repo_issue_seqs_new (repo_did, next_issue_id) 1903 + SELECT repo_did, next_issue_id 1904 + FROM repo_issue_seqs WHERE repo_did IS NOT NULL AND repo_did != ''; 1905 + DROP TABLE repo_issue_seqs; 1906 + ALTER TABLE repo_issue_seqs_new RENAME TO repo_issue_seqs; 1907 + 1908 + CREATE TABLE repo_pull_seqs_new ( 1909 + repo_did TEXT PRIMARY KEY, 1910 + next_pull_id INTEGER NOT NULL DEFAULT 1, 1911 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1912 + ); 1913 + INSERT INTO repo_pull_seqs_new (repo_did, next_pull_id) 1914 + SELECT repo_did, next_pull_id 1915 + FROM repo_pull_seqs WHERE repo_did IS NOT NULL AND repo_did != ''; 1916 + DROP TABLE repo_pull_seqs; 1917 + ALTER TABLE repo_pull_seqs_new RENAME TO repo_pull_seqs; 1918 + 1919 + CREATE TABLE repo_languages_new ( 1920 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1921 + repo_did TEXT NOT NULL, 1922 + ref TEXT NOT NULL, 1923 + is_default_ref INTEGER NOT NULL DEFAULT 0, 1924 + language TEXT NOT NULL, 1925 + bytes INTEGER NOT NULL CHECK (bytes >= 0), 1926 + UNIQUE(repo_did, ref, language), 1927 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1928 + ); 1929 + INSERT INTO repo_languages_new (id, repo_did, ref, is_default_ref, language, bytes) 1930 + SELECT id, repo_did, ref, is_default_ref, language, bytes 1931 + FROM repo_languages WHERE repo_did IS NOT NULL AND repo_did != ''; 1932 + DROP TABLE repo_languages; 1933 + ALTER TABLE repo_languages_new RENAME TO repo_languages; 1934 + 1935 + CREATE TABLE repo_labels_new ( 1936 + id INTEGER PRIMARY KEY AUTOINCREMENT, 1937 + repo_did TEXT NOT NULL, 1938 + label_at TEXT NOT NULL, 1939 + UNIQUE(repo_did, label_at), 1940 + FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1941 + ); 1942 + INSERT INTO repo_labels_new (id, repo_did, label_at) 1943 + SELECT id, repo_did, label_at 1944 + FROM repo_labels WHERE repo_did IS NOT NULL AND repo_did != ''; 1945 + DROP TABLE repo_labels; 1946 + ALTER TABLE repo_labels_new RENAME TO repo_labels; 1547 1947 `) 1548 1948 return err 1549 1949 })
+1 -1
appview/models/artifact.go
··· 15 15 Did string 16 16 Rkey string 17 17 18 - RepoAt syntax.ATURI 18 + RepoDid syntax.DID 19 19 Tag plumbing.Hash 20 20 CreatedAt time.Time 21 21
+1 -1
appview/models/collaborator.go
··· 14 14 15 15 // content 16 16 SubjectDid syntax.DID 17 - RepoAt syntax.ATURI 17 + RepoDid syntax.DID 18 18 19 19 // meta 20 20 Created time.Time
+3 -12
appview/models/issue.go
··· 13 13 Id int64 14 14 Did string 15 15 Rkey string 16 - RepoAt syntax.ATURI 16 + RepoDid syntax.DID 17 17 IssueId int 18 18 Created time.Time 19 19 Edited *time.Time ··· 44 44 for i, uri := range i.References { 45 45 references[i] = string(uri) 46 46 } 47 - repoAtStr := i.RepoAt.String() 48 47 rec := tangled.RepoIssue{ 49 - Repo: &repoAtStr, 48 + Repo: string(i.RepoDid), 50 49 Title: i.Title, 51 50 Body: &i.Body, 52 51 Mentions: mentions, 53 52 References: references, 54 53 CreatedAt: i.Created.Format(time.RFC3339), 55 - } 56 - if i.Repo != nil && i.Repo.RepoDid != "" { 57 - rec.RepoDid = &i.Repo.RepoDid 58 54 } 59 55 return rec 60 56 } ··· 166 162 body = *record.Body 167 163 } 168 164 169 - var repoAt syntax.ATURI 170 - if record.Repo != nil { 171 - repoAt = syntax.ATURI(*record.Repo) 172 - } 173 - 174 165 return Issue{ 175 - RepoAt: repoAt, 166 + RepoDid: syntax.DID(record.Repo), 176 167 Did: did, 177 168 Rkey: rkey, 178 169 Created: created,
+2 -4
appview/models/language.go
··· 1 1 package models 2 2 3 - import ( 4 - "github.com/bluesky-social/indigo/atproto/syntax" 5 - ) 3 + import "github.com/bluesky-social/indigo/atproto/syntax" 6 4 7 5 type RepoLanguage struct { 8 6 Id int64 9 - RepoAt syntax.ATURI 7 + RepoDid syntax.DID 10 8 Ref string 11 9 IsDefaultRef bool 12 10 Language string
+23 -48
appview/models/pull.go
··· 61 61 PullId int 62 62 63 63 // at ids 64 - RepoAt syntax.ATURI 64 + RepoDid syntax.DID 65 65 OwnerDid string 66 66 Rkey string 67 67 ··· 97 97 references[i] = string(uri) 98 98 } 99 99 100 - var targetRepoAt, targetRepoDid *string 101 - targetRepoAt = new(string) 102 - *targetRepoAt = p.RepoAt.String() 103 - if p.Repo != nil && p.Repo.RepoDid != "" { 104 - targetRepoDid = new(string) 105 - *targetRepoDid = p.Repo.RepoDid 106 - } 107 - 108 100 rounds := make([]*tangled.RepoPull_Round, len(p.Submissions)) 109 101 for i, submission := range p.Submissions { 110 102 rounds[i] = submission.AsRecord() ··· 123 115 References: references, 124 116 CreatedAt: p.Created.Format(time.RFC3339), 125 117 Target: &tangled.RepoPull_Target{ 126 - Repo: targetRepoAt, 127 - RepoDid: targetRepoDid, 128 - Branch: p.TargetBranch, 118 + Repo: string(p.RepoDid), 119 + Branch: p.TargetBranch, 129 120 }, 130 121 Rounds: rounds, 131 122 Source: p.PullSource.AsRecord(), ··· 151 142 } 152 143 } 153 144 154 - var targetRepoAt syntax.ATURI 145 + var targetRepoDid syntax.DID 155 146 var targetBranch string 156 147 if record.Target != nil { 157 - if record.Target.Repo != nil { 158 - uri, err := syntax.ParseATURI(*record.Target.Repo) 159 - if err != nil { 160 - return nil, fmt.Errorf("invalid target.repo aturi: %w", err) 161 - } 162 - targetRepoAt = uri 148 + did, err := syntax.ParseDID(record.Target.Repo) 149 + if err != nil { 150 + return nil, fmt.Errorf("invalid target.repo did: %w", err) 163 151 } 152 + targetRepoDid = did 164 153 targetBranch = record.Target.Branch 165 154 } 166 155 ··· 171 160 } 172 161 173 162 if record.Source.Repo != nil { 174 - uri, err := syntax.ParseATURI(*record.Source.Repo) 163 + did, err := syntax.ParseDID(*record.Source.Repo) 175 164 if err != nil { 176 - return nil, fmt.Errorf("invalid source.repo aturi: %w", err) 177 - } 178 - pullSource.RepoAt = &uri 179 - } 180 - if record.Source.RepoDid != nil { 181 - did, err := syntax.ParseDID(*record.Source.RepoDid) 182 - if err != nil { 183 - return nil, fmt.Errorf("invalid source.repoDid did: %w", err) 165 + return nil, fmt.Errorf("invalid source.repo did: %w", err) 184 166 } 185 167 pullSource.RepoDid = &did 186 168 } ··· 209 191 } 210 192 211 193 return &Pull{ 212 - RepoAt: targetRepoAt, 194 + RepoDid: targetRepoDid, 213 195 OwnerDid: did, 214 196 Rkey: rkey, 215 197 Title: record.Title, ··· 260 242 261 243 type PullSource struct { 262 244 Branch string 263 - RepoAt *syntax.ATURI 264 245 RepoDid *syntax.DID 265 246 266 247 // optionally populate this for reverse mappings ··· 271 252 if s == nil { 272 253 return nil 273 254 } 274 - var repoAt, repoDid *string 275 - if s.RepoAt != nil { 276 - repoAt = new(string) 277 - *repoAt = s.RepoAt.String() 278 - } 255 + var repo *string 279 256 if s.RepoDid != nil { 280 - repoDid = new(string) 281 - *repoDid = s.RepoDid.String() 257 + r := s.RepoDid.String() 258 + repo = &r 282 259 } 283 260 return &tangled.RepoPull_Source{ 284 - Branch: s.Branch, 285 - Repo: repoAt, 286 - RepoDid: repoDid, 261 + Branch: s.Branch, 262 + Repo: repo, 287 263 } 288 264 } 289 265 ··· 313 289 SubmissionId int 314 290 315 291 // at ids 316 - RepoAt string 292 + RepoDid string 317 293 OwnerDid string 318 294 CommentAt string 319 295 ··· 366 342 367 343 func (p *Pull) IsBranchBased() bool { 368 344 if p.PullSource != nil { 369 - if p.PullSource.RepoAt != nil { 370 - return p.PullSource.RepoAt == &p.RepoAt 371 - } else { 372 - // no repo specified 373 - return true 345 + if p.PullSource.RepoDid != nil { 346 + return *p.PullSource.RepoDid == p.RepoDid 374 347 } 348 + // no repo specified 349 + return true 375 350 } 376 351 return false 377 352 } 378 353 379 354 func (p *Pull) IsForkBased() bool { 380 355 if p.PullSource != nil { 381 - if p.PullSource.RepoAt != nil { 356 + if p.PullSource.RepoDid != nil { 382 357 // make sure repos are different 383 - return p.PullSource.RepoAt != &p.RepoAt 358 + return *p.PullSource.RepoDid != p.RepoDid 384 359 } 385 360 } 386 361 return false
+54 -3
appview/models/repo.go
··· 57 57 58 58 return tangled.Repo{ 59 59 Knot: r.Knot, 60 - Name: r.Name, 60 + Name: r.cosmeticName(), 61 61 Description: description, 62 62 Website: website, 63 63 Topics: r.Topics, ··· 69 69 } 70 70 } 71 71 72 + func (r *Repo) cosmeticName() *string { 73 + if r.Name == "" || r.Name == r.Rkey { 74 + return nil 75 + } 76 + return &r.Name 77 + } 78 + 72 79 func (r Repo) RepoAt() syntax.ATURI { 73 80 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) 74 81 } ··· 77 84 if r.RepoDid != "" { 78 85 return r.RepoDid 79 86 } 80 - p, _ := securejoin.SecureJoin(r.Did, r.Name) 87 + p, _ := securejoin.SecureJoin(r.Did, r.Rkey) 81 88 return p 82 89 } 83 90 ··· 113 120 114 121 type RepoLabel struct { 115 122 Id int64 116 - RepoAt syntax.ATURI 123 + RepoDid syntax.DID 117 124 LabelAt syntax.ATURI 125 + } 126 + 127 + var reservedRepoNames = map[string]struct{}{ 128 + "self": {}, 129 + } 130 + 131 + func ValidateRepoName(name string) error { 132 + if len(name) == 0 { 133 + return fmt.Errorf("Repository name cannot be empty") 134 + } 135 + if len(name) > 100 { 136 + return fmt.Errorf("Repository name must be 100 characters or fewer") 137 + } 138 + 139 + if strings.Contains(name, "/") || strings.Contains(name, "\\") { 140 + return fmt.Errorf("Repository name contains invalid path characters") 141 + } 142 + 143 + if strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 144 + return fmt.Errorf("Repository name contains invalid path sequence") 145 + } 146 + 147 + for _, char := range name { 148 + if !((char >= 'a' && char <= 'z') || 149 + (char >= 'A' && char <= 'Z') || 150 + (char >= '0' && char <= '9') || 151 + char == '-' || char == '_' || char == '.') { 152 + return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 153 + } 154 + } 155 + 156 + if strings.Contains(name, "..") { 157 + return fmt.Errorf("Repository name cannot contain sequential dots") 158 + } 159 + 160 + if _, reserved := reservedRepoNames[strings.ToLower(name)]; reserved { 161 + return fmt.Errorf("Repository name %q is reserved", name) 162 + } 163 + 164 + return nil 165 + } 166 + 167 + func StripGitExt(name string) string { 168 + return strings.TrimSuffix(name, ".git") 118 169 } 119 170 120 171 type RepoGroup struct {
+86
appview/models/repo_test.go
··· 1 + package models 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + ) 7 + 8 + func TestValidateRepoName_ValidRkeys(t *testing.T) { 9 + valid := []string{ 10 + "myrepo", 11 + "MyRepo", 12 + "my-repo", 13 + "my_repo", 14 + "my.repo", 15 + "a", 16 + "repo123", 17 + strings.Repeat("a", 100), 18 + } 19 + for _, name := range valid { 20 + if err := ValidateRepoName(name); err != nil { 21 + t.Errorf("ValidateRepoName(%q) = %v, want nil", name, err) 22 + } 23 + } 24 + } 25 + 26 + func TestValidateRepoName_InvalidRkeys(t *testing.T) { 27 + cases := []struct { 28 + input string 29 + substr string 30 + }{ 31 + {"", "empty"}, 32 + {strings.Repeat("a", 101), "100 characters"}, 33 + {"has space", "alphanumeric"}, 34 + {"has/slash", "invalid path"}, 35 + {"has\\backslash", "invalid path"}, 36 + {".dotprefix", "invalid path"}, 37 + {"dotsuffix.", "invalid path"}, 38 + {"two..dots", "sequential dots"}, 39 + {"../traversal", "invalid path"}, 40 + {"self", "reserved"}, 41 + {"SELF", "reserved"}, 42 + } 43 + for _, tc := range cases { 44 + err := ValidateRepoName(tc.input) 45 + if err == nil { 46 + t.Errorf("ValidateRepoName(%q) = nil, want error containing %q", tc.input, tc.substr) 47 + continue 48 + } 49 + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tc.substr)) { 50 + t.Errorf("ValidateRepoName(%q) = %q, want substring %q", tc.input, err.Error(), tc.substr) 51 + } 52 + } 53 + } 54 + 55 + func TestStripGitExt(t *testing.T) { 56 + cases := []struct{ in, want string }{ 57 + {"repo.git", "repo"}, 58 + {"repo", "repo"}, 59 + {"repo.git.git", "repo.git"}, 60 + {".git", ""}, 61 + } 62 + for _, tc := range cases { 63 + if got := StripGitExt(tc.in); got != tc.want { 64 + t.Errorf("StripGitExt(%q) = %q, want %q", tc.in, got, tc.want) 65 + } 66 + } 67 + } 68 + 69 + func TestCosmeticName_NilWhenMatchesRkey(t *testing.T) { 70 + r := Repo{Name: "myrepo", Rkey: "myrepo"} 71 + rec := r.AsRecord() 72 + if rec.Name != nil { 73 + t.Errorf("cosmeticName should be nil when Name == Rkey, got %q", *rec.Name) 74 + } 75 + } 76 + 77 + func TestCosmeticName_PresentWhenDiffers(t *testing.T) { 78 + r := Repo{Name: "MyRepo", Rkey: "myrepo", Knot: "k"} 79 + rec := r.AsRecord() 80 + if rec.Name == nil { 81 + t.Fatal("cosmeticName should be non-nil when Name != Rkey") 82 + } 83 + if *rec.Name != "MyRepo" { 84 + t.Errorf("cosmeticName = %q, want %q", *rec.Name, "MyRepo") 85 + } 86 + }
+2 -2
appview/models/search.go
··· 5 5 type IssueSearchOptions struct { 6 6 Keywords []string 7 7 Phrases []string 8 - RepoAt string 8 + RepoDid string 9 9 IsOpen *bool 10 10 AuthorDid string 11 11 Labels []string ··· 31 31 type PullSearchOptions struct { 32 32 Keywords []string 33 33 Phrases []string 34 - RepoAt string 34 + RepoDid string 35 35 State *PullState 36 36 AuthorDid string 37 37 Labels []string
+4 -4
appview/models/search_test.go
··· 16 16 want: false, 17 17 }, 18 18 { 19 - name: "non-filter fields only (RepoAt, IsOpen, Page) return false", 20 - opts: IssueSearchOptions{RepoAt: "at://did:plc:abc/repo"}, 19 + name: "non-filter fields only (RepoDid, IsOpen, Page) return false", 20 + opts: IssueSearchOptions{RepoDid: "did:plc:abc"}, 21 21 want: false, 22 22 }, 23 23 { ··· 93 93 want: false, 94 94 }, 95 95 { 96 - name: "non-filter fields only (RepoAt, State, Page) return false", 97 - opts: PullSearchOptions{RepoAt: "at://did:plc:abc/repo"}, 96 + name: "non-filter fields only (RepoDid, State, Page) return false", 97 + opts: PullSearchOptions{RepoDid: "did:plc:abc"}, 98 98 want: false, 99 99 }, 100 100 {
+6 -2
appview/models/site_deploy.go
··· 1 1 package models 2 2 3 - import "time" 3 + import ( 4 + "time" 5 + 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + ) 4 8 5 9 type SiteDeployStatus string 6 10 ··· 29 33 30 34 type SiteDeploy struct { 31 35 Id int64 32 - RepoAt string 36 + RepoDid syntax.DID 33 37 Branch string 34 38 Dir string 35 39 CommitSHA string
+7 -3
appview/models/sites.go
··· 1 1 package models 2 2 3 - import "time" 3 + import ( 4 + "time" 5 + 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + ) 4 8 5 9 type DomainClaim struct { 6 10 ID int64 ··· 11 15 12 16 type RepoSite struct { 13 17 ID int64 14 - RepoAt string 15 - RepoName string // populated when joined with repos table 18 + RepoDid syntax.DID 19 + RepoRkey string // populated when joined with repos table 16 20 Branch string 17 21 Dir string 18 22 IsIndex bool
+11 -5
appview/models/star.go
··· 2 2 3 3 import ( 4 4 "time" 5 + ) 5 6 6 - "github.com/bluesky-social/indigo/atproto/syntax" 7 + type StarSubjectType string 8 + 9 + const ( 10 + StarSubjectRepo StarSubjectType = "repo" 11 + StarSubjectString StarSubjectType = "string" 7 12 ) 8 13 9 14 type Star struct { 10 - Did string 11 - RepoAt syntax.ATURI 12 - Created time.Time 13 - Rkey string 15 + Did string 16 + SubjectType StarSubjectType 17 + Subject string 18 + Created time.Time 19 + Rkey string 14 20 } 15 21 16 22 // RepoStar is used for reverse mapping to repos
+11 -2
appview/models/webhook.go
··· 10 10 type WebhookEvent string 11 11 12 12 const ( 13 - WebhookEventPush WebhookEvent = "push" 13 + WebhookEventPush WebhookEvent = "push" 14 + WebhookEventRepoRenamed WebhookEvent = "repository:renamed" 14 15 ) 15 16 16 17 type Webhook struct { 17 18 Id int64 18 - RepoAt syntax.ATURI 19 + RepoDid syntax.DID 19 20 Url string 20 21 Secret string 21 22 Active bool ··· 72 73 type WebhookUser struct { 73 74 Did string `json:"did"` 74 75 } 76 + 77 + // WebhookRenamePayload represents the payload for a repository:renamed event 78 + type WebhookRenamePayload struct { 79 + OldName string `json:"old_name"` 80 + NewName string `json:"new_name"` 81 + Repository WebhookRepository `json:"repository"` 82 + Sender WebhookUser `json:"sender"` 83 + }
+12
orm/orm.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 + "errors" 6 7 "fmt" 7 8 "log/slog" 8 9 "reflect" 9 10 "strings" 11 + 12 + "github.com/mattn/go-sqlite3" 10 13 ) 14 + 15 + func IsUniqueViolation(err error) bool { 16 + var sqlErr sqlite3.Error 17 + if !errors.As(err, &sqlErr) { 18 + return false 19 + } 20 + return sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique || 21 + sqlErr.ExtendedCode == sqlite3.ErrConstraintPrimaryKey 22 + } 11 23 12 24 type migrationFn = func(*sql.Tx) error 13 25