forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

+373 -660
api/tangled/cbor_gen.go
··· 1202 1202 1203 1203 return nil 1204 1204 } 1205 - func (t *GitRefUpdate_Meta) MarshalCBOR(w io.Writer) error { 1206 - if t == nil { 1207 - _, err := w.Write(cbg.CborNull) 1208 - return err 1209 - } 1210 - 1211 - cw := cbg.NewCborWriter(w) 1212 - fieldCount := 3 1213 - 1214 - if t.LangBreakdown == nil { 1215 - fieldCount-- 1216 - } 1217 - 1218 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 1219 - return err 1220 - } 1221 - 1222 - // t.CommitCount (tangled.GitRefUpdate_Meta_CommitCount) (struct) 1223 - if len("commitCount") > 1000000 { 1224 - return xerrors.Errorf("Value in field \"commitCount\" was too long") 1225 - } 1226 - 1227 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("commitCount"))); err != nil { 1228 - return err 1229 - } 1230 - if _, err := cw.WriteString(string("commitCount")); err != nil { 1231 - return err 1232 - } 1233 - 1234 - if err := t.CommitCount.MarshalCBOR(cw); err != nil { 1235 - return err 1236 - } 1237 - 1238 - // t.IsDefaultRef (bool) (bool) 1239 - if len("isDefaultRef") > 1000000 { 1240 - return xerrors.Errorf("Value in field \"isDefaultRef\" was too long") 1241 - } 1242 - 1243 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("isDefaultRef"))); err != nil { 1244 - return err 1245 - } 1246 - if _, err := cw.WriteString(string("isDefaultRef")); err != nil { 1247 - return err 1248 - } 1249 - 1250 - if err := cbg.WriteBool(w, t.IsDefaultRef); err != nil { 1251 - return err 1252 - } 1253 - 1254 - // t.LangBreakdown (tangled.GitRefUpdate_Meta_LangBreakdown) (struct) 1255 - if t.LangBreakdown != nil { 1256 - 1257 - if len("langBreakdown") > 1000000 { 1258 - return xerrors.Errorf("Value in field \"langBreakdown\" was too long") 1259 - } 1260 - 1261 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("langBreakdown"))); err != nil { 1262 - return err 1263 - } 1264 - if _, err := cw.WriteString(string("langBreakdown")); err != nil { 1265 - return err 1266 - } 1267 - 1268 - if err := t.LangBreakdown.MarshalCBOR(cw); err != nil { 1269 - return err 1270 - } 1271 - } 1272 - return nil 1273 - } 1274 - 1275 - func (t *GitRefUpdate_Meta) UnmarshalCBOR(r io.Reader) (err error) { 1276 - *t = GitRefUpdate_Meta{} 1277 - 1278 - cr := cbg.NewCborReader(r) 1279 - 1280 - maj, extra, err := cr.ReadHeader() 1281 - if err != nil { 1282 - return err 1283 - } 1284 - defer func() { 1285 - if err == io.EOF { 1286 - err = io.ErrUnexpectedEOF 1287 - } 1288 - }() 1289 - 1290 - if maj != cbg.MajMap { 1291 - return fmt.Errorf("cbor input should be of type map") 1292 - } 1293 - 1294 - if extra > cbg.MaxLength { 1295 - return fmt.Errorf("GitRefUpdate_Meta: map struct too large (%d)", extra) 1296 - } 1297 - 1298 - n := extra 1299 - 1300 - nameBuf := make([]byte, 13) 1301 - for i := uint64(0); i < n; i++ { 1302 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1303 - if err != nil { 1304 - return err 1305 - } 1306 - 1307 - if !ok { 1308 - // Field doesn't exist on this type, so ignore it 1309 - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 1310 - return err 1311 - } 1312 - continue 1313 - } 1314 - 1315 - switch string(nameBuf[:nameLen]) { 1316 - // t.CommitCount (tangled.GitRefUpdate_Meta_CommitCount) (struct) 1317 - case "commitCount": 1318 - 1319 - { 1320 - 1321 - b, err := cr.ReadByte() 1322 - if err != nil { 1323 - return err 1324 - } 1325 - if b != cbg.CborNull[0] { 1326 - if err := cr.UnreadByte(); err != nil { 1327 - return err 1328 - } 1329 - t.CommitCount = new(GitRefUpdate_Meta_CommitCount) 1330 - if err := t.CommitCount.UnmarshalCBOR(cr); err != nil { 1331 - return xerrors.Errorf("unmarshaling t.CommitCount pointer: %w", err) 1332 - } 1333 - } 1334 - 1335 - } 1336 - // t.IsDefaultRef (bool) (bool) 1337 - case "isDefaultRef": 1338 - 1339 - maj, extra, err = cr.ReadHeader() 1340 - if err != nil { 1341 - return err 1342 - } 1343 - if maj != cbg.MajOther { 1344 - return fmt.Errorf("booleans must be major type 7") 1345 - } 1346 - switch extra { 1347 - case 20: 1348 - t.IsDefaultRef = false 1349 - case 21: 1350 - t.IsDefaultRef = true 1351 - default: 1352 - return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) 1353 - } 1354 - // t.LangBreakdown (tangled.GitRefUpdate_Meta_LangBreakdown) (struct) 1355 - case "langBreakdown": 1356 - 1357 - { 1358 - 1359 - b, err := cr.ReadByte() 1360 - if err != nil { 1361 - return err 1362 - } 1363 - if b != cbg.CborNull[0] { 1364 - if err := cr.UnreadByte(); err != nil { 1365 - return err 1366 - } 1367 - t.LangBreakdown = new(GitRefUpdate_Meta_LangBreakdown) 1368 - if err := t.LangBreakdown.UnmarshalCBOR(cr); err != nil { 1369 - return xerrors.Errorf("unmarshaling t.LangBreakdown pointer: %w", err) 1370 - } 1371 - } 1372 - 1373 - } 1374 - 1375 - default: 1376 - // Field doesn't exist on this type, so ignore it 1377 - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 1378 - return err 1379 - } 1380 - } 1381 - } 1382 - 1383 - return nil 1384 - } 1385 - func (t *GitRefUpdate_Meta_CommitCount) MarshalCBOR(w io.Writer) error { 1205 + func (t *GitRefUpdate_CommitCountBreakdown) MarshalCBOR(w io.Writer) error { 1386 1206 if t == nil { 1387 1207 _, err := w.Write(cbg.CborNull) 1388 1208 return err ··· 1399 1219 return err 1400 1220 } 1401 1221 1402 - // t.ByEmail ([]*tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem) (slice) 1222 + // t.ByEmail ([]*tangled.GitRefUpdate_IndividualEmailCommitCount) (slice) 1403 1223 if t.ByEmail != nil { 1404 1224 1405 1225 if len("byEmail") > 1000000 { ··· 1430 1250 return nil 1431 1251 } 1432 1252 1433 - func (t *GitRefUpdate_Meta_CommitCount) UnmarshalCBOR(r io.Reader) (err error) { 1434 - *t = GitRefUpdate_Meta_CommitCount{} 1253 + func (t *GitRefUpdate_CommitCountBreakdown) UnmarshalCBOR(r io.Reader) (err error) { 1254 + *t = GitRefUpdate_CommitCountBreakdown{} 1435 1255 1436 1256 cr := cbg.NewCborReader(r) 1437 1257 ··· 1450 1270 } 1451 1271 1452 1272 if extra > cbg.MaxLength { 1453 - return fmt.Errorf("GitRefUpdate_Meta_CommitCount: map struct too large (%d)", extra) 1273 + return fmt.Errorf("GitRefUpdate_CommitCountBreakdown: map struct too large (%d)", extra) 1454 1274 } 1455 1275 1456 1276 n := extra ··· 1471 1291 } 1472 1292 1473 1293 switch string(nameBuf[:nameLen]) { 1474 - // t.ByEmail ([]*tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem) (slice) 1294 + // t.ByEmail ([]*tangled.GitRefUpdate_IndividualEmailCommitCount) (slice) 1475 1295 case "byEmail": 1476 1296 1477 1297 maj, extra, err = cr.ReadHeader() ··· 1488 1308 } 1489 1309 1490 1310 if extra > 0 { 1491 - t.ByEmail = make([]*GitRefUpdate_Meta_CommitCount_ByEmail_Elem, extra) 1311 + t.ByEmail = make([]*GitRefUpdate_IndividualEmailCommitCount, extra) 1492 1312 } 1493 1313 1494 1314 for i := 0; i < int(extra); i++ { ··· 1510 1330 if err := cr.UnreadByte(); err != nil { 1511 1331 return err 1512 1332 } 1513 - t.ByEmail[i] = new(GitRefUpdate_Meta_CommitCount_ByEmail_Elem) 1333 + t.ByEmail[i] = new(GitRefUpdate_IndividualEmailCommitCount) 1514 1334 if err := t.ByEmail[i].UnmarshalCBOR(cr); err != nil { 1515 1335 return xerrors.Errorf("unmarshaling t.ByEmail[i] pointer: %w", err) 1516 1336 } ··· 1531 1351 1532 1352 return nil 1533 1353 } 1534 - func (t *GitRefUpdate_Meta_CommitCount_ByEmail_Elem) MarshalCBOR(w io.Writer) error { 1354 + func (t *GitRefUpdate_IndividualEmailCommitCount) MarshalCBOR(w io.Writer) error { 1535 1355 if t == nil { 1536 1356 _, err := w.Write(cbg.CborNull) 1537 1357 return err ··· 1590 1410 return nil 1591 1411 } 1592 1412 1593 - func (t *GitRefUpdate_Meta_CommitCount_ByEmail_Elem) UnmarshalCBOR(r io.Reader) (err error) { 1594 - *t = GitRefUpdate_Meta_CommitCount_ByEmail_Elem{} 1413 + func (t *GitRefUpdate_IndividualEmailCommitCount) UnmarshalCBOR(r io.Reader) (err error) { 1414 + *t = GitRefUpdate_IndividualEmailCommitCount{} 1595 1415 1596 1416 cr := cbg.NewCborReader(r) 1597 1417 ··· 1610 1430 } 1611 1431 1612 1432 if extra > cbg.MaxLength { 1613 - return fmt.Errorf("GitRefUpdate_Meta_CommitCount_ByEmail_Elem: map struct too large (%d)", extra) 1433 + return fmt.Errorf("GitRefUpdate_IndividualEmailCommitCount: map struct too large (%d)", extra) 1614 1434 } 1615 1435 1616 1436 n := extra ··· 1679 1499 1680 1500 return nil 1681 1501 } 1682 - func (t *GitRefUpdate_Meta_LangBreakdown) MarshalCBOR(w io.Writer) error { 1502 + func (t *GitRefUpdate_LangBreakdown) MarshalCBOR(w io.Writer) error { 1683 1503 if t == nil { 1684 1504 _, err := w.Write(cbg.CborNull) 1685 1505 return err ··· 1696 1516 return err 1697 1517 } 1698 1518 1699 - // t.Inputs ([]*tangled.GitRefUpdate_Pair) (slice) 1519 + // t.Inputs ([]*tangled.GitRefUpdate_IndividualLanguageSize) (slice) 1700 1520 if t.Inputs != nil { 1701 1521 1702 1522 if len("inputs") > 1000000 { ··· 1727 1547 return nil 1728 1548 } 1729 1549 1730 - func (t *GitRefUpdate_Meta_LangBreakdown) UnmarshalCBOR(r io.Reader) (err error) { 1731 - *t = GitRefUpdate_Meta_LangBreakdown{} 1550 + func (t *GitRefUpdate_LangBreakdown) UnmarshalCBOR(r io.Reader) (err error) { 1551 + *t = GitRefUpdate_LangBreakdown{} 1732 1552 1733 1553 cr := cbg.NewCborReader(r) 1734 1554 ··· 1747 1567 } 1748 1568 1749 1569 if extra > cbg.MaxLength { 1750 - return fmt.Errorf("GitRefUpdate_Meta_LangBreakdown: map struct too large (%d)", extra) 1570 + return fmt.Errorf("GitRefUpdate_LangBreakdown: map struct too large (%d)", extra) 1751 1571 } 1752 1572 1753 1573 n := extra ··· 1768 1588 } 1769 1589 1770 1590 switch string(nameBuf[:nameLen]) { 1771 - // t.Inputs ([]*tangled.GitRefUpdate_Pair) (slice) 1591 + // t.Inputs ([]*tangled.GitRefUpdate_IndividualLanguageSize) (slice) 1772 1592 case "inputs": 1773 1593 1774 1594 maj, extra, err = cr.ReadHeader() ··· 1785 1605 } 1786 1606 1787 1607 if extra > 0 { 1788 - t.Inputs = make([]*GitRefUpdate_Pair, extra) 1608 + t.Inputs = make([]*GitRefUpdate_IndividualLanguageSize, extra) 1789 1609 } 1790 1610 1791 1611 for i := 0; i < int(extra); i++ { ··· 1807 1627 if err := cr.UnreadByte(); err != nil { 1808 1628 return err 1809 1629 } 1810 - t.Inputs[i] = new(GitRefUpdate_Pair) 1630 + t.Inputs[i] = new(GitRefUpdate_IndividualLanguageSize) 1811 1631 if err := t.Inputs[i].UnmarshalCBOR(cr); err != nil { 1812 1632 return xerrors.Errorf("unmarshaling t.Inputs[i] pointer: %w", err) 1813 1633 } ··· 1828 1648 1829 1649 return nil 1830 1650 } 1831 - func (t *GitRefUpdate_Pair) MarshalCBOR(w io.Writer) error { 1651 + func (t *GitRefUpdate_IndividualLanguageSize) MarshalCBOR(w io.Writer) error { 1832 1652 if t == nil { 1833 1653 _, err := w.Write(cbg.CborNull) 1834 1654 return err ··· 1888 1708 return nil 1889 1709 } 1890 1710 1891 - func (t *GitRefUpdate_Pair) UnmarshalCBOR(r io.Reader) (err error) { 1892 - *t = GitRefUpdate_Pair{} 1711 + func (t *GitRefUpdate_IndividualLanguageSize) UnmarshalCBOR(r io.Reader) (err error) { 1712 + *t = GitRefUpdate_IndividualLanguageSize{} 1893 1713 1894 1714 cr := cbg.NewCborReader(r) 1895 1715 ··· 1908 1728 } 1909 1729 1910 1730 if extra > cbg.MaxLength { 1911 - return fmt.Errorf("GitRefUpdate_Pair: map struct too large (%d)", extra) 1731 + return fmt.Errorf("GitRefUpdate_IndividualLanguageSize: map struct too large (%d)", extra) 1912 1732 } 1913 1733 1914 1734 n := extra ··· 1965 1785 } 1966 1786 1967 1787 t.Size = int64(extraI) 1788 + } 1789 + 1790 + default: 1791 + // Field doesn't exist on this type, so ignore it 1792 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 1793 + return err 1794 + } 1795 + } 1796 + } 1797 + 1798 + return nil 1799 + } 1800 + func (t *GitRefUpdate_Meta) MarshalCBOR(w io.Writer) error { 1801 + if t == nil { 1802 + _, err := w.Write(cbg.CborNull) 1803 + return err 1804 + } 1805 + 1806 + cw := cbg.NewCborWriter(w) 1807 + fieldCount := 3 1808 + 1809 + if t.LangBreakdown == nil { 1810 + fieldCount-- 1811 + } 1812 + 1813 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 1814 + return err 1815 + } 1816 + 1817 + // t.CommitCount (tangled.GitRefUpdate_CommitCountBreakdown) (struct) 1818 + if len("commitCount") > 1000000 { 1819 + return xerrors.Errorf("Value in field \"commitCount\" was too long") 1820 + } 1821 + 1822 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("commitCount"))); err != nil { 1823 + return err 1824 + } 1825 + if _, err := cw.WriteString(string("commitCount")); err != nil { 1826 + return err 1827 + } 1828 + 1829 + if err := t.CommitCount.MarshalCBOR(cw); err != nil { 1830 + return err 1831 + } 1832 + 1833 + // t.IsDefaultRef (bool) (bool) 1834 + if len("isDefaultRef") > 1000000 { 1835 + return xerrors.Errorf("Value in field \"isDefaultRef\" was too long") 1836 + } 1837 + 1838 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("isDefaultRef"))); err != nil { 1839 + return err 1840 + } 1841 + if _, err := cw.WriteString(string("isDefaultRef")); err != nil { 1842 + return err 1843 + } 1844 + 1845 + if err := cbg.WriteBool(w, t.IsDefaultRef); err != nil { 1846 + return err 1847 + } 1848 + 1849 + // t.LangBreakdown (tangled.GitRefUpdate_LangBreakdown) (struct) 1850 + if t.LangBreakdown != nil { 1851 + 1852 + if len("langBreakdown") > 1000000 { 1853 + return xerrors.Errorf("Value in field \"langBreakdown\" was too long") 1854 + } 1855 + 1856 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("langBreakdown"))); err != nil { 1857 + return err 1858 + } 1859 + if _, err := cw.WriteString(string("langBreakdown")); err != nil { 1860 + return err 1861 + } 1862 + 1863 + if err := t.LangBreakdown.MarshalCBOR(cw); err != nil { 1864 + return err 1865 + } 1866 + } 1867 + return nil 1868 + } 1869 + 1870 + func (t *GitRefUpdate_Meta) UnmarshalCBOR(r io.Reader) (err error) { 1871 + *t = GitRefUpdate_Meta{} 1872 + 1873 + cr := cbg.NewCborReader(r) 1874 + 1875 + maj, extra, err := cr.ReadHeader() 1876 + if err != nil { 1877 + return err 1878 + } 1879 + defer func() { 1880 + if err == io.EOF { 1881 + err = io.ErrUnexpectedEOF 1882 + } 1883 + }() 1884 + 1885 + if maj != cbg.MajMap { 1886 + return fmt.Errorf("cbor input should be of type map") 1887 + } 1888 + 1889 + if extra > cbg.MaxLength { 1890 + return fmt.Errorf("GitRefUpdate_Meta: map struct too large (%d)", extra) 1891 + } 1892 + 1893 + n := extra 1894 + 1895 + nameBuf := make([]byte, 13) 1896 + for i := uint64(0); i < n; i++ { 1897 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1898 + if err != nil { 1899 + return err 1900 + } 1901 + 1902 + if !ok { 1903 + // Field doesn't exist on this type, so ignore it 1904 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 1905 + return err 1906 + } 1907 + continue 1908 + } 1909 + 1910 + switch string(nameBuf[:nameLen]) { 1911 + // t.CommitCount (tangled.GitRefUpdate_CommitCountBreakdown) (struct) 1912 + case "commitCount": 1913 + 1914 + { 1915 + 1916 + b, err := cr.ReadByte() 1917 + if err != nil { 1918 + return err 1919 + } 1920 + if b != cbg.CborNull[0] { 1921 + if err := cr.UnreadByte(); err != nil { 1922 + return err 1923 + } 1924 + t.CommitCount = new(GitRefUpdate_CommitCountBreakdown) 1925 + if err := t.CommitCount.UnmarshalCBOR(cr); err != nil { 1926 + return xerrors.Errorf("unmarshaling t.CommitCount pointer: %w", err) 1927 + } 1928 + } 1929 + 1930 + } 1931 + // t.IsDefaultRef (bool) (bool) 1932 + case "isDefaultRef": 1933 + 1934 + maj, extra, err = cr.ReadHeader() 1935 + if err != nil { 1936 + return err 1937 + } 1938 + if maj != cbg.MajOther { 1939 + return fmt.Errorf("booleans must be major type 7") 1940 + } 1941 + switch extra { 1942 + case 20: 1943 + t.IsDefaultRef = false 1944 + case 21: 1945 + t.IsDefaultRef = true 1946 + default: 1947 + return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) 1948 + } 1949 + // t.LangBreakdown (tangled.GitRefUpdate_LangBreakdown) (struct) 1950 + case "langBreakdown": 1951 + 1952 + { 1953 + 1954 + b, err := cr.ReadByte() 1955 + if err != nil { 1956 + return err 1957 + } 1958 + if b != cbg.CborNull[0] { 1959 + if err := cr.UnreadByte(); err != nil { 1960 + return err 1961 + } 1962 + t.LangBreakdown = new(GitRefUpdate_LangBreakdown) 1963 + if err := t.LangBreakdown.UnmarshalCBOR(cr); err != nil { 1964 + return xerrors.Errorf("unmarshaling t.LangBreakdown pointer: %w", err) 1965 + } 1966 + } 1967 + 1968 1968 } 1969 1969 1970 1970 default: ··· 5642 5642 } 5643 5643 5644 5644 cw := cbg.NewCborWriter(w) 5645 - fieldCount := 7 5645 + fieldCount := 5 5646 5646 5647 5647 if t.Body == nil { 5648 5648 fieldCount-- ··· 5726 5726 return err 5727 5727 } 5728 5728 5729 - // t.Owner (string) (string) 5730 - if len("owner") > 1000000 { 5731 - return xerrors.Errorf("Value in field \"owner\" was too long") 5732 - } 5733 - 5734 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("owner"))); err != nil { 5735 - return err 5736 - } 5737 - if _, err := cw.WriteString(string("owner")); err != nil { 5738 - return err 5739 - } 5740 - 5741 - if len(t.Owner) > 1000000 { 5742 - return xerrors.Errorf("Value in field t.Owner was too long") 5743 - } 5744 - 5745 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Owner))); err != nil { 5746 - return err 5747 - } 5748 - if _, err := cw.WriteString(string(t.Owner)); err != nil { 5749 - return err 5750 - } 5751 - 5752 5729 // t.Title (string) (string) 5753 5730 if len("title") > 1000000 { 5754 5731 return xerrors.Errorf("Value in field \"title\" was too long") ··· 5772 5749 return err 5773 5750 } 5774 5751 5775 - // t.IssueId (int64) (int64) 5776 - if len("issueId") > 1000000 { 5777 - return xerrors.Errorf("Value in field \"issueId\" was too long") 5778 - } 5779 - 5780 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("issueId"))); err != nil { 5781 - return err 5782 - } 5783 - if _, err := cw.WriteString(string("issueId")); err != nil { 5784 - return err 5785 - } 5786 - 5787 - if t.IssueId >= 0 { 5788 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.IssueId)); err != nil { 5789 - return err 5790 - } 5791 - } else { 5792 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.IssueId-1)); err != nil { 5793 - return err 5794 - } 5795 - } 5796 - 5797 5752 // t.CreatedAt (string) (string) 5798 5753 if len("createdAt") > 1000000 { 5799 5754 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 5903 5858 5904 5859 t.LexiconTypeID = string(sval) 5905 5860 } 5906 - // t.Owner (string) (string) 5907 - case "owner": 5908 - 5909 - { 5910 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 5911 - if err != nil { 5912 - return err 5913 - } 5914 - 5915 - t.Owner = string(sval) 5916 - } 5917 5861 // t.Title (string) (string) 5918 5862 case "title": 5919 5863 ··· 5925 5869 5926 5870 t.Title = string(sval) 5927 5871 } 5928 - // t.IssueId (int64) (int64) 5929 - case "issueId": 5930 - { 5931 - maj, extra, err := cr.ReadHeader() 5932 - if err != nil { 5933 - return err 5934 - } 5935 - var extraI int64 5936 - switch maj { 5937 - case cbg.MajUnsignedInt: 5938 - extraI = int64(extra) 5939 - if extraI < 0 { 5940 - return fmt.Errorf("int64 positive overflow") 5941 - } 5942 - case cbg.MajNegativeInt: 5943 - extraI = int64(extra) 5944 - if extraI < 0 { 5945 - return fmt.Errorf("int64 negative overflow") 5946 - } 5947 - extraI = -1 - extraI 5948 - default: 5949 - return fmt.Errorf("wrong type for int64 field: %d", maj) 5950 - } 5951 - 5952 - t.IssueId = int64(extraI) 5953 - } 5954 5872 // t.CreatedAt (string) (string) 5955 5873 case "createdAt": 5956 5874 ··· 5980 5898 } 5981 5899 5982 5900 cw := cbg.NewCborWriter(w) 5983 - fieldCount := 7 5984 - 5985 - if t.CommentId == nil { 5986 - fieldCount-- 5987 - } 5901 + fieldCount := 6 5988 5902 5989 5903 if t.Owner == nil { 5990 5904 fieldCount-- ··· 6127 6041 } 6128 6042 } 6129 6043 6130 - // t.CommentId (int64) (int64) 6131 - if t.CommentId != nil { 6132 - 6133 - if len("commentId") > 1000000 { 6134 - return xerrors.Errorf("Value in field \"commentId\" was too long") 6135 - } 6136 - 6137 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("commentId"))); err != nil { 6138 - return err 6139 - } 6140 - if _, err := cw.WriteString(string("commentId")); err != nil { 6141 - return err 6142 - } 6143 - 6144 - if t.CommentId == nil { 6145 - if _, err := cw.Write(cbg.CborNull); err != nil { 6146 - return err 6147 - } 6148 - } else { 6149 - if *t.CommentId >= 0 { 6150 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.CommentId)); err != nil { 6151 - return err 6152 - } 6153 - } else { 6154 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.CommentId-1)); err != nil { 6155 - return err 6156 - } 6157 - } 6158 - } 6159 - 6160 - } 6161 - 6162 6044 // t.CreatedAt (string) (string) 6163 6045 if len("createdAt") > 1000000 { 6164 6046 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 6300 6182 t.Owner = (*string)(&sval) 6301 6183 } 6302 6184 } 6303 - // t.CommentId (int64) (int64) 6304 - case "commentId": 6305 - { 6306 - 6307 - b, err := cr.ReadByte() 6308 - if err != nil { 6309 - return err 6310 - } 6311 - if b != cbg.CborNull[0] { 6312 - if err := cr.UnreadByte(); err != nil { 6313 - return err 6314 - } 6315 - maj, extra, err := cr.ReadHeader() 6316 - if err != nil { 6317 - return err 6318 - } 6319 - var extraI int64 6320 - switch maj { 6321 - case cbg.MajUnsignedInt: 6322 - extraI = int64(extra) 6323 - if extraI < 0 { 6324 - return fmt.Errorf("int64 positive overflow") 6325 - } 6326 - case cbg.MajNegativeInt: 6327 - extraI = int64(extra) 6328 - if extraI < 0 { 6329 - return fmt.Errorf("int64 negative overflow") 6330 - } 6331 - extraI = -1 - extraI 6332 - default: 6333 - return fmt.Errorf("wrong type for int64 field: %d", maj) 6334 - } 6335 - 6336 - t.CommentId = (*int64)(&extraI) 6337 - } 6338 - } 6339 6185 // t.CreatedAt (string) (string) 6340 6186 case "createdAt": 6341 6187 ··· 6529 6375 } 6530 6376 6531 6377 cw := cbg.NewCborWriter(w) 6532 - fieldCount := 9 6378 + fieldCount := 7 6533 6379 6534 6380 if t.Body == nil { 6535 6381 fieldCount-- ··· 6640 6486 return err 6641 6487 } 6642 6488 6643 - // t.PullId (int64) (int64) 6644 - if len("pullId") > 1000000 { 6645 - return xerrors.Errorf("Value in field \"pullId\" was too long") 6646 - } 6647 - 6648 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pullId"))); err != nil { 6649 - return err 6650 - } 6651 - if _, err := cw.WriteString(string("pullId")); err != nil { 6652 - return err 6653 - } 6654 - 6655 - if t.PullId >= 0 { 6656 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.PullId)); err != nil { 6657 - return err 6658 - } 6659 - } else { 6660 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.PullId-1)); err != nil { 6661 - return err 6662 - } 6663 - } 6664 - 6665 6489 // t.Source (tangled.RepoPull_Source) (struct) 6666 6490 if t.Source != nil { 6667 6491 ··· 6681 6505 } 6682 6506 } 6683 6507 6684 - // t.CreatedAt (string) (string) 6685 - if len("createdAt") > 1000000 { 6686 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 6687 - } 6688 - 6689 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 6690 - return err 6691 - } 6692 - if _, err := cw.WriteString(string("createdAt")); err != nil { 6693 - return err 6694 - } 6695 - 6696 - if len(t.CreatedAt) > 1000000 { 6697 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 6508 + // t.Target (tangled.RepoPull_Target) (struct) 6509 + if len("target") > 1000000 { 6510 + return xerrors.Errorf("Value in field \"target\" was too long") 6698 6511 } 6699 6512 6700 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 6513 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("target"))); err != nil { 6701 6514 return err 6702 6515 } 6703 - if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 6516 + if _, err := cw.WriteString(string("target")); err != nil { 6704 6517 return err 6705 6518 } 6706 6519 6707 - // t.TargetRepo (string) (string) 6708 - if len("targetRepo") > 1000000 { 6709 - return xerrors.Errorf("Value in field \"targetRepo\" was too long") 6710 - } 6711 - 6712 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("targetRepo"))); err != nil { 6713 - return err 6714 - } 6715 - if _, err := cw.WriteString(string("targetRepo")); err != nil { 6520 + if err := t.Target.MarshalCBOR(cw); err != nil { 6716 6521 return err 6717 6522 } 6718 6523 6719 - if len(t.TargetRepo) > 1000000 { 6720 - return xerrors.Errorf("Value in field t.TargetRepo was too long") 6524 + // t.CreatedAt (string) (string) 6525 + if len("createdAt") > 1000000 { 6526 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 6721 6527 } 6722 6528 6723 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.TargetRepo))); err != nil { 6529 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 6724 6530 return err 6725 6531 } 6726 - if _, err := cw.WriteString(string(t.TargetRepo)); err != nil { 6532 + if _, err := cw.WriteString(string("createdAt")); err != nil { 6727 6533 return err 6728 6534 } 6729 6535 6730 - // t.TargetBranch (string) (string) 6731 - if len("targetBranch") > 1000000 { 6732 - return xerrors.Errorf("Value in field \"targetBranch\" was too long") 6733 - } 6734 - 6735 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("targetBranch"))); err != nil { 6736 - return err 6737 - } 6738 - if _, err := cw.WriteString(string("targetBranch")); err != nil { 6739 - return err 6740 - } 6741 - 6742 - if len(t.TargetBranch) > 1000000 { 6743 - return xerrors.Errorf("Value in field t.TargetBranch was too long") 6536 + if len(t.CreatedAt) > 1000000 { 6537 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 6744 6538 } 6745 6539 6746 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.TargetBranch))); err != nil { 6540 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 6747 6541 return err 6748 6542 } 6749 - if _, err := cw.WriteString(string(t.TargetBranch)); err != nil { 6543 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 6750 6544 return err 6751 6545 } 6752 6546 return nil ··· 6777 6571 6778 6572 n := extra 6779 6573 6780 - nameBuf := make([]byte, 12) 6574 + nameBuf := make([]byte, 9) 6781 6575 for i := uint64(0); i < n; i++ { 6782 6576 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 6783 6577 if err != nil { ··· 6847 6641 6848 6642 t.Title = string(sval) 6849 6643 } 6850 - // t.PullId (int64) (int64) 6851 - case "pullId": 6852 - { 6853 - maj, extra, err := cr.ReadHeader() 6854 - if err != nil { 6855 - return err 6856 - } 6857 - var extraI int64 6858 - switch maj { 6859 - case cbg.MajUnsignedInt: 6860 - extraI = int64(extra) 6861 - if extraI < 0 { 6862 - return fmt.Errorf("int64 positive overflow") 6863 - } 6864 - case cbg.MajNegativeInt: 6865 - extraI = int64(extra) 6866 - if extraI < 0 { 6867 - return fmt.Errorf("int64 negative overflow") 6868 - } 6869 - extraI = -1 - extraI 6870 - default: 6871 - return fmt.Errorf("wrong type for int64 field: %d", maj) 6872 - } 6873 - 6874 - t.PullId = int64(extraI) 6875 - } 6876 6644 // t.Source (tangled.RepoPull_Source) (struct) 6877 6645 case "source": 6878 6646 ··· 6893 6661 } 6894 6662 6895 6663 } 6896 - // t.CreatedAt (string) (string) 6897 - case "createdAt": 6664 + // t.Target (tangled.RepoPull_Target) (struct) 6665 + case "target": 6898 6666 6899 6667 { 6900 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 6668 + 6669 + b, err := cr.ReadByte() 6901 6670 if err != nil { 6902 6671 return err 6903 6672 } 6904 - 6905 - t.CreatedAt = string(sval) 6906 - } 6907 - // t.TargetRepo (string) (string) 6908 - case "targetRepo": 6909 - 6910 - { 6911 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 6912 - if err != nil { 6913 - return err 6673 + if b != cbg.CborNull[0] { 6674 + if err := cr.UnreadByte(); err != nil { 6675 + return err 6676 + } 6677 + t.Target = new(RepoPull_Target) 6678 + if err := t.Target.UnmarshalCBOR(cr); err != nil { 6679 + return xerrors.Errorf("unmarshaling t.Target pointer: %w", err) 6680 + } 6914 6681 } 6915 6682 6916 - t.TargetRepo = string(sval) 6917 6683 } 6918 - // t.TargetBranch (string) (string) 6919 - case "targetBranch": 6684 + // t.CreatedAt (string) (string) 6685 + case "createdAt": 6920 6686 6921 6687 { 6922 6688 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 6924 6690 return err 6925 6691 } 6926 6692 6927 - t.TargetBranch = string(sval) 6693 + t.CreatedAt = string(sval) 6928 6694 } 6929 6695 6930 6696 default: ··· 6944 6710 } 6945 6711 6946 6712 cw := cbg.NewCborWriter(w) 6947 - fieldCount := 7 6948 6713 6949 - if t.CommentId == nil { 6950 - fieldCount-- 6951 - } 6952 - 6953 - if t.Owner == nil { 6954 - fieldCount-- 6955 - } 6956 - 6957 - if t.Repo == nil { 6958 - fieldCount-- 6959 - } 6960 - 6961 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 6714 + if _, err := cw.Write([]byte{164}); err != nil { 6962 6715 return err 6963 6716 } 6964 6717 ··· 7008 6761 return err 7009 6762 } 7010 6763 7011 - // t.Repo (string) (string) 7012 - if t.Repo != nil { 7013 - 7014 - if len("repo") > 1000000 { 7015 - return xerrors.Errorf("Value in field \"repo\" was too long") 7016 - } 7017 - 7018 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7019 - return err 7020 - } 7021 - if _, err := cw.WriteString(string("repo")); err != nil { 7022 - return err 7023 - } 7024 - 7025 - if t.Repo == nil { 7026 - if _, err := cw.Write(cbg.CborNull); err != nil { 7027 - return err 7028 - } 7029 - } else { 7030 - if len(*t.Repo) > 1000000 { 7031 - return xerrors.Errorf("Value in field t.Repo was too long") 7032 - } 7033 - 7034 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 7035 - return err 7036 - } 7037 - if _, err := cw.WriteString(string(*t.Repo)); err != nil { 7038 - return err 7039 - } 7040 - } 7041 - } 7042 - 7043 6764 // t.LexiconTypeID (string) (string) 7044 6765 if len("$type") > 1000000 { 7045 6766 return xerrors.Errorf("Value in field \"$type\" was too long") ··· 7059 6780 return err 7060 6781 } 7061 6782 7062 - // t.Owner (string) (string) 7063 - if t.Owner != nil { 7064 - 7065 - if len("owner") > 1000000 { 7066 - return xerrors.Errorf("Value in field \"owner\" was too long") 7067 - } 7068 - 7069 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("owner"))); err != nil { 7070 - return err 7071 - } 7072 - if _, err := cw.WriteString(string("owner")); err != nil { 7073 - return err 7074 - } 7075 - 7076 - if t.Owner == nil { 7077 - if _, err := cw.Write(cbg.CborNull); err != nil { 7078 - return err 7079 - } 7080 - } else { 7081 - if len(*t.Owner) > 1000000 { 7082 - return xerrors.Errorf("Value in field t.Owner was too long") 7083 - } 7084 - 7085 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Owner))); err != nil { 7086 - return err 7087 - } 7088 - if _, err := cw.WriteString(string(*t.Owner)); err != nil { 7089 - return err 7090 - } 7091 - } 7092 - } 7093 - 7094 - // t.CommentId (int64) (int64) 7095 - if t.CommentId != nil { 7096 - 7097 - if len("commentId") > 1000000 { 7098 - return xerrors.Errorf("Value in field \"commentId\" was too long") 7099 - } 7100 - 7101 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("commentId"))); err != nil { 7102 - return err 7103 - } 7104 - if _, err := cw.WriteString(string("commentId")); err != nil { 7105 - return err 7106 - } 7107 - 7108 - if t.CommentId == nil { 7109 - if _, err := cw.Write(cbg.CborNull); err != nil { 7110 - return err 7111 - } 7112 - } else { 7113 - if *t.CommentId >= 0 { 7114 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.CommentId)); err != nil { 7115 - return err 7116 - } 7117 - } else { 7118 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.CommentId-1)); err != nil { 7119 - return err 7120 - } 7121 - } 7122 - } 7123 - 7124 - } 7125 - 7126 6783 // t.CreatedAt (string) (string) 7127 6784 if len("createdAt") > 1000000 { 7128 6785 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 7211 6868 7212 6869 t.Pull = string(sval) 7213 6870 } 7214 - // t.Repo (string) (string) 7215 - case "repo": 7216 - 7217 - { 7218 - b, err := cr.ReadByte() 7219 - if err != nil { 7220 - return err 7221 - } 7222 - if b != cbg.CborNull[0] { 7223 - if err := cr.UnreadByte(); err != nil { 7224 - return err 7225 - } 7226 - 7227 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7228 - if err != nil { 7229 - return err 7230 - } 7231 - 7232 - t.Repo = (*string)(&sval) 7233 - } 7234 - } 7235 6871 // t.LexiconTypeID (string) (string) 7236 6872 case "$type": 7237 6873 ··· 7242 6878 } 7243 6879 7244 6880 t.LexiconTypeID = string(sval) 7245 - } 7246 - // t.Owner (string) (string) 7247 - case "owner": 7248 - 7249 - { 7250 - b, err := cr.ReadByte() 7251 - if err != nil { 7252 - return err 7253 - } 7254 - if b != cbg.CborNull[0] { 7255 - if err := cr.UnreadByte(); err != nil { 7256 - return err 7257 - } 7258 - 7259 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7260 - if err != nil { 7261 - return err 7262 - } 7263 - 7264 - t.Owner = (*string)(&sval) 7265 - } 7266 - } 7267 - // t.CommentId (int64) (int64) 7268 - case "commentId": 7269 - { 7270 - 7271 - b, err := cr.ReadByte() 7272 - if err != nil { 7273 - return err 7274 - } 7275 - if b != cbg.CborNull[0] { 7276 - if err := cr.UnreadByte(); err != nil { 7277 - return err 7278 - } 7279 - maj, extra, err := cr.ReadHeader() 7280 - if err != nil { 7281 - return err 7282 - } 7283 - var extraI int64 7284 - switch maj { 7285 - case cbg.MajUnsignedInt: 7286 - extraI = int64(extra) 7287 - if extraI < 0 { 7288 - return fmt.Errorf("int64 positive overflow") 7289 - } 7290 - case cbg.MajNegativeInt: 7291 - extraI = int64(extra) 7292 - if extraI < 0 { 7293 - return fmt.Errorf("int64 negative overflow") 7294 - } 7295 - extraI = -1 - extraI 7296 - default: 7297 - return fmt.Errorf("wrong type for int64 field: %d", maj) 7298 - } 7299 - 7300 - t.CommentId = (*int64)(&extraI) 7301 - } 7302 6881 } 7303 6882 // t.CreatedAt (string) (string) 7304 6883 case "createdAt": ··· 7666 7245 } 7667 7246 7668 7247 t.Status = string(sval) 7248 + } 7249 + 7250 + default: 7251 + // Field doesn't exist on this type, so ignore it 7252 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 7253 + return err 7254 + } 7255 + } 7256 + } 7257 + 7258 + return nil 7259 + } 7260 + func (t *RepoPull_Target) MarshalCBOR(w io.Writer) error { 7261 + if t == nil { 7262 + _, err := w.Write(cbg.CborNull) 7263 + return err 7264 + } 7265 + 7266 + cw := cbg.NewCborWriter(w) 7267 + 7268 + if _, err := cw.Write([]byte{162}); err != nil { 7269 + return err 7270 + } 7271 + 7272 + // t.Repo (string) (string) 7273 + if len("repo") > 1000000 { 7274 + return xerrors.Errorf("Value in field \"repo\" was too long") 7275 + } 7276 + 7277 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7278 + return err 7279 + } 7280 + if _, err := cw.WriteString(string("repo")); err != nil { 7281 + return err 7282 + } 7283 + 7284 + if len(t.Repo) > 1000000 { 7285 + return xerrors.Errorf("Value in field t.Repo was too long") 7286 + } 7287 + 7288 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 7289 + return err 7290 + } 7291 + if _, err := cw.WriteString(string(t.Repo)); err != nil { 7292 + return err 7293 + } 7294 + 7295 + // t.Branch (string) (string) 7296 + if len("branch") > 1000000 { 7297 + return xerrors.Errorf("Value in field \"branch\" was too long") 7298 + } 7299 + 7300 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("branch"))); err != nil { 7301 + return err 7302 + } 7303 + if _, err := cw.WriteString(string("branch")); err != nil { 7304 + return err 7305 + } 7306 + 7307 + if len(t.Branch) > 1000000 { 7308 + return xerrors.Errorf("Value in field t.Branch was too long") 7309 + } 7310 + 7311 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Branch))); err != nil { 7312 + return err 7313 + } 7314 + if _, err := cw.WriteString(string(t.Branch)); err != nil { 7315 + return err 7316 + } 7317 + return nil 7318 + } 7319 + 7320 + func (t *RepoPull_Target) UnmarshalCBOR(r io.Reader) (err error) { 7321 + *t = RepoPull_Target{} 7322 + 7323 + cr := cbg.NewCborReader(r) 7324 + 7325 + maj, extra, err := cr.ReadHeader() 7326 + if err != nil { 7327 + return err 7328 + } 7329 + defer func() { 7330 + if err == io.EOF { 7331 + err = io.ErrUnexpectedEOF 7332 + } 7333 + }() 7334 + 7335 + if maj != cbg.MajMap { 7336 + return fmt.Errorf("cbor input should be of type map") 7337 + } 7338 + 7339 + if extra > cbg.MaxLength { 7340 + return fmt.Errorf("RepoPull_Target: map struct too large (%d)", extra) 7341 + } 7342 + 7343 + n := extra 7344 + 7345 + nameBuf := make([]byte, 6) 7346 + for i := uint64(0); i < n; i++ { 7347 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 7348 + if err != nil { 7349 + return err 7350 + } 7351 + 7352 + if !ok { 7353 + // Field doesn't exist on this type, so ignore it 7354 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 7355 + return err 7356 + } 7357 + continue 7358 + } 7359 + 7360 + switch string(nameBuf[:nameLen]) { 7361 + // t.Repo (string) (string) 7362 + case "repo": 7363 + 7364 + { 7365 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7366 + if err != nil { 7367 + return err 7368 + } 7369 + 7370 + t.Repo = string(sval) 7371 + } 7372 + // t.Branch (string) (string) 7373 + case "branch": 7374 + 7375 + { 7376 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7377 + if err != nil { 7378 + return err 7379 + } 7380 + 7381 + t.Branch = string(sval) 7669 7382 } 7670 7383 7671 7384 default:
+19 -15
api/tangled/gitrefUpdate.go
··· 33 33 RepoName string `json:"repoName" cborgen:"repoName"` 34 34 } 35 35 36 - type GitRefUpdate_Meta struct { 37 - CommitCount *GitRefUpdate_Meta_CommitCount `json:"commitCount" cborgen:"commitCount"` 38 - IsDefaultRef bool `json:"isDefaultRef" cborgen:"isDefaultRef"` 39 - LangBreakdown *GitRefUpdate_Meta_LangBreakdown `json:"langBreakdown,omitempty" cborgen:"langBreakdown,omitempty"` 36 + // GitRefUpdate_CommitCountBreakdown is a "commitCountBreakdown" in the sh.tangled.git.refUpdate schema. 37 + type GitRefUpdate_CommitCountBreakdown struct { 38 + ByEmail []*GitRefUpdate_IndividualEmailCommitCount `json:"byEmail,omitempty" cborgen:"byEmail,omitempty"` 40 39 } 41 40 42 - type GitRefUpdate_Meta_CommitCount struct { 43 - ByEmail []*GitRefUpdate_Meta_CommitCount_ByEmail_Elem `json:"byEmail,omitempty" cborgen:"byEmail,omitempty"` 44 - } 45 - 46 - type GitRefUpdate_Meta_CommitCount_ByEmail_Elem struct { 41 + // GitRefUpdate_IndividualEmailCommitCount is a "individualEmailCommitCount" in the sh.tangled.git.refUpdate schema. 42 + type GitRefUpdate_IndividualEmailCommitCount struct { 47 43 Count int64 `json:"count" cborgen:"count"` 48 44 Email string `json:"email" cborgen:"email"` 49 45 } 50 46 51 - type GitRefUpdate_Meta_LangBreakdown struct { 52 - Inputs []*GitRefUpdate_Pair `json:"inputs,omitempty" cborgen:"inputs,omitempty"` 53 - } 54 - 55 - // GitRefUpdate_Pair is a "pair" in the sh.tangled.git.refUpdate schema. 56 - type GitRefUpdate_Pair struct { 47 + // GitRefUpdate_IndividualLanguageSize is a "individualLanguageSize" in the sh.tangled.git.refUpdate schema. 48 + type GitRefUpdate_IndividualLanguageSize struct { 57 49 Lang string `json:"lang" cborgen:"lang"` 58 50 Size int64 `json:"size" cborgen:"size"` 59 51 } 52 + 53 + // GitRefUpdate_LangBreakdown is a "langBreakdown" in the sh.tangled.git.refUpdate schema. 54 + type GitRefUpdate_LangBreakdown struct { 55 + Inputs []*GitRefUpdate_IndividualLanguageSize `json:"inputs,omitempty" cborgen:"inputs,omitempty"` 56 + } 57 + 58 + // GitRefUpdate_Meta is a "meta" in the sh.tangled.git.refUpdate schema. 59 + type GitRefUpdate_Meta struct { 60 + CommitCount *GitRefUpdate_CommitCountBreakdown `json:"commitCount" cborgen:"commitCount"` 61 + IsDefaultRef bool `json:"isDefaultRef" cborgen:"isDefaultRef"` 62 + LangBreakdown *GitRefUpdate_LangBreakdown `json:"langBreakdown,omitempty" cborgen:"langBreakdown,omitempty"` 63 + }
-1
api/tangled/issuecomment.go
··· 19 19 type RepoIssueComment struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"` 21 21 Body string `json:"body" cborgen:"body"` 22 - CommentId *int64 `json:"commentId,omitempty" cborgen:"commentId,omitempty"` 23 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 24 23 Issue string `json:"issue" cborgen:"issue"` 25 24 Owner *string `json:"owner,omitempty" cborgen:"owner,omitempty"`
+4 -7
api/tangled/pullcomment.go
··· 17 17 } // 18 18 // RECORDTYPE: RepoPullComment 19 19 type RepoPullComment struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.comment" cborgen:"$type,const=sh.tangled.repo.pull.comment"` 21 - Body string `json:"body" cborgen:"body"` 22 - CommentId *int64 `json:"commentId,omitempty" cborgen:"commentId,omitempty"` 23 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` 24 - Owner *string `json:"owner,omitempty" cborgen:"owner,omitempty"` 25 - Pull string `json:"pull" cborgen:"pull"` 26 - Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 20 + LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.comment" cborgen:"$type,const=sh.tangled.repo.pull.comment"` 21 + Body string `json:"body" cborgen:"body"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 + Pull string `json:"pull" cborgen:"pull"` 27 24 }
-2
api/tangled/repoissue.go
··· 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"` 21 21 Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 - IssueId int64 `json:"issueId" cborgen:"issueId"` 24 - Owner string `json:"owner" cborgen:"owner"` 25 23 Repo string `json:"repo" cborgen:"repo"` 26 24 Title string `json:"title" cborgen:"title"` 27 25 }
+7 -3
api/tangled/repopull.go
··· 21 21 Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 23 Patch string `json:"patch" cborgen:"patch"` 24 - PullId int64 `json:"pullId" cborgen:"pullId"` 25 24 Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 26 - TargetBranch string `json:"targetBranch" cborgen:"targetBranch"` 27 - TargetRepo string `json:"targetRepo" cborgen:"targetRepo"` 25 + Target *RepoPull_Target `json:"target" cborgen:"target"` 28 26 Title string `json:"title" cborgen:"title"` 29 27 } 30 28 ··· 34 32 Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 35 33 Sha string `json:"sha" cborgen:"sha"` 36 34 } 35 + 36 + // RepoPull_Target is a "target" in the sh.tangled.repo.pull schema. 37 + type RepoPull_Target struct { 38 + Branch string `json:"branch" cborgen:"branch"` 39 + Repo string `json:"repo" cborgen:"repo"` 40 + }
+1 -1
appview/config/config.go
··· 21 21 AppPassword string `env:"APP_PASSWORD"` 22 22 23 23 // uhhhh this is because knot1 is under icy's did 24 - TmpAltAppPassword string `env:"ALT_APP_PASSWORD, required"` 24 + TmpAltAppPassword string `env:"ALT_APP_PASSWORD"` 25 25 } 26 26 27 27 type OAuthConfig struct {
+4 -4
appview/db/follow.go
··· 56 56 } 57 57 58 58 type FollowStats struct { 59 - Followers int 60 - Following int 59 + Followers int64 60 + Following int64 61 61 } 62 62 63 63 func GetFollowerFollowingCount(e Execer, did string) (FollowStats, error) { 64 - followers, following := 0, 0 64 + var followers, following int64 65 65 err := e.QueryRow( 66 66 `SELECT 67 67 COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers, ··· 122 122 123 123 for rows.Next() { 124 124 var did string 125 - var followers, following int 125 + var followers, following int64 126 126 if err := rows.Scan(&did, &followers, &following); err != nil { 127 127 return nil, err 128 128 }
+105
appview/db/issues.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "fmt" 6 + mathrand "math/rand/v2" 6 7 "strings" 7 8 "time" 8 9 ··· 47 48 48 49 func (i *Issue) AtUri() syntax.ATURI { 49 50 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.OwnerDid, tangled.RepoIssueNSID, i.Rkey)) 51 + } 52 + 53 + func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue { 54 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 55 + if err != nil { 56 + created = time.Now() 57 + } 58 + 59 + body := "" 60 + if record.Body != nil { 61 + body = *record.Body 62 + } 63 + 64 + return Issue{ 65 + RepoAt: syntax.ATURI(record.Repo), 66 + OwnerDid: did, 67 + Rkey: rkey, 68 + Created: created, 69 + Title: record.Title, 70 + Body: body, 71 + Open: true, // new issues are open by default 72 + } 73 + } 74 + 75 + func ResolveIssueFromAtUri(e Execer, issueUri syntax.ATURI) (syntax.ATURI, int, error) { 76 + ownerDid := issueUri.Authority().String() 77 + issueRkey := issueUri.RecordKey().String() 78 + 79 + var repoAt string 80 + var issueId int 81 + 82 + query := `select repo_at, issue_id from issues where owner_did = ? and rkey = ?` 83 + err := e.QueryRow(query, ownerDid, issueRkey).Scan(&repoAt, &issueId) 84 + if err != nil { 85 + return "", 0, err 86 + } 87 + 88 + return syntax.ATURI(repoAt), issueId, nil 89 + } 90 + 91 + func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (Comment, error) { 92 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 93 + if err != nil { 94 + created = time.Now() 95 + } 96 + 97 + ownerDid := did 98 + if record.Owner != nil { 99 + ownerDid = *record.Owner 100 + } 101 + 102 + issueUri, err := syntax.ParseATURI(record.Issue) 103 + if err != nil { 104 + return Comment{}, err 105 + } 106 + 107 + repoAt, issueId, err := ResolveIssueFromAtUri(e, issueUri) 108 + if err != nil { 109 + return Comment{}, err 110 + } 111 + 112 + comment := Comment{ 113 + OwnerDid: ownerDid, 114 + RepoAt: repoAt, 115 + Rkey: rkey, 116 + Body: record.Body, 117 + Issue: issueId, 118 + CommentId: mathrand.IntN(1000000), 119 + Created: &created, 120 + } 121 + 122 + return comment, nil 50 123 } 51 124 52 125 func NewIssue(tx *sql.Tx, issue *Issue) error { ··· 550 623 deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 551 624 where repo_at = ? and issue_id = ? and comment_id = ? 552 625 `, repoAt, issueId, commentId) 626 + return err 627 + } 628 + 629 + func UpdateCommentByRkey(e Execer, ownerDid, rkey, newBody string) error { 630 + _, err := e.Exec( 631 + ` 632 + update comments 633 + set body = ?, 634 + edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 635 + where owner_did = ? and rkey = ? 636 + `, newBody, ownerDid, rkey) 637 + return err 638 + } 639 + 640 + func DeleteCommentByRkey(e Execer, ownerDid, rkey string) error { 641 + _, err := e.Exec( 642 + ` 643 + update comments 644 + set body = "", 645 + deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 646 + where owner_did = ? and rkey = ? 647 + `, ownerDid, rkey) 648 + return err 649 + } 650 + 651 + func UpdateIssueByRkey(e Execer, ownerDid, rkey, title, body string) error { 652 + _, err := e.Exec(`update issues set title = ?, body = ? where owner_did = ? and rkey = ?`, title, body, ownerDid, rkey) 653 + return err 654 + } 655 + 656 + func DeleteIssueByRkey(e Execer, ownerDid, rkey string) error { 657 + _, err := e.Exec(`delete from issues where owner_did = ? and rkey = ?`, ownerDid, rkey) 553 658 return err 554 659 } 555 660
+14
appview/db/profile.go
··· 22 22 ByMonth []ByMonth 23 23 } 24 24 25 + func (p *ProfileTimeline) IsEmpty() bool { 26 + if p == nil { 27 + return true 28 + } 29 + 30 + for _, m := range p.ByMonth { 31 + if !m.IsEmpty() { 32 + return false 33 + } 34 + } 35 + 36 + return true 37 + } 38 + 25 39 type ByMonth struct { 26 40 RepoEvents []RepoEvent 27 41 IssueEvents IssueEvents
+9 -8
appview/db/pulls.go
··· 91 91 } 92 92 93 93 record := tangled.RepoPull{ 94 - Title: p.Title, 95 - Body: &p.Body, 96 - CreatedAt: p.Created.Format(time.RFC3339), 97 - PullId: int64(p.PullId), 98 - TargetRepo: p.RepoAt.String(), 99 - TargetBranch: p.TargetBranch, 100 - Patch: p.LatestPatch(), 101 - Source: source, 94 + Title: p.Title, 95 + Body: &p.Body, 96 + CreatedAt: p.Created.Format(time.RFC3339), 97 + Target: &tangled.RepoPull_Target{ 98 + Repo: p.RepoAt.String(), 99 + Branch: p.TargetBranch, 100 + }, 101 + Patch: p.LatestPatch(), 102 + Source: source, 102 103 } 103 104 return record 104 105 }
+4 -4
appview/db/punchcard.go
··· 29 29 Punches []Punch 30 30 } 31 31 32 - func MakePunchcard(e Execer, filters ...filter) (Punchcard, error) { 33 - punchcard := Punchcard{} 32 + func MakePunchcard(e Execer, filters ...filter) (*Punchcard, error) { 33 + punchcard := &Punchcard{} 34 34 now := time.Now() 35 35 startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 36 36 endOfYear := time.Date(now.Year(), 12, 31, 0, 0, 0, 0, time.UTC) ··· 63 63 64 64 rows, err := e.Query(query, args...) 65 65 if err != nil { 66 - return punchcard, err 66 + return nil, err 67 67 } 68 68 defer rows.Close() 69 69 ··· 72 72 var date string 73 73 var count sql.NullInt64 74 74 if err := rows.Scan(&date, &count); err != nil { 75 - return punchcard, err 75 + return nil, err 76 76 } 77 77 78 78 punch.Date, err = time.Parse(time.DateOnly, date)
+35 -71
appview/db/repos.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 + "errors" 5 6 "fmt" 6 7 "log" 7 8 "slices" ··· 36 37 func (r Repo) DidSlashRepo() string { 37 38 p, _ := securejoin.SecureJoin(r.Did, r.Name) 38 39 return p 39 - } 40 - 41 - func GetAllRepos(e Execer, limit int) ([]Repo, error) { 42 - var repos []Repo 43 - 44 - rows, err := e.Query( 45 - `select did, name, knot, rkey, description, created, source 46 - from repos 47 - order by created desc 48 - limit ? 49 - `, 50 - limit, 51 - ) 52 - if err != nil { 53 - return nil, err 54 - } 55 - defer rows.Close() 56 - 57 - for rows.Next() { 58 - var repo Repo 59 - err := scanRepo( 60 - rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source, 61 - ) 62 - if err != nil { 63 - return nil, err 64 - } 65 - repos = append(repos, repo) 66 - } 67 - 68 - if err := rows.Err(); err != nil { 69 - return nil, err 70 - } 71 - 72 - return repos, nil 73 40 } 74 41 75 42 func GetRepos(e Execer, limit int, filters ...filter) ([]Repo, error) { ··· 310 277 311 278 slices.SortFunc(repos, func(a, b Repo) int { 312 279 if a.Created.After(b.Created) { 313 - return 1 280 + return -1 314 281 } 315 - return -1 282 + return 1 316 283 }) 317 284 318 285 return repos, nil 319 286 } 320 287 288 + func CountRepos(e Execer, filters ...filter) (int64, error) { 289 + var conditions []string 290 + var args []any 291 + for _, filter := range filters { 292 + conditions = append(conditions, filter.Condition()) 293 + args = append(args, filter.Arg()...) 294 + } 295 + 296 + whereClause := "" 297 + if conditions != nil { 298 + whereClause = " where " + strings.Join(conditions, " and ") 299 + } 300 + 301 + repoQuery := fmt.Sprintf(`select count(1) from repos %s`, whereClause) 302 + var count int64 303 + err := e.QueryRow(repoQuery, args...).Scan(&count) 304 + 305 + if !errors.Is(err, sql.ErrNoRows) && err != nil { 306 + return 0, err 307 + } 308 + 309 + return count, nil 310 + } 311 + 321 312 func GetAllReposByDid(e Execer, did string) ([]Repo, error) { 322 313 var repos []Repo 323 314 ··· 466 457 var repos []Repo 467 458 468 459 rows, err := e.Query( 469 - `select did, name, knot, rkey, description, created, source 470 - from repos 471 - where did = ? and source is not null and source != '' 472 - order by created desc`, 473 - did, 460 + `select distinct r.did, r.name, r.knot, r.rkey, r.description, r.created, r.source 461 + from repos r 462 + left join collaborators c on r.at_uri = c.repo_at 463 + where (r.did = ? or c.subject_did = ?) 464 + and r.source is not null 465 + and r.source != '' 466 + order by r.created desc`, 467 + did, did, 474 468 ) 475 469 if err != nil { 476 470 return nil, err ··· 567 561 IssueCount IssueCount 568 562 PullCount PullCount 569 563 } 570 - 571 - func scanRepo(rows *sql.Rows, did, name, knot, rkey, description *string, created *time.Time, source *string) error { 572 - var createdAt string 573 - var nullableDescription sql.NullString 574 - var nullableSource sql.NullString 575 - if err := rows.Scan(did, name, knot, rkey, &nullableDescription, &createdAt, &nullableSource); err != nil { 576 - return err 577 - } 578 - 579 - if nullableDescription.Valid { 580 - *description = nullableDescription.String 581 - } else { 582 - *description = "" 583 - } 584 - 585 - createdAtTime, err := time.Parse(time.RFC3339, createdAt) 586 - if err != nil { 587 - *created = time.Now() 588 - } else { 589 - *created = createdAtTime 590 - } 591 - 592 - if nullableSource.Valid { 593 - *source = nullableSource.String 594 - } else { 595 - *source = "" 596 - } 597 - 598 - return nil 599 - }
+26
appview/db/star.go
··· 1 1 package db 2 2 3 3 import ( 4 + "database/sql" 5 + "errors" 4 6 "fmt" 5 7 "log" 6 8 "strings" ··· 181 183 } 182 184 183 185 return stars, nil 186 + } 187 + 188 + func CountStars(e Execer, filters ...filter) (int64, error) { 189 + var conditions []string 190 + var args []any 191 + for _, filter := range filters { 192 + conditions = append(conditions, filter.Condition()) 193 + args = append(args, filter.Arg()...) 194 + } 195 + 196 + whereClause := "" 197 + if conditions != nil { 198 + whereClause = " where " + strings.Join(conditions, " and ") 199 + } 200 + 201 + repoQuery := fmt.Sprintf(`select count(1) from stars %s`, whereClause) 202 + var count int64 203 + err := e.QueryRow(repoQuery, args...).Scan(&count) 204 + 205 + if !errors.Is(err, sql.ErrNoRows) && err != nil { 206 + return 0, err 207 + } 208 + 209 + return count, nil 184 210 } 185 211 186 212 func GetAllStars(e Execer, limit int) ([]Star, error) {
+24
appview/db/strings.go
··· 206 206 return all, nil 207 207 } 208 208 209 + func CountStrings(e Execer, filters ...filter) (int64, error) { 210 + var conditions []string 211 + var args []any 212 + for _, filter := range filters { 213 + conditions = append(conditions, filter.Condition()) 214 + args = append(args, filter.Arg()...) 215 + } 216 + 217 + whereClause := "" 218 + if conditions != nil { 219 + whereClause = " where " + strings.Join(conditions, " and ") 220 + } 221 + 222 + repoQuery := fmt.Sprintf(`select count(1) from strings %s`, whereClause) 223 + var count int64 224 + err := e.QueryRow(repoQuery, args...).Scan(&count) 225 + 226 + if !errors.Is(err, sql.ErrNoRows) && err != nil { 227 + return 0, err 228 + } 229 + 230 + return count, nil 231 + } 232 + 209 233 func DeleteString(e Execer, filters ...filter) error { 210 234 var conditions []string 211 235 var args []any
+178 -6
appview/ingester.go
··· 5 5 "encoding/json" 6 6 "fmt" 7 7 "log/slog" 8 + "strings" 8 9 "time" 9 10 10 11 "github.com/bluesky-social/indigo/atproto/syntax" ··· 14 15 "tangled.sh/tangled.sh/core/api/tangled" 15 16 "tangled.sh/tangled.sh/core/appview/config" 16 17 "tangled.sh/tangled.sh/core/appview/db" 18 + "tangled.sh/tangled.sh/core/appview/pages/markup" 17 19 "tangled.sh/tangled.sh/core/appview/serververify" 18 20 "tangled.sh/tangled.sh/core/idresolver" 19 21 "tangled.sh/tangled.sh/core/rbac" ··· 61 63 case tangled.ActorProfileNSID: 62 64 err = i.ingestProfile(e) 63 65 case tangled.SpindleMemberNSID: 64 - err = i.ingestSpindleMember(e) 66 + err = i.ingestSpindleMember(ctx, e) 65 67 case tangled.SpindleNSID: 66 - err = i.ingestSpindle(e) 68 + err = i.ingestSpindle(ctx, e) 67 69 case tangled.KnotMemberNSID: 68 70 err = i.ingestKnotMember(e) 69 71 case tangled.KnotNSID: 70 72 err = i.ingestKnot(e) 71 73 case tangled.StringNSID: 72 74 err = i.ingestString(e) 75 + case tangled.RepoIssueNSID: 76 + err = i.ingestIssue(ctx, e) 77 + case tangled.RepoIssueCommentNSID: 78 + err = i.ingestIssueComment(e) 73 79 } 74 80 l = i.Logger.With("nsid", e.Commit.Collection) 75 81 } ··· 340 346 return nil 341 347 } 342 348 343 - func (i *Ingester) ingestSpindleMember(e *models.Event) error { 349 + func (i *Ingester) ingestSpindleMember(ctx context.Context, e *models.Event) error { 344 350 did := e.Did 345 351 var err error 346 352 ··· 363 369 return fmt.Errorf("failed to enforce permissions: %w", err) 364 370 } 365 371 366 - memberId, err := i.IdResolver.ResolveIdent(context.Background(), record.Subject) 372 + memberId, err := i.IdResolver.ResolveIdent(ctx, record.Subject) 367 373 if err != nil { 368 374 return err 369 375 } ··· 446 452 return nil 447 453 } 448 454 449 - func (i *Ingester) ingestSpindle(e *models.Event) error { 455 + func (i *Ingester) ingestSpindle(ctx context.Context, e *models.Event) error { 450 456 did := e.Did 451 457 var err error 452 458 ··· 479 485 return err 480 486 } 481 487 482 - err = serververify.RunVerification(context.Background(), instance, did, i.Config.Core.Dev) 488 + err = serververify.RunVerification(ctx, instance, did, i.Config.Core.Dev) 483 489 if err != nil { 484 490 l.Error("failed to add spindle to db", "err", err, "instance", instance) 485 491 return err ··· 769 775 770 776 return nil 771 777 } 778 + func (i *Ingester) ingestIssue(ctx context.Context, e *models.Event) error { 779 + did := e.Did 780 + rkey := e.Commit.RKey 781 + 782 + var err error 783 + 784 + l := i.Logger.With("handler", "ingestIssue", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 785 + l.Info("ingesting record") 786 + 787 + ddb, ok := i.Db.Execer.(*db.DB) 788 + if !ok { 789 + return fmt.Errorf("failed to index issue record, invalid db cast") 790 + } 791 + 792 + switch e.Commit.Operation { 793 + case models.CommitOperationCreate: 794 + raw := json.RawMessage(e.Commit.Record) 795 + record := tangled.RepoIssue{} 796 + err = json.Unmarshal(raw, &record) 797 + if err != nil { 798 + l.Error("invalid record", "err", err) 799 + return err 800 + } 801 + 802 + issue := db.IssueFromRecord(did, rkey, record) 803 + 804 + sanitizer := markup.NewSanitizer() 805 + if st := strings.TrimSpace(sanitizer.SanitizeDescription(issue.Title)); st == "" { 806 + return fmt.Errorf("title is empty after HTML sanitization") 807 + } 808 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(issue.Body)); sb == "" { 809 + return fmt.Errorf("body is empty after HTML sanitization") 810 + } 811 + 812 + tx, err := ddb.BeginTx(ctx, nil) 813 + if err != nil { 814 + l.Error("failed to begin transaction", "err", err) 815 + return err 816 + } 817 + 818 + err = db.NewIssue(tx, &issue) 819 + if err != nil { 820 + l.Error("failed to create issue", "err", err) 821 + return err 822 + } 823 + 824 + return nil 825 + 826 + case models.CommitOperationUpdate: 827 + raw := json.RawMessage(e.Commit.Record) 828 + record := tangled.RepoIssue{} 829 + err = json.Unmarshal(raw, &record) 830 + if err != nil { 831 + l.Error("invalid record", "err", err) 832 + return err 833 + } 834 + 835 + body := "" 836 + if record.Body != nil { 837 + body = *record.Body 838 + } 839 + 840 + sanitizer := markup.NewSanitizer() 841 + if st := strings.TrimSpace(sanitizer.SanitizeDescription(record.Title)); st == "" { 842 + return fmt.Errorf("title is empty after HTML sanitization") 843 + } 844 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(body)); sb == "" { 845 + return fmt.Errorf("body is empty after HTML sanitization") 846 + } 847 + 848 + err = db.UpdateIssueByRkey(ddb, did, rkey, record.Title, body) 849 + if err != nil { 850 + l.Error("failed to update issue", "err", err) 851 + return err 852 + } 853 + 854 + return nil 855 + 856 + case models.CommitOperationDelete: 857 + if err := db.DeleteIssueByRkey(ddb, did, rkey); err != nil { 858 + l.Error("failed to delete", "err", err) 859 + return fmt.Errorf("failed to delete issue record: %w", err) 860 + } 861 + 862 + return nil 863 + } 864 + 865 + return fmt.Errorf("unknown operation: %s", e.Commit.Operation) 866 + } 867 + 868 + func (i *Ingester) ingestIssueComment(e *models.Event) error { 869 + did := e.Did 870 + rkey := e.Commit.RKey 871 + 872 + var err error 873 + 874 + l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 875 + l.Info("ingesting record") 876 + 877 + ddb, ok := i.Db.Execer.(*db.DB) 878 + if !ok { 879 + return fmt.Errorf("failed to index issue comment record, invalid db cast") 880 + } 881 + 882 + switch e.Commit.Operation { 883 + case models.CommitOperationCreate: 884 + raw := json.RawMessage(e.Commit.Record) 885 + record := tangled.RepoIssueComment{} 886 + err = json.Unmarshal(raw, &record) 887 + if err != nil { 888 + l.Error("invalid record", "err", err) 889 + return err 890 + } 891 + 892 + comment, err := db.IssueCommentFromRecord(ddb, did, rkey, record) 893 + if err != nil { 894 + l.Error("failed to parse comment from record", "err", err) 895 + return err 896 + } 897 + 898 + sanitizer := markup.NewSanitizer() 899 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(comment.Body)); sb == "" { 900 + return fmt.Errorf("body is empty after HTML sanitization") 901 + } 902 + 903 + err = db.NewIssueComment(ddb, &comment) 904 + if err != nil { 905 + l.Error("failed to create issue comment", "err", err) 906 + return err 907 + } 908 + 909 + return nil 910 + 911 + case models.CommitOperationUpdate: 912 + raw := json.RawMessage(e.Commit.Record) 913 + record := tangled.RepoIssueComment{} 914 + err = json.Unmarshal(raw, &record) 915 + if err != nil { 916 + l.Error("invalid record", "err", err) 917 + return err 918 + } 919 + 920 + sanitizer := markup.NewSanitizer() 921 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(record.Body)); sb == "" { 922 + return fmt.Errorf("body is empty after HTML sanitization") 923 + } 924 + 925 + err = db.UpdateCommentByRkey(ddb, did, rkey, record.Body) 926 + if err != nil { 927 + l.Error("failed to update issue comment", "err", err) 928 + return err 929 + } 930 + 931 + return nil 932 + 933 + case models.CommitOperationDelete: 934 + if err := db.DeleteCommentByRkey(ddb, did, rkey); err != nil { 935 + l.Error("failed to delete", "err", err) 936 + return fmt.Errorf("failed to delete issue comment record: %w", err) 937 + } 938 + 939 + return nil 940 + } 941 + 942 + return fmt.Errorf("unknown operation: %s", e.Commit.Operation) 943 + }
+3 -9
appview/issues/issues.go
··· 278 278 } 279 279 280 280 createdAt := time.Now().Format(time.RFC3339) 281 - commentIdInt64 := int64(commentId) 282 281 ownerDid := user.Did 283 282 issueAt, err := db.GetIssueAt(rp.db, f.RepoAt(), issueIdInt) 284 283 if err != nil { ··· 302 301 Val: &tangled.RepoIssueComment{ 303 302 Repo: &atUri, 304 303 Issue: issueAt, 305 - CommentId: &commentIdInt64, 306 304 Owner: &ownerDid, 307 305 Body: body, 308 306 CreatedAt: createdAt, ··· 451 449 repoAt := record["repo"].(string) 452 450 issueAt := record["issue"].(string) 453 451 createdAt := record["createdAt"].(string) 454 - commentIdInt64 := int64(commentIdInt) 455 452 456 453 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 457 454 Collection: tangled.RepoIssueCommentNSID, ··· 462 459 Val: &tangled.RepoIssueComment{ 463 460 Repo: &repoAt, 464 461 Issue: issueAt, 465 - CommentId: &commentIdInt64, 466 462 Owner: &comment.OwnerDid, 467 463 Body: newBody, 468 464 CreatedAt: createdAt, ··· 687 683 Rkey: issue.Rkey, 688 684 Record: &lexutil.LexiconTypeDecoder{ 689 685 Val: &tangled.RepoIssue{ 690 - Repo: atUri, 691 - Title: title, 692 - Body: &body, 693 - Owner: user.Did, 694 - IssueId: int64(issue.IssueId), 686 + Repo: atUri, 687 + Title: title, 688 + Body: &body, 695 689 }, 696 690 }, 697 691 })
+5
appview/oauth/handler/handler.go
··· 435 435 return 436 436 } 437 437 438 + if err := o.enforcer.AddKnotMember(defaultKnot, did); err != nil { 439 + log.Printf("failed to set up enforcer rules: %s", err) 440 + return 441 + } 442 + 438 443 log.Printf("successfully added %s to default Knot", did) 439 444 } 440 445
+35
appview/pages/cache.go
··· 1 + package pages 2 + 3 + import ( 4 + "sync" 5 + ) 6 + 7 + type TmplCache[K comparable, V any] struct { 8 + data map[K]V 9 + mutex sync.RWMutex 10 + } 11 + 12 + func NewTmplCache[K comparable, V any]() *TmplCache[K, V] { 13 + return &TmplCache[K, V]{ 14 + data: make(map[K]V), 15 + } 16 + } 17 + 18 + func (c *TmplCache[K, V]) Get(key K) (V, bool) { 19 + c.mutex.RLock() 20 + defer c.mutex.RUnlock() 21 + val, exists := c.data[key] 22 + return val, exists 23 + } 24 + 25 + func (c *TmplCache[K, V]) Set(key K, value V) { 26 + c.mutex.Lock() 27 + defer c.mutex.Unlock() 28 + c.data[key] = value 29 + } 30 + 31 + func (c *TmplCache[K, V]) Size() int { 32 + c.mutex.RLock() 33 + defer c.mutex.RUnlock() 34 + return len(c.data) 35 + }
+2
appview/pages/markup/markdown.go
··· 11 11 12 12 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 13 13 "github.com/alecthomas/chroma/v2/styles" 14 + treeblood "github.com/wyatt915/goldmark-treeblood" 14 15 "github.com/yuin/goldmark" 15 16 highlighting "github.com/yuin/goldmark-highlighting/v2" 16 17 "github.com/yuin/goldmark/ast" ··· 59 60 extension.NewFootnote( 60 61 extension.WithFootnoteIDPrefix([]byte("footnote")), 61 62 ), 63 + treeblood.MathML(), 62 64 ), 63 65 goldmark.WithParserOptions( 64 66 parser.WithAutoHeadingID(),
+17
appview/pages/markup/sanitizer.go
··· 97 97 "margin-bottom", 98 98 ) 99 99 100 + // math 101 + mathAttrs := []string{ 102 + "accent", "columnalign", "columnlines", "columnspan", "dir", "display", 103 + "displaystyle", "encoding", "fence", "form", "largeop", "linebreak", 104 + "linethickness", "lspace", "mathcolor", "mathsize", "mathvariant", "minsize", 105 + "movablelimits", "notation", "rowalign", "rspace", "rowspacing", "rowspan", 106 + "scriptlevel", "stretchy", "symmetric", "title", "voffset", "width", 107 + } 108 + mathElements := []string{ 109 + "annotation", "math", "menclose", "merror", "mfrac", "mi", "mmultiscripts", 110 + "mn", "mo", "mover", "mpadded", "mprescripts", "mroot", "mrow", "mspace", 111 + "msqrt", "mstyle", "msub", "msubsup", "msup", "mtable", "mtd", "mtext", 112 + "mtr", "munder", "munderover", "semantics", 113 + } 114 + policy.AllowNoAttrs().OnElements(mathElements...) 115 + policy.AllowAttrs(mathAttrs...).OnElements(mathElements...) 116 + 100 117 return policy 101 118 } 102 119
+202 -164
appview/pages/pages.go
··· 9 9 "html/template" 10 10 "io" 11 11 "io/fs" 12 - "log" 12 + "log/slog" 13 13 "net/http" 14 14 "os" 15 15 "path/filepath" ··· 42 42 var Files embed.FS 43 43 44 44 type Pages struct { 45 - mu sync.RWMutex 46 - t map[string]*template.Template 45 + mu sync.RWMutex 46 + cache *TmplCache[string, *template.Template] 47 47 48 48 avatar config.AvatarConfig 49 49 resolver *idresolver.Resolver 50 50 dev bool 51 - embedFS embed.FS 51 + embedFS fs.FS 52 52 templateDir string // Path to templates on disk for dev mode 53 53 rctx *markup.RenderContext 54 + logger *slog.Logger 54 55 } 55 56 56 57 func NewPages(config *config.Config, res *idresolver.Resolver) *Pages { ··· 64 65 65 66 p := &Pages{ 66 67 mu: sync.RWMutex{}, 67 - t: make(map[string]*template.Template), 68 + cache: NewTmplCache[string, *template.Template](), 68 69 dev: config.Core.Dev, 69 70 avatar: config.Avatar, 70 - embedFS: Files, 71 71 rctx: rctx, 72 72 resolver: res, 73 73 templateDir: "appview/pages", 74 + logger: slog.Default().With("component", "pages"), 74 75 } 75 76 76 - // Initial load of all templates 77 - p.loadAllTemplates() 77 + if p.dev { 78 + p.embedFS = os.DirFS(p.templateDir) 79 + } else { 80 + p.embedFS = Files 81 + } 78 82 79 83 return p 80 84 } 81 85 82 - func (p *Pages) loadAllTemplates() { 83 - templates := make(map[string]*template.Template) 84 - var fragmentPaths []string 86 + func (p *Pages) pathToName(s string) string { 87 + return strings.TrimSuffix(strings.TrimPrefix(s, "templates/"), ".html") 88 + } 85 89 86 - // Use embedded FS for initial loading 87 - // First, collect all fragment paths 90 + // reverse of pathToName 91 + func (p *Pages) nameToPath(s string) string { 92 + return "templates/" + s + ".html" 93 + } 94 + 95 + func (p *Pages) fragmentPaths() ([]string, error) { 96 + var fragmentPaths []string 88 97 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 89 98 if err != nil { 90 99 return err ··· 98 107 if !strings.Contains(path, "fragments/") { 99 108 return nil 100 109 } 101 - name := strings.TrimPrefix(path, "templates/") 102 - name = strings.TrimSuffix(name, ".html") 103 - tmpl, err := template.New(name). 104 - Funcs(p.funcMap()). 105 - ParseFS(p.embedFS, path) 106 - if err != nil { 107 - log.Fatalf("setting up fragment: %v", err) 108 - } 109 - templates[name] = tmpl 110 110 fragmentPaths = append(fragmentPaths, path) 111 - log.Printf("loaded fragment: %s", name) 112 111 return nil 113 112 }) 114 113 if err != nil { 115 - log.Fatalf("walking template dir for fragments: %v", err) 114 + return nil, err 116 115 } 117 116 118 - // Then walk through and setup the rest of the templates 119 - err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 117 + return fragmentPaths, nil 118 + } 119 + 120 + func (p *Pages) fragments() (*template.Template, error) { 121 + fragmentPaths, err := p.fragmentPaths() 122 + if err != nil { 123 + return nil, err 124 + } 125 + 126 + funcs := p.funcMap() 127 + 128 + // parse all fragments together 129 + allFragments := template.New("").Funcs(funcs) 130 + for _, f := range fragmentPaths { 131 + name := p.pathToName(f) 132 + 133 + pf, err := template.New(name). 134 + Funcs(funcs). 135 + ParseFS(p.embedFS, f) 120 136 if err != nil { 121 - return err 122 - } 123 - if d.IsDir() { 124 - return nil 125 - } 126 - if !strings.HasSuffix(path, "html") { 127 - return nil 137 + return nil, err 128 138 } 129 - // Skip fragments as they've already been loaded 130 - if strings.Contains(path, "fragments/") { 131 - return nil 132 - } 133 - // Skip layouts 134 - if strings.Contains(path, "layouts/") { 135 - return nil 136 - } 137 - name := strings.TrimPrefix(path, "templates/") 138 - name = strings.TrimSuffix(name, ".html") 139 - // Add the page template on top of the base 140 - allPaths := []string{} 141 - allPaths = append(allPaths, "templates/layouts/*.html") 142 - allPaths = append(allPaths, fragmentPaths...) 143 - allPaths = append(allPaths, path) 144 - tmpl, err := template.New(name). 145 - Funcs(p.funcMap()). 146 - ParseFS(p.embedFS, allPaths...) 139 + 140 + allFragments, err = allFragments.AddParseTree(name, pf.Tree) 147 141 if err != nil { 148 - return fmt.Errorf("setting up template: %w", err) 142 + return nil, err 149 143 } 150 - templates[name] = tmpl 151 - log.Printf("loaded template: %s", name) 152 - return nil 153 - }) 154 - if err != nil { 155 - log.Fatalf("walking template dir: %v", err) 156 144 } 157 145 158 - log.Printf("total templates loaded: %d", len(templates)) 159 - p.mu.Lock() 160 - defer p.mu.Unlock() 161 - p.t = templates 146 + return allFragments, nil 162 147 } 163 148 164 - // loadTemplateFromDisk loads a template from the filesystem in dev mode 165 - func (p *Pages) loadTemplateFromDisk(name string) error { 166 - if !p.dev { 167 - return nil 149 + // parse without memoization 150 + func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 151 + paths, err := p.fragmentPaths() 152 + if err != nil { 153 + return nil, err 154 + } 155 + for _, s := range stack { 156 + paths = append(paths, p.nameToPath(s)) 168 157 } 169 158 170 - log.Printf("reloading template from disk: %s", name) 171 - 172 - // Find all fragments first 173 - var fragmentPaths []string 174 - err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error { 175 - if err != nil { 176 - return err 177 - } 178 - if d.IsDir() { 179 - return nil 180 - } 181 - if !strings.HasSuffix(path, ".html") { 182 - return nil 183 - } 184 - if !strings.Contains(path, "fragments/") { 185 - return nil 186 - } 187 - fragmentPaths = append(fragmentPaths, path) 188 - return nil 189 - }) 159 + funcs := p.funcMap() 160 + top := stack[len(stack)-1] 161 + parsed, err := template.New(top). 162 + Funcs(funcs). 163 + ParseFS(p.embedFS, paths...) 190 164 if err != nil { 191 - return fmt.Errorf("walking disk template dir for fragments: %w", err) 165 + return nil, err 192 166 } 193 167 194 - // Find the template path on disk 195 - templatePath := filepath.Join(p.templateDir, "templates", name+".html") 196 - if _, err := os.Stat(templatePath); os.IsNotExist(err) { 197 - return fmt.Errorf("template not found on disk: %s", name) 198 - } 168 + return parsed, nil 169 + } 199 170 200 - // Create a new template 201 - tmpl := template.New(name).Funcs(p.funcMap()) 171 + func (p *Pages) parse(stack ...string) (*template.Template, error) { 172 + key := strings.Join(stack, "|") 202 173 203 - // Parse layouts 204 - layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html") 205 - layouts, err := filepath.Glob(layoutGlob) 206 - if err != nil { 207 - return fmt.Errorf("finding layout templates: %w", err) 174 + // never cache in dev mode 175 + if cached, exists := p.cache.Get(key); !p.dev && exists { 176 + return cached, nil 208 177 } 209 178 210 - // Create paths for parsing 211 - allFiles := append(layouts, fragmentPaths...) 212 - allFiles = append(allFiles, templatePath) 213 - 214 - // Parse all templates 215 - tmpl, err = tmpl.ParseFiles(allFiles...) 179 + result, err := p.rawParse(stack...) 216 180 if err != nil { 217 - return fmt.Errorf("parsing template files: %w", err) 181 + return nil, err 218 182 } 219 183 220 - // Update the template in the map 221 - p.mu.Lock() 222 - defer p.mu.Unlock() 223 - p.t[name] = tmpl 224 - log.Printf("template reloaded from disk: %s", name) 225 - return nil 184 + p.cache.Set(key, result) 185 + return result, nil 226 186 } 227 187 228 - func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error { 229 - // In dev mode, reload the template from disk before executing 230 - if p.dev { 231 - if err := p.loadTemplateFromDisk(templateName); err != nil { 232 - log.Printf("warning: failed to reload template %s from disk: %v", templateName, err) 233 - // Continue with the existing template 234 - } 188 + func (p *Pages) parseBase(top string) (*template.Template, error) { 189 + stack := []string{ 190 + "layouts/base", 191 + top, 235 192 } 193 + return p.parse(stack...) 194 + } 236 195 237 - p.mu.RLock() 238 - defer p.mu.RUnlock() 239 - tmpl, exists := p.t[templateName] 240 - if !exists { 241 - return fmt.Errorf("template not found: %s", templateName) 196 + func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 197 + stack := []string{ 198 + "layouts/base", 199 + "layouts/repobase", 200 + top, 242 201 } 202 + return p.parse(stack...) 203 + } 243 204 244 - if base == "" { 245 - return tmpl.Execute(w, params) 246 - } else { 247 - return tmpl.ExecuteTemplate(w, base, params) 205 + func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 206 + stack := []string{ 207 + "layouts/base", 208 + "layouts/profilebase", 209 + top, 248 210 } 211 + return p.parse(stack...) 212 + } 213 + 214 + func (p *Pages) executePlain(name string, w io.Writer, params any) error { 215 + tpl, err := p.parse(name) 216 + if err != nil { 217 + return err 218 + } 219 + 220 + return tpl.Execute(w, params) 249 221 } 250 222 251 223 func (p *Pages) execute(name string, w io.Writer, params any) error { 252 - return p.executeOrReload(name, w, "layouts/base", params) 224 + tpl, err := p.parseBase(name) 225 + if err != nil { 226 + return err 227 + } 228 + 229 + return tpl.ExecuteTemplate(w, "layouts/base", params) 253 230 } 254 231 255 - func (p *Pages) executePlain(name string, w io.Writer, params any) error { 256 - return p.executeOrReload(name, w, "", params) 232 + func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 233 + tpl, err := p.parseRepoBase(name) 234 + if err != nil { 235 + return err 236 + } 237 + 238 + return tpl.ExecuteTemplate(w, "layouts/base", params) 257 239 } 258 240 259 - func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 260 - return p.executeOrReload(name, w, "layouts/repobase", params) 241 + func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 242 + tpl, err := p.parseProfileBase(name) 243 + if err != nil { 244 + return err 245 + } 246 + 247 + return tpl.ExecuteTemplate(w, "layouts/base", params) 261 248 } 262 249 263 250 func (p *Pages) Favicon(w io.Writer) error { ··· 422 409 return p.execute("repo/fork", w, params) 423 410 } 424 411 425 - type ProfileHomePageParams struct { 412 + type ProfileCard struct { 413 + UserDid string 414 + UserHandle string 415 + FollowStatus db.FollowStatus 416 + Punchcard *db.Punchcard 417 + Profile *db.Profile 418 + Stats ProfileStats 419 + Active string 420 + } 421 + 422 + type ProfileStats struct { 423 + RepoCount int64 424 + StarredCount int64 425 + StringCount int64 426 + FollowersCount int64 427 + FollowingCount int64 428 + } 429 + 430 + func (p *ProfileCard) GetTabs() [][]any { 431 + tabs := [][]any{ 432 + {"overview", "overview", "square-chart-gantt", nil}, 433 + {"repos", "repos", "book-marked", p.Stats.RepoCount}, 434 + {"starred", "starred", "star", p.Stats.StarredCount}, 435 + {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 436 + } 437 + 438 + return tabs 439 + } 440 + 441 + type ProfileOverviewParams struct { 426 442 LoggedInUser *oauth.User 427 443 Repos []db.Repo 428 444 CollaboratingRepos []db.Repo 429 445 ProfileTimeline *db.ProfileTimeline 430 - Card ProfileCard 431 - Punchcard db.Punchcard 446 + Card *ProfileCard 447 + Active string 432 448 } 433 449 434 - type ProfileCard struct { 435 - UserDid string 436 - UserHandle string 437 - FollowStatus db.FollowStatus 438 - FollowersCount int 439 - FollowingCount int 450 + func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 451 + params.Active = "overview" 452 + return p.executeProfile("user/overview", w, params) 453 + } 440 454 441 - Profile *db.Profile 455 + type ProfileReposParams struct { 456 + LoggedInUser *oauth.User 457 + Repos []db.Repo 458 + Card *ProfileCard 459 + Active string 442 460 } 443 461 444 - func (p *Pages) ProfileHomePage(w io.Writer, params ProfileHomePageParams) error { 445 - return p.execute("user/profile", w, params) 462 + func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 463 + params.Active = "repos" 464 + return p.executeProfile("user/repos", w, params) 446 465 } 447 466 448 - type ReposPageParams struct { 467 + type ProfileStarredParams struct { 449 468 LoggedInUser *oauth.User 450 469 Repos []db.Repo 451 - Card ProfileCard 470 + Card *ProfileCard 471 + Active string 472 + } 473 + 474 + func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 475 + params.Active = "starred" 476 + return p.executeProfile("user/starred", w, params) 477 + } 478 + 479 + type ProfileStringsParams struct { 480 + LoggedInUser *oauth.User 481 + Strings []db.String 482 + Card *ProfileCard 483 + Active string 452 484 } 453 485 454 - func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 455 - return p.execute("user/repos", w, params) 486 + func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 487 + params.Active = "strings" 488 + return p.executeProfile("user/strings", w, params) 456 489 } 457 490 458 491 type FollowCard struct { 459 492 UserDid string 460 493 FollowStatus db.FollowStatus 461 - FollowersCount int 462 - FollowingCount int 494 + FollowersCount int64 495 + FollowingCount int64 463 496 Profile *db.Profile 464 497 } 465 498 466 - type FollowersPageParams struct { 499 + type ProfileFollowersParams struct { 467 500 LoggedInUser *oauth.User 468 501 Followers []FollowCard 469 - Card ProfileCard 502 + Card *ProfileCard 503 + Active string 470 504 } 471 505 472 - func (p *Pages) FollowersPage(w io.Writer, params FollowersPageParams) error { 473 - return p.execute("user/followers", w, params) 506 + func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 507 + params.Active = "overview" 508 + return p.executeProfile("user/followers", w, params) 474 509 } 475 510 476 - type FollowingPageParams struct { 511 + type ProfileFollowingParams struct { 477 512 LoggedInUser *oauth.User 478 513 Following []FollowCard 479 - Card ProfileCard 514 + Card *ProfileCard 515 + Active string 480 516 } 481 517 482 - func (p *Pages) FollowingPage(w io.Writer, params FollowingPageParams) error { 483 - return p.execute("user/following", w, params) 518 + func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 519 + params.Active = "overview" 520 + return p.executeProfile("user/following", w, params) 484 521 } 485 522 486 523 type FollowFragmentParams struct { ··· 649 686 650 687 func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 651 688 params.Active = "overview" 652 - return p.execute("repo/tree", w, params) 689 + return p.executeRepo("repo/tree", w, params) 653 690 } 654 691 655 692 type RepoBranchesParams struct { ··· 863 900 } else { 864 901 params.State = "closed" 865 902 } 866 - return p.execute("repo/issues/issue", w, params) 903 + return p.executeRepo("repo/issues/issue", w, params) 867 904 } 868 905 869 906 type RepoNewIssueParams struct { ··· 1269 1306 1270 1307 sub, err := fs.Sub(Files, "static") 1271 1308 if err != nil { 1272 - log.Fatalf("no static dir found? that's crazy: %v", err) 1309 + p.logger.Error("no static dir found? that's crazy", "err", err) 1310 + panic(err) 1273 1311 } 1274 1312 // Custom handler to apply Cache-Control headers for font files 1275 1313 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) ··· 1292 1330 func CssContentHash() string { 1293 1331 cssFile, err := Files.Open("static/tw.css") 1294 1332 if err != nil { 1295 - log.Printf("Error opening CSS file: %v", err) 1333 + slog.Debug("Error opening CSS file", "err", err) 1296 1334 return "" 1297 1335 } 1298 1336 defer cssFile.Close() 1299 1337 1300 1338 hasher := sha256.New() 1301 1339 if _, err := io.Copy(hasher, cssFile); err != nil { 1302 - log.Printf("Error hashing CSS file: %v", err) 1340 + slog.Debug("Error hashing CSS file", "err", err) 1303 1341 return "" 1304 1342 } 1305 1343
+2 -7
appview/pages/repoinfo/repoinfo.go
··· 78 78 func (r RepoInfo) TabMetadata() map[string]any { 79 79 meta := make(map[string]any) 80 80 81 - if r.Stats.PullCount.Open > 0 { 82 - meta["pulls"] = r.Stats.PullCount.Open 83 - } 84 - 85 - if r.Stats.IssueCount.Open > 0 { 86 - meta["issues"] = r.Stats.IssueCount.Open 87 - } 81 + meta["pulls"] = r.Stats.PullCount.Open 82 + meta["issues"] = r.Stats.IssueCount.Open 88 83 89 84 // more stuff? 90 85
+2 -2
appview/pages/templates/layouts/base.html
··· 17 17 <body class="min-h-screen grid grid-cols-1 grid-rows-[min-content_auto_min-content] md:grid-cols-12 gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200"> 18 18 {{ block "topbarLayout" . }} 19 19 <header class="px-1 col-span-1 md:col-start-3 md:col-span-8" style="z-index: 20;"> 20 - {{ template "layouts/topbar" . }} 20 + {{ template "layouts/fragments/topbar" . }} 21 21 </header> 22 22 {{ end }} 23 23 ··· 39 39 40 40 {{ block "footerLayout" . }} 41 41 <footer class="px-1 col-span-1 md:col-start-3 md:col-span-8 mt-12"> 42 - {{ template "layouts/footer" . }} 42 + {{ template "layouts/fragments/footer" . }} 43 43 </footer> 44 44 {{ end }} 45 45 </body>
-48
appview/pages/templates/layouts/footer.html
··· 1 - {{ define "layouts/footer" }} 2 - <div class="w-full p-4 md:p-8 bg-white dark:bg-gray-800 rounded-t drop-shadow-sm"> 3 - <div class="container mx-auto max-w-7xl px-4"> 4 - <div class="flex flex-col lg:flex-row justify-between items-start text-gray-600 dark:text-gray-400 text-sm gap-8"> 5 - <div class="mb-4 md:mb-0"> 6 - <a href="/" hx-boost="true" class="flex gap-2 font-semibold italic"> 7 - tangled<sub>alpha</sub> 8 - </a> 9 - </div> 10 - 11 - {{ $headerStyle := "text-gray-900 dark:text-gray-200 font-bold text-xs uppercase tracking-wide mb-1" }} 12 - {{ $linkStyle := "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:underline inline-flex gap-1 items-center" }} 13 - {{ $iconStyle := "w-4 h-4 flex-shrink-0" }} 14 - <div class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-4 sm:gap-6 md:gap-2 gap-6 flex-1"> 15 - <div class="flex flex-col gap-1"> 16 - <div class="{{ $headerStyle }}">legal</div> 17 - <a href="/terms" class="{{ $linkStyle }}">{{ i "file-text" $iconStyle }} terms of service</a> 18 - <a href="/privacy" class="{{ $linkStyle }}">{{ i "shield" $iconStyle }} privacy policy</a> 19 - </div> 20 - 21 - <div class="flex flex-col gap-1"> 22 - <div class="{{ $headerStyle }}">resources</div> 23 - <a href="https://blog.tangled.sh" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 24 - <a href="https://tangled.sh/@tangled.sh/core/tree/master/docs" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 25 - <a href="https://tangled.sh/@tangled.sh/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 26 - </div> 27 - 28 - <div class="flex flex-col gap-1"> 29 - <div class="{{ $headerStyle }}">social</div> 30 - <a href="https://chat.tangled.sh" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "message-circle" $iconStyle }} discord</a> 31 - <a href="https://web.libera.chat/#tangled" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "hash" $iconStyle }} irc</a> 32 - <a href="https://bsky.app/profile/tangled.sh" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ template "user/fragments/bluesky" $iconStyle }} bluesky</a> 33 - </div> 34 - 35 - <div class="flex flex-col gap-1"> 36 - <div class="{{ $headerStyle }}">contact</div> 37 - <a href="mailto:team@tangled.sh" class="{{ $linkStyle }}">{{ i "mail" "w-4 h-4 flex-shrink-0" }} team@tangled.sh</a> 38 - <a href="mailto:security@tangled.sh" class="{{ $linkStyle }}">{{ i "shield-check" "w-4 h-4 flex-shrink-0" }} security@tangled.sh</a> 39 - </div> 40 - </div> 41 - 42 - <div class="text-center lg:text-right flex-shrink-0"> 43 - <div class="text-xs">&copy; 2025 Tangled Labs Oy. All rights reserved.</div> 44 - </div> 45 - </div> 46 - </div> 47 - </div> 48 - {{ end }}
+48
appview/pages/templates/layouts/fragments/footer.html
··· 1 + {{ define "layouts/fragments/footer" }} 2 + <div class="w-full p-4 md:p-8 bg-white dark:bg-gray-800 rounded-t drop-shadow-sm"> 3 + <div class="container mx-auto max-w-7xl px-4"> 4 + <div class="flex flex-col lg:flex-row justify-between items-start text-gray-600 dark:text-gray-400 text-sm gap-8"> 5 + <div class="mb-4 md:mb-0"> 6 + <a href="/" hx-boost="true" class="flex gap-2 font-semibold italic"> 7 + tangled<sub>alpha</sub> 8 + </a> 9 + </div> 10 + 11 + {{ $headerStyle := "text-gray-900 dark:text-gray-200 font-bold text-xs uppercase tracking-wide mb-1" }} 12 + {{ $linkStyle := "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:underline inline-flex gap-1 items-center" }} 13 + {{ $iconStyle := "w-4 h-4 flex-shrink-0" }} 14 + <div class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-4 sm:gap-6 md:gap-2 gap-6 flex-1"> 15 + <div class="flex flex-col gap-1"> 16 + <div class="{{ $headerStyle }}">legal</div> 17 + <a href="/terms" class="{{ $linkStyle }}">{{ i "file-text" $iconStyle }} terms of service</a> 18 + <a href="/privacy" class="{{ $linkStyle }}">{{ i "shield" $iconStyle }} privacy policy</a> 19 + </div> 20 + 21 + <div class="flex flex-col gap-1"> 22 + <div class="{{ $headerStyle }}">resources</div> 23 + <a href="https://blog.tangled.sh" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 24 + <a href="https://tangled.sh/@tangled.sh/core/tree/master/docs" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 25 + <a href="https://tangled.sh/@tangled.sh/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 26 + </div> 27 + 28 + <div class="flex flex-col gap-1"> 29 + <div class="{{ $headerStyle }}">social</div> 30 + <a href="https://chat.tangled.sh" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "message-circle" $iconStyle }} discord</a> 31 + <a href="https://web.libera.chat/#tangled" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "hash" $iconStyle }} irc</a> 32 + <a href="https://bsky.app/profile/tangled.sh" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ template "user/fragments/bluesky" $iconStyle }} bluesky</a> 33 + </div> 34 + 35 + <div class="flex flex-col gap-1"> 36 + <div class="{{ $headerStyle }}">contact</div> 37 + <a href="mailto:team@tangled.sh" class="{{ $linkStyle }}">{{ i "mail" "w-4 h-4 flex-shrink-0" }} team@tangled.sh</a> 38 + <a href="mailto:security@tangled.sh" class="{{ $linkStyle }}">{{ i "shield-check" "w-4 h-4 flex-shrink-0" }} security@tangled.sh</a> 39 + </div> 40 + </div> 41 + 42 + <div class="text-center lg:text-right flex-shrink-0"> 43 + <div class="text-xs">&copy; 2025 Tangled Labs Oy. All rights reserved.</div> 44 + </div> 45 + </div> 46 + </div> 47 + </div> 48 + {{ end }}
+87
appview/pages/templates/layouts/fragments/topbar.html
··· 1 + {{ define "layouts/fragments/topbar" }} 2 + <nav class="space-x-4 px-6 py-2 rounded bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm"> 3 + <div class="flex justify-between p-0 items-center"> 4 + <div id="left-items"> 5 + <a href="/" hx-boost="true" class="flex gap-2 font-bold italic"> 6 + tangled<sub>alpha</sub> 7 + </a> 8 + </div> 9 + 10 + <div id="right-items" class="flex items-center gap-2"> 11 + {{ with .LoggedInUser }} 12 + {{ block "newButton" . }} {{ end }} 13 + {{ block "dropDown" . }} {{ end }} 14 + {{ else }} 15 + <a href="/login">login</a> 16 + <span class="text-gray-500 dark:text-gray-400">or</span> 17 + <a href="/signup" class="btn-create py-0 hover:no-underline hover:text-white flex items-center gap-2"> 18 + join now {{ i "arrow-right" "size-4" }} 19 + </a> 20 + {{ end }} 21 + </div> 22 + </div> 23 + </nav> 24 + {{ if .LoggedInUser }} 25 + <div id="upgrade-banner" 26 + hx-get="/knots/upgradeBanner" 27 + hx-trigger="load" 28 + hx-swap="innerHTML"> 29 + </div> 30 + {{ end }} 31 + {{ end }} 32 + 33 + {{ define "newButton" }} 34 + <details class="relative inline-block text-left nav-dropdown"> 35 + <summary class="btn-create py-0 cursor-pointer list-none flex items-center gap-2"> 36 + {{ i "plus" "w-4 h-4" }} new 37 + </summary> 38 + <div class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700"> 39 + <a href="/repo/new" class="flex items-center gap-2"> 40 + {{ i "book-plus" "w-4 h-4" }} 41 + new repository 42 + </a> 43 + <a href="/strings/new" class="flex items-center gap-2"> 44 + {{ i "line-squiggle" "w-4 h-4" }} 45 + new string 46 + </a> 47 + </div> 48 + </details> 49 + {{ end }} 50 + 51 + {{ define "dropDown" }} 52 + <details class="relative inline-block text-left nav-dropdown"> 53 + <summary 54 + class="cursor-pointer list-none flex items-center" 55 + > 56 + {{ $user := didOrHandle .Did .Handle }} 57 + {{ template "user/fragments/picHandle" $user }} 58 + </summary> 59 + <div 60 + class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700" 61 + > 62 + <a href="/{{ $user }}">profile</a> 63 + <a href="/{{ $user }}?tab=repos">repositories</a> 64 + <a href="/{{ $user }}?tab=strings">strings</a> 65 + <a href="/knots">knots</a> 66 + <a href="/spindles">spindles</a> 67 + <a href="/settings">settings</a> 68 + <a href="#" 69 + hx-post="/logout" 70 + hx-swap="none" 71 + class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 72 + logout 73 + </a> 74 + </div> 75 + </details> 76 + 77 + <script> 78 + document.addEventListener('click', function(event) { 79 + const dropdowns = document.querySelectorAll('.nav-dropdown'); 80 + dropdowns.forEach(function(dropdown) { 81 + if (!dropdown.contains(event.target)) { 82 + dropdown.removeAttribute('open'); 83 + } 84 + }); 85 + }); 86 + </script> 87 + {{ end }}
+104
appview/pages/templates/layouts/profilebase.html
··· 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }} 2 + 3 + {{ define "extrameta" }} 4 + <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}" /> 5 + <meta property="og:type" content="profile" /> 6 + <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab={{ .Active }}" /> 7 + <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 + {{ end }} 9 + 10 + {{ define "content" }} 11 + {{ template "profileTabs" . }} 12 + <section class="bg-white dark:bg-gray-800 p-6 rounded w-full dark:text-white drop-shadow-sm"> 13 + <div class="grid grid-cols-1 md:grid-cols-11 gap-4"> 14 + <div class="md:col-span-3 order-1 md:order-1"> 15 + <div class="flex flex-col gap-4"> 16 + {{ template "user/fragments/profileCard" .Card }} 17 + {{ block "punchcard" .Card.Punchcard }} {{ end }} 18 + </div> 19 + </div> 20 + {{ block "profileContent" . }} {{ end }} 21 + </div> 22 + </section> 23 + {{ end }} 24 + 25 + {{ define "profileTabs" }} 26 + <nav class="w-full pl-4 overflow-x-auto overflow-y-hidden"> 27 + <div class="flex z-60"> 28 + {{ $activeTabStyles := "-mb-px bg-white dark:bg-gray-800" }} 29 + {{ $tabs := .Card.GetTabs }} 30 + {{ $tabmeta := dict "x" "y" }} 31 + {{ range $item := $tabs }} 32 + {{ $key := index $item 0 }} 33 + {{ $value := index $item 1 }} 34 + {{ $icon := index $item 2 }} 35 + {{ $meta := index $item 3 }} 36 + <a 37 + href="?tab={{ $value }}" 38 + class="relative -mr-px group no-underline hover:no-underline" 39 + hx-boost="true"> 40 + <div 41 + class="px-4 py-1 mr-1 text-black dark:text-white min-w-[80px] text-center relative rounded-t whitespace-nowrap 42 + {{ if eq $.Active $key }} 43 + {{ $activeTabStyles }} 44 + {{ else }} 45 + group-hover:bg-gray-100/25 group-hover:dark:bg-gray-700/25 46 + {{ end }} 47 + "> 48 + <span class="flex items-center justify-center"> 49 + {{ i $icon "w-4 h-4 mr-2" }} 50 + {{ $key }} 51 + {{ if $meta }} 52 + <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 text-sm ml-1">{{ $meta }}</span> 53 + {{ end }} 54 + </span> 55 + </div> 56 + </a> 57 + {{ end }} 58 + </div> 59 + </nav> 60 + {{ end }} 61 + 62 + {{ define "punchcard" }} 63 + {{ $now := now }} 64 + <div> 65 + <p class="px-2 pb-4 flex gap-2 text-sm font-bold dark:text-white"> 66 + PUNCHCARD 67 + <span class="font-mono font-normal text-sm text-gray-500 dark:text-gray-400 "> 68 + {{ .Total | int64 | commaFmt }} commits 69 + </span> 70 + </p> 71 + <div class="grid grid-cols-28 md:grid-cols-14 gap-y-3 w-full h-full"> 72 + {{ range .Punches }} 73 + {{ $count := .Count }} 74 + {{ $theme := "bg-gray-200 dark:bg-gray-700 size-[4px]" }} 75 + {{ if lt $count 1 }} 76 + {{ $theme = "bg-gray-200 dark:bg-gray-700 size-[4px]" }} 77 + {{ else if lt $count 2 }} 78 + {{ $theme = "bg-green-200 dark:bg-green-900 size-[5px]" }} 79 + {{ else if lt $count 4 }} 80 + {{ $theme = "bg-green-300 dark:bg-green-800 size-[5px]" }} 81 + {{ else if lt $count 8 }} 82 + {{ $theme = "bg-green-400 dark:bg-green-700 size-[6px]" }} 83 + {{ else }} 84 + {{ $theme = "bg-green-500 dark:bg-green-600 size-[7px]" }} 85 + {{ end }} 86 + 87 + {{ if .Date.After $now }} 88 + {{ $theme = "border border-gray-200 dark:border-gray-700 size-[4px]" }} 89 + {{ end }} 90 + <div class="w-full h-full flex justify-center items-center"> 91 + <div 92 + class="aspect-square rounded-full transition-all duration-300 {{ $theme }} max-w-full max-h-full" 93 + title="{{ .Date.Format "2006-01-02" }}: {{ .Count }} commits"> 94 + </div> 95 + </div> 96 + {{ end }} 97 + </div> 98 + </div> 99 + {{ end }} 100 + 101 + {{ define "layouts/profilebase" }} 102 + {{ template "layouts/base" . }} 103 + {{ end }} 104 +
+2 -6
appview/pages/templates/layouts/repobase.html
··· 71 71 <span class="flex items-center justify-center"> 72 72 {{ i $icon "w-4 h-4 mr-2" }} 73 73 {{ $key }} 74 - {{ if not (isNil $meta) }} 75 - <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 text-sm ml-1">{{ $meta }}</span> 74 + {{ if $meta }} 75 + <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 text-sm ml-1">{{ $meta }}</span> 76 76 {{ end }} 77 77 </span> 78 78 </div> ··· 88 88 {{ block "repoAfter" . }}{{ end }} 89 89 </section> 90 90 {{ end }} 91 - 92 - {{ define "layouts/repobase" }} 93 - {{ template "layouts/base" . }} 94 - {{ end }}
-87
appview/pages/templates/layouts/topbar.html
··· 1 - {{ define "layouts/topbar" }} 2 - <nav class="space-x-4 px-6 py-2 rounded bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm"> 3 - <div class="flex justify-between p-0 items-center"> 4 - <div id="left-items"> 5 - <a href="/" hx-boost="true" class="flex gap-2 font-bold italic"> 6 - tangled<sub>alpha</sub> 7 - </a> 8 - </div> 9 - 10 - <div id="right-items" class="flex items-center gap-2"> 11 - {{ with .LoggedInUser }} 12 - {{ block "newButton" . }} {{ end }} 13 - {{ block "dropDown" . }} {{ end }} 14 - {{ else }} 15 - <a href="/login">login</a> 16 - <span class="text-gray-500 dark:text-gray-400">or</span> 17 - <a href="/signup" class="btn-create py-0 hover:no-underline hover:text-white flex items-center gap-2"> 18 - join now {{ i "arrow-right" "size-4" }} 19 - </a> 20 - {{ end }} 21 - </div> 22 - </div> 23 - </nav> 24 - {{ if .LoggedInUser }} 25 - <div id="upgrade-banner" 26 - hx-get="/knots/upgradeBanner" 27 - hx-trigger="load" 28 - hx-swap="innerHTML"> 29 - </div> 30 - {{ end }} 31 - {{ end }} 32 - 33 - {{ define "newButton" }} 34 - <details class="relative inline-block text-left nav-dropdown"> 35 - <summary class="btn-create py-0 cursor-pointer list-none flex items-center gap-2"> 36 - {{ i "plus" "w-4 h-4" }} new 37 - </summary> 38 - <div class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700"> 39 - <a href="/repo/new" class="flex items-center gap-2"> 40 - {{ i "book-plus" "w-4 h-4" }} 41 - new repository 42 - </a> 43 - <a href="/strings/new" class="flex items-center gap-2"> 44 - {{ i "line-squiggle" "w-4 h-4" }} 45 - new string 46 - </a> 47 - </div> 48 - </details> 49 - {{ end }} 50 - 51 - {{ define "dropDown" }} 52 - <details class="relative inline-block text-left nav-dropdown"> 53 - <summary 54 - class="cursor-pointer list-none flex items-center" 55 - > 56 - {{ $user := didOrHandle .Did .Handle }} 57 - {{ template "user/fragments/picHandle" $user }} 58 - </summary> 59 - <div 60 - class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700" 61 - > 62 - <a href="/{{ $user }}">profile</a> 63 - <a href="/{{ $user }}?tab=repos">repositories</a> 64 - <a href="/strings/{{ $user }}">strings</a> 65 - <a href="/knots">knots</a> 66 - <a href="/spindles">spindles</a> 67 - <a href="/settings">settings</a> 68 - <a href="#" 69 - hx-post="/logout" 70 - hx-swap="none" 71 - class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 72 - logout 73 - </a> 74 - </div> 75 - </details> 76 - 77 - <script> 78 - document.addEventListener('click', function(event) { 79 - const dropdowns = document.querySelectorAll('.nav-dropdown'); 80 - dropdowns.forEach(function(dropdown) { 81 - if (!dropdown.contains(event.target)) { 82 - dropdown.removeAttribute('open'); 83 - } 84 - }); 85 - }); 86 - </script> 87 - {{ end }}
+2 -2
appview/pages/templates/repo/commit.html
··· 81 81 82 82 {{ define "topbarLayout" }} 83 83 <header class="px-1 col-span-full" style="z-index: 20;"> 84 - {{ template "layouts/topbar" . }} 84 + {{ template "layouts/fragments/topbar" . }} 85 85 </header> 86 86 {{ end }} 87 87 ··· 106 106 107 107 {{ define "footerLayout" }} 108 108 <footer class="px-1 col-span-full mt-12"> 109 - {{ template "layouts/footer" . }} 109 + {{ template "layouts/fragments/footer" . }} 110 110 </footer> 111 111 {{ end }} 112 112
+2 -2
appview/pages/templates/repo/compare/compare.html
··· 12 12 13 13 {{ define "topbarLayout" }} 14 14 <header class="px-1 col-span-full" style="z-index: 20;"> 15 - {{ template "layouts/topbar" . }} 15 + {{ template "layouts/fragments/topbar" . }} 16 16 </header> 17 17 {{ end }} 18 18 ··· 37 37 38 38 {{ define "footerLayout" }} 39 39 <footer class="px-1 col-span-full mt-12"> 40 - {{ template "layouts/footer" . }} 40 + {{ template "layouts/fragments/footer" . }} 41 41 </footer> 42 42 {{ end }} 43 43
+29 -83
appview/pages/templates/repo/fragments/diff.html
··· 13 13 <div class="flex flex-col gap-4"> 14 14 {{ range $idx, $hunk := $diff }} 15 15 {{ with $hunk }} 16 - <section class="border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 17 - <div id="file-{{ .Name.New }}"> 18 - <div id="diff-file"> 19 - <details open> 20 - <summary class="list-none cursor-pointer sticky top-0"> 21 - <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 22 - <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 23 - <div class="flex gap-1 items-center"> 24 - {{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }} 25 - {{ if .IsNew }} 26 - <span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">ADDED</span> 27 - {{ else if .IsDelete }} 28 - <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">DELETED</span> 29 - {{ else if .IsCopy }} 30 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">COPIED</span> 31 - {{ else if .IsRename }} 32 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">RENAMED</span> 33 - {{ else }} 34 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">MODIFIED</span> 35 - {{ end }} 36 - 37 - {{ template "repo/fragments/diffStatPill" .Stats }} 38 - </div> 39 - 40 - <div class="flex gap-2 items-center overflow-x-auto"> 41 - {{ if .IsDelete }} 42 - <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this }}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.Old }}"{{end}}> 43 - {{ .Name.Old }} 44 - </a> 45 - {{ else if (or .IsCopy .IsRename) }} 46 - <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $parent}}href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}"{{end}}> 47 - {{ .Name.Old }} 48 - </a> 49 - {{ i "arrow-right" "w-4 h-4" }} 50 - <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}> 51 - {{ .Name.New }} 52 - </a> 53 - {{ else }} 54 - <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this}}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}"{{end}}> 55 - {{ .Name.New }} 56 - </a> 57 - {{ end }} 58 - </div> 59 - </div> 60 - 61 - {{ $iconstyle := "p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" }} 62 - <div id="right-side-items" class="p-2 flex items-center"> 63 - <a title="top of file" href="#file-{{ .Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up-to-line" "w-4 h-4" }}</a> 64 - {{ if gt $idx 0 }} 65 - {{ $prev := index $diff (sub $idx 1) }} 66 - <a title="previous file" href="#file-{{ $prev.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-up" "w-4 h-4" }}</a> 67 - {{ end }} 68 - 69 - {{ if lt $idx $last }} 70 - {{ $next := index $diff (add $idx 1) }} 71 - <a title="next file" href="#file-{{ $next.Name.New }}" class="{{ $iconstyle }}">{{ i "arrow-down" "w-4 h-4" }}</a> 72 - {{ end }} 73 - </div> 74 - 75 - </div> 76 - </summary> 16 + <details open id="file-{{ .Name.New }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}"> 17 + <summary class="list-none cursor-pointer sticky top-0"> 18 + <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 19 + <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 20 + <span class="group-open:hidden inline">{{ i "chevron-right" "w-4 h-4" }}</span> 21 + <span class="hidden group-open:inline">{{ i "chevron-down" "w-4 h-4" }}</span> 22 + {{ template "repo/fragments/diffStatPill" .Stats }} 77 23 78 - <div class="transition-all duration-700 ease-in-out"> 79 - {{ if .IsDelete }} 80 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 81 - This file has been deleted. 82 - </p> 83 - {{ else if .IsCopy }} 84 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 85 - This file has been copied. 86 - </p> 87 - {{ else if .IsBinary }} 88 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 89 - This is a binary file and will not be displayed. 90 - </p> 91 - {{ else }} 92 - {{ if $isSplit }} 93 - {{- template "repo/fragments/splitDiff" .Split -}} 24 + <div class="flex gap-2 items-center overflow-x-auto"> 25 + {{ if .IsDelete }} 26 + {{ .Name.Old }} 27 + {{ else if (or .IsCopy .IsRename) }} 28 + {{ .Name.Old }} {{ i "arrow-right" "w-4 h-4" }} {{ .Name.New }} 94 29 {{ else }} 95 - {{- template "repo/fragments/unifiedDiff" . -}} 30 + {{ .Name.New }} 96 31 {{ end }} 97 - {{- end -}} 32 + </div> 98 33 </div> 34 + </div> 35 + </summary> 99 36 100 - </details> 101 - 37 + <div class="transition-all duration-700 ease-in-out"> 38 + {{ if .IsBinary }} 39 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 40 + This is a binary file and will not be displayed. 41 + </p> 42 + {{ else }} 43 + {{ if $isSplit }} 44 + {{- template "repo/fragments/splitDiff" .Split -}} 45 + {{ else }} 46 + {{- template "repo/fragments/unifiedDiff" . -}} 47 + {{ end }} 48 + {{- end -}} 102 49 </div> 103 - </div> 104 - </section> 50 + </details> 105 51 {{ end }} 106 52 {{ end }} 107 53 </div>
+4
appview/pages/templates/repo/fragments/duration.html
··· 1 + {{ define "repo/fragments/duration" }} 2 + <time datetime="{{ . | iso8601DurationFmt }}" title="{{ . | longDurationFmt }}">{{ . | durationFmt }}</time> 3 + {{ end }} 4 +
+44 -69
appview/pages/templates/repo/fragments/interdiff.html
··· 10 10 <div class="flex flex-col gap-4"> 11 11 {{ range $idx, $hunk := $diff }} 12 12 {{ with $hunk }} 13 - <section class="border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 14 - <div id="file-{{ .Name }}"> 15 - <div id="diff-file"> 16 - <details {{ if not (.Status.IsOnlyInOne) }}open{{end}}> 17 - <summary class="list-none cursor-pointer sticky top-0"> 18 - <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 19 - <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 20 - <div class="flex gap-1 items-center" style="direction: ltr;"> 21 - {{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }} 22 - {{ if .Status.IsOk }} 23 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">CHANGED</span> 24 - {{ else if .Status.IsUnchanged }} 25 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">UNCHANGED</span> 26 - {{ else if .Status.IsOnlyInOne }} 27 - <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">REVERTED</span> 28 - {{ else if .Status.IsOnlyInTwo }} 29 - <span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">NEW</span> 30 - {{ else if .Status.IsRebased }} 31 - <span class="bg-amber-100 text-amber-700 dark:bg-amber-800/50 dark:text-amber-400 {{ $markerstyle }}">REBASED</span> 32 - {{ else }} 33 - <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">ERROR</span> 34 - {{ end }} 35 - </div> 36 - 37 - <div class="flex gap-2 items-center overflow-x-auto" style="direction: rtl;"> 38 - <a class="dark:text-white whitespace-nowrap overflow-x-auto" href=""> 39 - {{ .Name }} 40 - </a> 41 - </div> 42 - </div> 43 - 44 - {{ $iconstyle := "p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" }} 45 - <div id="right-side-items" class="p-2 flex items-center"> 46 - <a title="top of file" href="#file-{{ .Name }}" class="{{ $iconstyle }}">{{ i "arrow-up-to-line" "w-4 h-4" }}</a> 47 - {{ if gt $idx 0 }} 48 - {{ $prev := index $diff (sub $idx 1) }} 49 - <a title="previous file" href="#file-{{ $prev.Name }}" class="{{ $iconstyle }}">{{ i "arrow-up" "w-4 h-4" }}</a> 50 - {{ end }} 51 - 52 - {{ if lt $idx $last }} 53 - {{ $next := index $diff (add $idx 1) }} 54 - <a title="next file" href="#file-{{ $next.Name }}" class="{{ $iconstyle }}">{{ i "arrow-down" "w-4 h-4" }}</a> 55 - {{ end }} 56 - </div> 57 - 13 + <details {{ if not (.Status.IsOnlyInOne) }}open{{end}} id="file-{{ .Name }}" class="border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 14 + <summary class="list-none cursor-pointer sticky top-0"> 15 + <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 16 + <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 17 + <div class="flex gap-1 items-center" style="direction: ltr;"> 18 + {{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }} 19 + {{ if .Status.IsOk }} 20 + <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">CHANGED</span> 21 + {{ else if .Status.IsUnchanged }} 22 + <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">UNCHANGED</span> 23 + {{ else if .Status.IsOnlyInOne }} 24 + <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">REVERTED</span> 25 + {{ else if .Status.IsOnlyInTwo }} 26 + <span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">NEW</span> 27 + {{ else if .Status.IsRebased }} 28 + <span class="bg-amber-100 text-amber-700 dark:bg-amber-800/50 dark:text-amber-400 {{ $markerstyle }}">REBASED</span> 29 + {{ else }} 30 + <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">ERROR</span> 31 + {{ end }} 58 32 </div> 59 - </summary> 60 33 61 - <div class="transition-all duration-700 ease-in-out"> 62 - {{ if .Status.IsUnchanged }} 63 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 64 - This file has not been changed. 65 - </p> 66 - {{ else if .Status.IsRebased }} 67 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 68 - This patch was likely rebased, as context lines do not match. 69 - </p> 70 - {{ else if .Status.IsError }} 71 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 72 - Failed to calculate interdiff for this file. 73 - </p> 74 - {{ else }} 75 - {{ if $isSplit }} 76 - {{- template "repo/fragments/splitDiff" .Split -}} 77 - {{ else }} 78 - {{- template "repo/fragments/unifiedDiff" . -}} 79 - {{ end }} 80 - {{- end -}} 34 + <div class="flex gap-2 items-center overflow-x-auto" style="direction: rtl;">{{ .Name }}</div> 81 35 </div> 82 36 83 - </details> 37 + </div> 38 + </summary> 84 39 40 + <div class="transition-all duration-700 ease-in-out"> 41 + {{ if .Status.IsUnchanged }} 42 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 43 + This file has not been changed. 44 + </p> 45 + {{ else if .Status.IsRebased }} 46 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 47 + This patch was likely rebased, as context lines do not match. 48 + </p> 49 + {{ else if .Status.IsError }} 50 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 51 + Failed to calculate interdiff for this file. 52 + </p> 53 + {{ else }} 54 + {{ if $isSplit }} 55 + {{- template "repo/fragments/splitDiff" .Split -}} 56 + {{ else }} 57 + {{- template "repo/fragments/unifiedDiff" . -}} 58 + {{ end }} 59 + {{- end -}} 85 60 </div> 86 - </div> 87 - </section> 61 + 62 + </details> 88 63 {{ end }} 89 64 {{ end }} 90 65 </div>
+4
appview/pages/templates/repo/fragments/shortTime.html
··· 1 + {{ define "repo/fragments/shortTime" }} 2 + {{ template "repo/fragments/timeWrapper" (dict "Time" . "Content" (. | shortRelTimeFmt)) }} 3 + {{ end }} 4 +
+4
appview/pages/templates/repo/fragments/shortTimeAgo.html
··· 1 + {{ define "repo/fragments/shortTimeAgo" }} 2 + {{ template "repo/fragments/timeWrapper" (dict "Time" . "Content" (print (. | shortRelTimeFmt) " ago")) }} 3 + {{ end }} 4 +
-16
appview/pages/templates/repo/fragments/time.html
··· 1 - {{ define "repo/fragments/timeWrapper" }} 2 - <time datetime="{{ .Time | iso8601DateTimeFmt }}" title="{{ .Time | longTimeFmt }}">{{ .Content }}</time> 3 - {{ end }} 4 - 5 1 {{ define "repo/fragments/time" }} 6 2 {{ template "repo/fragments/timeWrapper" (dict "Time" . "Content" (. | relTimeFmt)) }} 7 3 {{ end }} 8 - 9 - {{ define "repo/fragments/shortTime" }} 10 - {{ template "repo/fragments/timeWrapper" (dict "Time" . "Content" (. | shortRelTimeFmt)) }} 11 - {{ end }} 12 - 13 - {{ define "repo/fragments/shortTimeAgo" }} 14 - {{ template "repo/fragments/timeWrapper" (dict "Time" . "Content" (print (. | shortRelTimeFmt) " ago")) }} 15 - {{ end }} 16 - 17 - {{ define "repo/fragments/duration" }} 18 - <time datetime="{{ . | iso8601DurationFmt }}" title="{{ . | longDurationFmt }}">{{ . | durationFmt }}</time> 19 - {{ end }}
+5
appview/pages/templates/repo/fragments/timeWrapper.html
··· 1 + {{ define "repo/fragments/timeWrapper" }} 2 + <time datetime="{{ .Time | iso8601DateTimeFmt }}" title="{{ .Time | longTimeFmt }}">{{ .Content }}</time> 3 + {{ end }} 4 + 5 +
+2 -2
appview/pages/templates/repo/pulls/fragments/pullCompareForks.html
··· 19 19 > 20 20 <option disabled selected>select a fork</option> 21 21 {{ range .Forks }} 22 - <option value="{{ .Name }}" {{ if eq .Name $.Selected }}selected{{ end }} class="py-1"> 23 - {{ .Name }} 22 + <option value="{{ .Did }}/{{ .Name }}" {{ if eq .Name $.Selected }}selected{{ end }} class="py-1"> 23 + {{ .Did | resolve }}/{{ .Name }} 24 24 </option> 25 25 {{ end }} 26 26 </select>
+1 -1
appview/pages/templates/repo/pulls/fragments/pullHeader.html
··· 17 17 {{ $icon = "git-merge" }} 18 18 {{ end }} 19 19 20 - {{ $owner := resolve .Pull.OwnerDid }} 21 20 <section class="mt-2"> 22 21 <div class="flex items-center gap-2"> 23 22 <div ··· 45 44 <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> 46 45 {{ if .Pull.IsForkBased }} 47 46 {{ if .Pull.PullSource.Repo }} 47 + {{ $owner := resolve .Pull.PullSource.Repo.Did }} 48 48 <a href="/{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}</a>: 49 49 {{- else -}} 50 50 <span class="italic">[deleted fork]</span>
+1 -1
appview/pages/templates/repo/pulls/fragments/pullStack.html
··· 52 52 </div> 53 53 {{ end }} 54 54 <div class="{{ if not $isCurrent }} pl-6 {{ end }} flex-grow min-w-0 w-full py-2"> 55 - {{ template "repo/pulls/fragments/summarizedHeader" (list $pull $pipeline) }} 55 + {{ template "repo/pulls/fragments/summarizedPullHeader" (list $pull $pipeline) }} 56 56 </div> 57 57 </div> 58 58 </a>
+1 -1
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
··· 1 - {{ define "repo/pulls/fragments/summarizedHeader" }} 1 + {{ define "repo/pulls/fragments/summarizedPullHeader" }} 2 2 {{ $pull := index . 0 }} 3 3 {{ $pipeline := index . 1 }} 4 4 {{ with $pull }}
+2 -2
appview/pages/templates/repo/pulls/interdiff.html
··· 30 30 31 31 {{ define "topbarLayout" }} 32 32 <header class="px-1 col-span-full" style="z-index: 20;"> 33 - {{ template "layouts/topbar" . }} 33 + {{ template "layouts/fragments/topbar" . }} 34 34 </header> 35 35 {{ end }} 36 36 ··· 55 55 56 56 {{ define "footerLayout" }} 57 57 <footer class="px-1 col-span-full mt-12"> 58 - {{ template "layouts/footer" . }} 58 + {{ template "layouts/fragments/footer" . }} 59 59 </footer> 60 60 {{ end }} 61 61
+2 -2
appview/pages/templates/repo/pulls/patch.html
··· 36 36 37 37 {{ define "topbarLayout" }} 38 38 <header class="px-1 col-span-full" style="z-index: 20;"> 39 - {{ template "layouts/topbar" . }} 39 + {{ template "layouts/fragments/topbar" . }} 40 40 </header> 41 41 {{ end }} 42 42 ··· 61 61 62 62 {{ define "footerLayout" }} 63 63 <footer class="px-1 col-span-full mt-12"> 64 - {{ template "layouts/footer" . }} 64 + {{ template "layouts/fragments/footer" . }} 65 65 </footer> 66 66 {{ end }} 67 67
+1 -1
appview/pages/templates/repo/pulls/pulls.html
··· 144 144 <a href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $pull.PullId }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 145 145 <div class="flex gap-2 items-center px-6"> 146 146 <div class="flex-grow min-w-0 w-full py-2"> 147 - {{ template "repo/pulls/fragments/summarizedHeader" (list $pull $pipeline) }} 147 + {{ template "repo/pulls/fragments/summarizedPullHeader" (list $pull $pipeline) }} 148 148 </div> 149 149 </div> 150 150 </a>
-4
appview/pages/templates/strings/put.html
··· 1 1 {{ define "title" }}publish a new string{{ end }} 2 2 3 - {{ define "topbar" }} 4 - {{ template "layouts/topbar" $ }} 5 - {{ end }} 6 - 7 3 {{ define "content" }} 8 4 <div class="px-6 py-2 mb-4"> 9 5 {{ if eq .Action "new" }}
-4
appview/pages/templates/strings/string.html
··· 8 8 <meta property="og:description" content="{{ .String.Description }}" /> 9 9 {{ end }} 10 10 11 - {{ define "topbar" }} 12 - {{ template "layouts/topbar" $ }} 13 - {{ end }} 14 - 15 11 {{ define "content" }} 16 12 {{ $ownerId := didOrHandle .Owner.DID.String .Owner.Handle.String }} 17 13 <section id="string-header" class="mb-4 py-2 px-6 dark:text-white">
-4
appview/pages/templates/strings/timeline.html
··· 1 1 {{ define "title" }} all strings {{ end }} 2 2 3 - {{ define "topbar" }} 4 - {{ template "layouts/topbar" $ }} 5 - {{ end }} 6 - 7 3 {{ define "content" }} 8 4 {{ block "timeline" $ }}{{ end }} 9 5 {{ end }}
+4 -16
appview/pages/templates/user/followers.html
··· 1 1 {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท followers {{ end }} 2 2 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s followers" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=followers" /> 7 - <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 - {{ end }} 9 - 10 - {{ define "content" }} 11 - <div class="grid grid-cols-1 md:grid-cols-11 gap-4"> 12 - <div class="md:col-span-3 order-1 md:order-1"> 13 - {{ template "user/fragments/profileCard" .Card }} 14 - </div> 15 - <div id="all-followers" class="md:col-span-8 order-2 md:order-2"> 16 - {{ block "followers" . }}{{ end }} 17 - </div> 18 - </div> 3 + {{ define "profileContent" }} 4 + <div id="all-followers" class="md:col-span-8 order-2 md:order-2"> 5 + {{ block "followers" . }}{{ end }} 6 + </div> 19 7 {{ end }} 20 8 21 9 {{ define "followers" }}
+4 -16
appview/pages/templates/user/following.html
··· 1 1 {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท following {{ end }} 2 2 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s following" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=following" /> 7 - <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 - {{ end }} 9 - 10 - {{ define "content" }} 11 - <div class="grid grid-cols-1 md:grid-cols-11 gap-4"> 12 - <div class="md:col-span-3 order-1 md:order-1"> 13 - {{ template "user/fragments/profileCard" .Card }} 14 - </div> 15 - <div id="all-following" class="md:col-span-8 order-2 md:order-2"> 16 - {{ block "following" . }}{{ end }} 17 - </div> 18 - </div> 3 + {{ define "profileContent" }} 4 + <div id="all-following" class="md:col-span-8 order-2 md:order-2"> 5 + {{ block "following" . }}{{ end }} 6 + </div> 19 7 {{ end }} 20 8 21 9 {{ define "following" }}
+1 -1
appview/pages/templates/user/fragments/editBio.html
··· 13 13 <label class="m-0 p-0" for="description">bio</label> 14 14 <textarea 15 15 type="text" 16 - class="py-1 px-1 w-full" 16 + class="p-2 w-full" 17 17 name="description" 18 18 rows="3" 19 19 placeholder="write a bio">{{ $description }}</textarea>
+2 -4
appview/pages/templates/user/fragments/profileCard.html
··· 1 1 {{ define "user/fragments/profileCard" }} 2 2 {{ $userIdent := didOrHandle .UserDid .UserHandle }} 3 - <div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit"> 4 3 <div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center"> 5 4 <div id="avatar" class="col-span-1 flex justify-center items-center"> 6 5 <div class="w-3/4 aspect-square relative"> ··· 85 84 <div id="update-profile" class="text-red-400 dark:text-red-500"></div> 86 85 </div> 87 86 </div> 88 - </div> 89 87 {{ end }} 90 88 91 89 {{ define "followerFollowing" }} ··· 94 92 {{ with $root }} 95 93 <div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 96 94 <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 97 - <span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .FollowersCount }} followers</a></span> 95 + <span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .Stats.FollowersCount }} followers</a></span> 98 96 <span class="select-none after:content-['ยท']"></span> 99 - <span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span> 97 + <span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .Stats.FollowingCount }} following</a></span> 100 98 </div> 101 99 {{ end }} 102 100 {{ end }}
+258
appview/pages/templates/user/overview.html
··· 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }} 2 + 3 + {{ define "profileContent" }} 4 + <div id="all-repos" class="md:col-span-4 order-2 md:order-2"> 5 + <div class="grid grid-cols-1 gap-4"> 6 + {{ block "ownRepos" . }}{{ end }} 7 + {{ block "collaboratingRepos" . }}{{ end }} 8 + </div> 9 + </div> 10 + <div class="md:col-span-4 order-3 md:order-3"> 11 + {{ block "profileTimeline" . }}{{ end }} 12 + </div> 13 + {{ end }} 14 + 15 + {{ define "profileTimeline" }} 16 + <p class="text-sm font-bold px-2 pb-4 dark:text-white">ACTIVITY</p> 17 + <div class="flex flex-col gap-4 relative"> 18 + {{ if .ProfileTimeline.IsEmpty }} 19 + <p class="dark:text-white">This user does not have any activity yet.</p> 20 + {{ end }} 21 + 22 + {{ with .ProfileTimeline }} 23 + {{ range $idx, $byMonth := .ByMonth }} 24 + {{ with $byMonth }} 25 + {{ if not .IsEmpty }} 26 + <div class="border border-gray-200 dark:border-gray-700 rounded-sm py-4 px-6"> 27 + <p class="text-sm font-mono mb-2 text-gray-500 dark:text-gray-400"> 28 + {{ if eq $idx 0 }} 29 + this month 30 + {{ else }} 31 + {{$idx}} month{{if ne $idx 1}}s{{end}} ago 32 + {{ end }} 33 + </p> 34 + 35 + <div class="flex flex-col gap-1"> 36 + {{ block "repoEvents" .RepoEvents }} {{ end }} 37 + {{ block "issueEvents" .IssueEvents }} {{ end }} 38 + {{ block "pullEvents" .PullEvents }} {{ end }} 39 + </div> 40 + </div> 41 + {{ end }} 42 + {{ end }} 43 + {{ end }} 44 + {{ end }} 45 + </div> 46 + {{ end }} 47 + 48 + {{ define "repoEvents" }} 49 + {{ if gt (len .) 0 }} 50 + <details> 51 + <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 52 + <div class="flex flex-wrap items-center gap-2"> 53 + {{ i "book-plus" "w-4 h-4" }} 54 + created {{ len . }} {{if eq (len .) 1 }}repository{{else}}repositories{{end}} 55 + </div> 56 + </summary> 57 + <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 58 + {{ range . }} 59 + <div class="flex flex-wrap items-center gap-2"> 60 + <span class="text-gray-500 dark:text-gray-400"> 61 + {{ if .Source }} 62 + {{ i "git-fork" "w-4 h-4" }} 63 + {{ else }} 64 + {{ i "book-plus" "w-4 h-4" }} 65 + {{ end }} 66 + </span> 67 + <a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 68 + {{- .Repo.Name -}} 69 + </a> 70 + </div> 71 + {{ end }} 72 + </div> 73 + </details> 74 + {{ end }} 75 + {{ end }} 76 + 77 + {{ define "issueEvents" }} 78 + {{ $items := .Items }} 79 + {{ $stats := .Stats }} 80 + 81 + {{ if gt (len $items) 0 }} 82 + <details> 83 + <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 84 + <div class="flex flex-wrap items-center gap-2"> 85 + {{ i "circle-dot" "w-4 h-4" }} 86 + 87 + <div> 88 + created {{ len $items }} {{if eq (len $items) 1 }}issue{{else}}issues{{end}} 89 + </div> 90 + 91 + {{ if gt $stats.Open 0 }} 92 + <span class="px-2 py-1/2 text-sm rounded text-white bg-green-600 dark:bg-green-700"> 93 + {{$stats.Open}} open 94 + </span> 95 + {{ end }} 96 + 97 + {{ if gt $stats.Closed 0 }} 98 + <span class="px-2 py-1/2 text-sm rounded text-white bg-gray-800 dark:bg-gray-700"> 99 + {{$stats.Closed}} closed 100 + </span> 101 + {{ end }} 102 + 103 + </div> 104 + </summary> 105 + <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 106 + {{ range $items }} 107 + {{ $repoOwner := resolve .Metadata.Repo.Did }} 108 + {{ $repoName := .Metadata.Repo.Name }} 109 + {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 110 + 111 + <div class="flex gap-2 text-gray-600 dark:text-gray-300"> 112 + {{ if .Open }} 113 + <span class="text-green-600 dark:text-green-500"> 114 + {{ i "circle-dot" "w-4 h-4" }} 115 + </span> 116 + {{ else }} 117 + <span class="text-gray-500 dark:text-gray-400"> 118 + {{ i "ban" "w-4 h-4" }} 119 + </span> 120 + {{ end }} 121 + <div class="flex-none min-w-8 text-right"> 122 + <span class="text-gray-500 dark:text-gray-400">#{{ .IssueId }}</span> 123 + </div> 124 + <div class="break-words max-w-full"> 125 + <a href="/{{$repoUrl}}/issues/{{ .IssueId }}" class="no-underline hover:underline"> 126 + {{ .Title -}} 127 + </a> 128 + on 129 + <a href="/{{$repoUrl}}" class="no-underline hover:underline whitespace-nowrap"> 130 + {{$repoUrl}} 131 + </a> 132 + </div> 133 + </div> 134 + {{ end }} 135 + </div> 136 + </details> 137 + {{ end }} 138 + {{ end }} 139 + 140 + {{ define "pullEvents" }} 141 + {{ $items := .Items }} 142 + {{ $stats := .Stats }} 143 + {{ if gt (len $items) 0 }} 144 + <details> 145 + <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 146 + <div class="flex flex-wrap items-center gap-2"> 147 + {{ i "git-pull-request" "w-4 h-4" }} 148 + 149 + <div> 150 + created {{ len $items }} {{if eq (len $items) 1 }}pull request{{else}}pull requests{{end}} 151 + </div> 152 + 153 + {{ if gt $stats.Open 0 }} 154 + <span class="px-2 py-1/2 text-sm rounded text-white bg-green-600 dark:bg-green-700"> 155 + {{$stats.Open}} open 156 + </span> 157 + {{ end }} 158 + 159 + {{ if gt $stats.Merged 0 }} 160 + <span class="px-2 py-1/2 text-sm rounded text-white bg-purple-600 dark:bg-purple-700"> 161 + {{$stats.Merged}} merged 162 + </span> 163 + {{ end }} 164 + 165 + 166 + {{ if gt $stats.Closed 0 }} 167 + <span class="px-2 py-1/2 text-sm rounded text-white bg-gray-800 dark:bg-gray-700"> 168 + {{$stats.Closed}} closed 169 + </span> 170 + {{ end }} 171 + 172 + </div> 173 + </summary> 174 + <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 175 + {{ range $items }} 176 + {{ $repoOwner := resolve .Repo.Did }} 177 + {{ $repoName := .Repo.Name }} 178 + {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 179 + 180 + <div class="flex gap-2 text-gray-600 dark:text-gray-300"> 181 + {{ if .State.IsOpen }} 182 + <span class="text-green-600 dark:text-green-500"> 183 + {{ i "git-pull-request" "w-4 h-4" }} 184 + </span> 185 + {{ else if .State.IsMerged }} 186 + <span class="text-purple-600 dark:text-purple-500"> 187 + {{ i "git-merge" "w-4 h-4" }} 188 + </span> 189 + {{ else }} 190 + <span class="text-gray-600 dark:text-gray-300"> 191 + {{ i "git-pull-request-closed" "w-4 h-4" }} 192 + </span> 193 + {{ end }} 194 + <div class="flex-none min-w-8 text-right"> 195 + <span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span> 196 + </div> 197 + <div class="break-words max-w-full"> 198 + <a href="/{{$repoUrl}}/pulls/{{ .PullId }}" class="no-underline hover:underline"> 199 + {{ .Title -}} 200 + </a> 201 + on 202 + <a href="/{{$repoUrl}}" class="no-underline hover:underline whitespace-nowrap"> 203 + {{$repoUrl}} 204 + </a> 205 + </div> 206 + </div> 207 + {{ end }} 208 + </div> 209 + </details> 210 + {{ end }} 211 + {{ end }} 212 + 213 + {{ define "ownRepos" }} 214 + <div> 215 + <div class="text-sm font-bold px-2 pb-4 dark:text-white flex items-center gap-2"> 216 + <a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos" 217 + class="flex text-black dark:text-white items-center gap-2 no-underline hover:no-underline group"> 218 + <span>PINNED REPOS</span> 219 + </a> 220 + {{ if and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }} 221 + <button 222 + hx-get="profile/edit-pins" 223 + hx-target="#all-repos" 224 + class="py-0 font-normal text-sm flex gap-2 items-center group"> 225 + {{ i "pencil" "w-3 h-3" }} 226 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 227 + </button> 228 + {{ end }} 229 + </div> 230 + <div id="repos" class="grid grid-cols-1 gap-4 items-stretch"> 231 + {{ range .Repos }} 232 + <div class="border border-gray-200 dark:border-gray-700 rounded-sm"> 233 + {{ template "user/fragments/repoCard" (list $ . false) }} 234 + </div> 235 + {{ else }} 236 + <p class="dark:text-white">This user does not have any pinned repos.</p> 237 + {{ end }} 238 + </div> 239 + </div> 240 + {{ end }} 241 + 242 + {{ define "collaboratingRepos" }} 243 + {{ if gt (len .CollaboratingRepos) 0 }} 244 + <div> 245 + <p class="text-sm font-bold px-2 pb-4 dark:text-white">COLLABORATING ON</p> 246 + <div id="collaborating" class="grid grid-cols-1 gap-4"> 247 + {{ range .CollaboratingRepos }} 248 + <div class="border border-gray-200 dark:border-gray-700 rounded-sm"> 249 + {{ template "user/fragments/repoCard" (list $ . true) }} 250 + </div> 251 + {{ else }} 252 + <p class="px-6 dark:text-white">This user is not collaborating.</p> 253 + {{ end }} 254 + </div> 255 + </div> 256 + {{ end }} 257 + {{ end }} 258 +
-318
appview/pages/templates/user/profile.html
··· 1 - {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }} 2 - 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}" /> 5 - <meta property="og:type" content="profile" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}" /> 7 - <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 - {{ end }} 9 - 10 - {{ define "content" }} 11 - <div class="grid grid-cols-1 md:grid-cols-11 gap-4"> 12 - <div class="md:col-span-3 order-1 md:order-1"> 13 - <div class="grid grid-cols-1 gap-4"> 14 - {{ template "user/fragments/profileCard" .Card }} 15 - {{ block "punchcard" .Punchcard }} {{ end }} 16 - </div> 17 - </div> 18 - <div id="all-repos" class="md:col-span-4 order-2 md:order-2"> 19 - <div class="grid grid-cols-1 gap-4"> 20 - {{ block "ownRepos" . }}{{ end }} 21 - {{ block "collaboratingRepos" . }}{{ end }} 22 - </div> 23 - </div> 24 - <div class="md:col-span-4 order-3 md:order-3"> 25 - {{ block "profileTimeline" . }}{{ end }} 26 - </div> 27 - </div> 28 - {{ end }} 29 - 30 - {{ define "profileTimeline" }} 31 - <p class="text-sm font-bold p-2 dark:text-white">ACTIVITY</p> 32 - <div class="flex flex-col gap-4 relative"> 33 - {{ with .ProfileTimeline }} 34 - {{ range $idx, $byMonth := .ByMonth }} 35 - {{ with $byMonth }} 36 - <div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm"> 37 - {{ if eq $idx 0 }} 38 - 39 - {{ else }} 40 - {{ $s := "s" }} 41 - {{ if eq $idx 1 }} 42 - {{ $s = "" }} 43 - {{ end }} 44 - <p class="text-sm font-bold dark:text-white mb-2">{{$idx}} month{{$s}} ago</p> 45 - {{ end }} 46 - 47 - {{ if .IsEmpty }} 48 - <div class="text-gray-500 dark:text-gray-400"> 49 - No activity for this month 50 - </div> 51 - {{ else }} 52 - <div class="flex flex-col gap-1"> 53 - {{ block "repoEvents" .RepoEvents }} {{ end }} 54 - {{ block "issueEvents" .IssueEvents }} {{ end }} 55 - {{ block "pullEvents" .PullEvents }} {{ end }} 56 - </div> 57 - {{ end }} 58 - </div> 59 - 60 - {{ end }} 61 - {{ else }} 62 - <p class="dark:text-white">This user does not have any activity yet.</p> 63 - {{ end }} 64 - {{ end }} 65 - </div> 66 - {{ end }} 67 - 68 - {{ define "repoEvents" }} 69 - {{ if gt (len .) 0 }} 70 - <details> 71 - <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 72 - <div class="flex flex-wrap items-center gap-2"> 73 - {{ i "book-plus" "w-4 h-4" }} 74 - created {{ len . }} {{if eq (len .) 1 }}repository{{else}}repositories{{end}} 75 - </div> 76 - </summary> 77 - <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 78 - {{ range . }} 79 - <div class="flex flex-wrap items-center gap-2"> 80 - <span class="text-gray-500 dark:text-gray-400"> 81 - {{ if .Source }} 82 - {{ i "git-fork" "w-4 h-4" }} 83 - {{ else }} 84 - {{ i "book-plus" "w-4 h-4" }} 85 - {{ end }} 86 - </span> 87 - <a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 88 - {{- .Repo.Name -}} 89 - </a> 90 - </div> 91 - {{ end }} 92 - </div> 93 - </details> 94 - {{ end }} 95 - {{ end }} 96 - 97 - {{ define "issueEvents" }} 98 - {{ $items := .Items }} 99 - {{ $stats := .Stats }} 100 - 101 - {{ if gt (len $items) 0 }} 102 - <details> 103 - <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 104 - <div class="flex flex-wrap items-center gap-2"> 105 - {{ i "circle-dot" "w-4 h-4" }} 106 - 107 - <div> 108 - created {{ len $items }} {{if eq (len $items) 1 }}issue{{else}}issues{{end}} 109 - </div> 110 - 111 - {{ if gt $stats.Open 0 }} 112 - <span class="px-2 py-1/2 text-sm rounded text-white bg-green-600 dark:bg-green-700"> 113 - {{$stats.Open}} open 114 - </span> 115 - {{ end }} 116 - 117 - {{ if gt $stats.Closed 0 }} 118 - <span class="px-2 py-1/2 text-sm rounded text-white bg-gray-800 dark:bg-gray-700"> 119 - {{$stats.Closed}} closed 120 - </span> 121 - {{ end }} 122 - 123 - </div> 124 - </summary> 125 - <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 126 - {{ range $items }} 127 - {{ $repoOwner := resolve .Metadata.Repo.Did }} 128 - {{ $repoName := .Metadata.Repo.Name }} 129 - {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 130 - 131 - <div class="flex gap-2 text-gray-600 dark:text-gray-300"> 132 - {{ if .Open }} 133 - <span class="text-green-600 dark:text-green-500"> 134 - {{ i "circle-dot" "w-4 h-4" }} 135 - </span> 136 - {{ else }} 137 - <span class="text-gray-500 dark:text-gray-400"> 138 - {{ i "ban" "w-4 h-4" }} 139 - </span> 140 - {{ end }} 141 - <div class="flex-none min-w-8 text-right"> 142 - <span class="text-gray-500 dark:text-gray-400">#{{ .IssueId }}</span> 143 - </div> 144 - <div class="break-words max-w-full"> 145 - <a href="/{{$repoUrl}}/issues/{{ .IssueId }}" class="no-underline hover:underline"> 146 - {{ .Title -}} 147 - </a> 148 - on 149 - <a href="/{{$repoUrl}}" class="no-underline hover:underline whitespace-nowrap"> 150 - {{$repoUrl}} 151 - </a> 152 - </div> 153 - </div> 154 - {{ end }} 155 - </div> 156 - </details> 157 - {{ end }} 158 - {{ end }} 159 - 160 - {{ define "pullEvents" }} 161 - {{ $items := .Items }} 162 - {{ $stats := .Stats }} 163 - {{ if gt (len $items) 0 }} 164 - <details> 165 - <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 166 - <div class="flex flex-wrap items-center gap-2"> 167 - {{ i "git-pull-request" "w-4 h-4" }} 168 - 169 - <div> 170 - created {{ len $items }} {{if eq (len $items) 1 }}pull request{{else}}pull requests{{end}} 171 - </div> 172 - 173 - {{ if gt $stats.Open 0 }} 174 - <span class="px-2 py-1/2 text-sm rounded text-white bg-green-600 dark:bg-green-700"> 175 - {{$stats.Open}} open 176 - </span> 177 - {{ end }} 178 - 179 - {{ if gt $stats.Merged 0 }} 180 - <span class="px-2 py-1/2 text-sm rounded text-white bg-purple-600 dark:bg-purple-700"> 181 - {{$stats.Merged}} merged 182 - </span> 183 - {{ end }} 184 - 185 - 186 - {{ if gt $stats.Closed 0 }} 187 - <span class="px-2 py-1/2 text-sm rounded text-white bg-gray-800 dark:bg-gray-700"> 188 - {{$stats.Closed}} closed 189 - </span> 190 - {{ end }} 191 - 192 - </div> 193 - </summary> 194 - <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 195 - {{ range $items }} 196 - {{ $repoOwner := resolve .Repo.Did }} 197 - {{ $repoName := .Repo.Name }} 198 - {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 199 - 200 - <div class="flex gap-2 text-gray-600 dark:text-gray-300"> 201 - {{ if .State.IsOpen }} 202 - <span class="text-green-600 dark:text-green-500"> 203 - {{ i "git-pull-request" "w-4 h-4" }} 204 - </span> 205 - {{ else if .State.IsMerged }} 206 - <span class="text-purple-600 dark:text-purple-500"> 207 - {{ i "git-merge" "w-4 h-4" }} 208 - </span> 209 - {{ else }} 210 - <span class="text-gray-600 dark:text-gray-300"> 211 - {{ i "git-pull-request-closed" "w-4 h-4" }} 212 - </span> 213 - {{ end }} 214 - <div class="flex-none min-w-8 text-right"> 215 - <span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span> 216 - </div> 217 - <div class="break-words max-w-full"> 218 - <a href="/{{$repoUrl}}/pulls/{{ .PullId }}" class="no-underline hover:underline"> 219 - {{ .Title -}} 220 - </a> 221 - on 222 - <a href="/{{$repoUrl}}" class="no-underline hover:underline whitespace-nowrap"> 223 - {{$repoUrl}} 224 - </a> 225 - </div> 226 - </div> 227 - {{ end }} 228 - </div> 229 - </details> 230 - {{ end }} 231 - {{ end }} 232 - 233 - {{ define "ownRepos" }} 234 - <div> 235 - <div class="text-sm font-bold p-2 pr-0 dark:text-white flex items-center justify-between gap-2"> 236 - <a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos" 237 - class="flex text-black dark:text-white items-center gap-2 no-underline hover:no-underline group"> 238 - <span>PINNED REPOS</span> 239 - <span class="flex gap-1 items-center font-normal text-sm text-gray-500 dark:text-gray-400 "> 240 - view all {{ i "chevron-right" "w-4 h-4" }} 241 - </span> 242 - </a> 243 - {{ if and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }} 244 - <button 245 - hx-get="profile/edit-pins" 246 - hx-target="#all-repos" 247 - class="btn py-0 font-normal text-sm flex gap-2 items-center group"> 248 - {{ i "pencil" "w-3 h-3" }} 249 - edit 250 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 251 - </button> 252 - {{ end }} 253 - </div> 254 - <div id="repos" class="grid grid-cols-1 gap-4 items-stretch"> 255 - {{ range .Repos }} 256 - {{ template "user/fragments/repoCard" (list $ . false) }} 257 - {{ else }} 258 - <p class="px-6 dark:text-white">This user does not have any repos yet.</p> 259 - {{ end }} 260 - </div> 261 - </div> 262 - {{ end }} 263 - 264 - {{ define "collaboratingRepos" }} 265 - {{ if gt (len .CollaboratingRepos) 0 }} 266 - <div> 267 - <p class="text-sm font-bold p-2 dark:text-white">COLLABORATING ON</p> 268 - <div id="collaborating" class="grid grid-cols-1 gap-4"> 269 - {{ range .CollaboratingRepos }} 270 - {{ template "user/fragments/repoCard" (list $ . true) }} 271 - {{ else }} 272 - <p class="px-6 dark:text-white">This user is not collaborating.</p> 273 - {{ end }} 274 - </div> 275 - </div> 276 - {{ end }} 277 - {{ end }} 278 - 279 - {{ define "punchcard" }} 280 - {{ $now := now }} 281 - <div> 282 - <p class="p-2 flex gap-2 text-sm font-bold dark:text-white"> 283 - PUNCHCARD 284 - <span class="font-normal text-sm text-gray-500 dark:text-gray-400 "> 285 - {{ .Total | int64 | commaFmt }} commits 286 - </span> 287 - </p> 288 - <div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm"> 289 - <div class="grid grid-cols-28 md:grid-cols-14 gap-y-2 w-full h-full"> 290 - {{ range .Punches }} 291 - {{ $count := .Count }} 292 - {{ $theme := "bg-gray-200 dark:bg-gray-700 size-[4px]" }} 293 - {{ if lt $count 1 }} 294 - {{ $theme = "bg-gray-200 dark:bg-gray-700 size-[4px]" }} 295 - {{ else if lt $count 2 }} 296 - {{ $theme = "bg-green-200 dark:bg-green-900 size-[5px]" }} 297 - {{ else if lt $count 4 }} 298 - {{ $theme = "bg-green-300 dark:bg-green-800 size-[5px]" }} 299 - {{ else if lt $count 8 }} 300 - {{ $theme = "bg-green-400 dark:bg-green-700 size-[6px]" }} 301 - {{ else }} 302 - {{ $theme = "bg-green-500 dark:bg-green-600 size-[7px]" }} 303 - {{ end }} 304 - 305 - {{ if .Date.After $now }} 306 - {{ $theme = "border border-gray-200 dark:border-gray-700 size-[4px]" }} 307 - {{ end }} 308 - <div class="w-full h-full flex justify-center items-center"> 309 - <div 310 - class="aspect-square rounded-full transition-all duration-300 {{ $theme }} max-w-full max-h-full" 311 - title="{{ .Date.Format "2006-01-02" }}: {{ .Count }} commits"> 312 - </div> 313 - </div> 314 - {{ end }} 315 - </div> 316 - </div> 317 - </div> 318 - {{ end }}
+7 -18
appview/pages/templates/user/repos.html
··· 1 1 {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท repos {{ end }} 2 2 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s repos" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=repos" /> 7 - <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 - {{ end }} 9 - 10 - {{ define "content" }} 11 - <div class="grid grid-cols-1 md:grid-cols-11 gap-4"> 12 - <div class="md:col-span-3 order-1 md:order-1"> 13 - {{ template "user/fragments/profileCard" .Card }} 14 - </div> 15 - <div id="all-repos" class="md:col-span-8 order-2 md:order-2"> 16 - {{ block "ownRepos" . }}{{ end }} 17 - </div> 18 - </div> 3 + {{ define "profileContent" }} 4 + <div id="all-repos" class="md:col-span-8 order-2 md:order-2"> 5 + {{ block "ownRepos" . }}{{ end }} 6 + </div> 19 7 {{ end }} 20 8 21 9 {{ define "ownRepos" }} 22 - <p class="text-sm font-bold p-2 dark:text-white">ALL REPOSITORIES</p> 23 10 <div id="repos" class="grid grid-cols-1 gap-4 mb-6"> 24 11 {{ range .Repos }} 25 - {{ template "user/fragments/repoCard" (list $ . false) }} 12 + <div class="border border-gray-200 dark:border-gray-700 rounded-sm"> 13 + {{ template "user/fragments/repoCard" (list $ . false) }} 14 + </div> 26 15 {{ else }} 27 16 <p class="px-6 dark:text-white">This user does not have any repos yet.</p> 28 17 {{ end }}
+19
appview/pages/templates/user/starred.html
··· 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท repos {{ end }} 2 + 3 + {{ define "profileContent" }} 4 + <div id="all-repos" class="md:col-span-8 order-2 md:order-2"> 5 + {{ block "starredRepos" . }}{{ end }} 6 + </div> 7 + {{ end }} 8 + 9 + {{ define "starredRepos" }} 10 + <div id="repos" class="grid grid-cols-1 gap-4 mb-6"> 11 + {{ range .Repos }} 12 + <div class="border border-gray-200 dark:border-gray-700 rounded-sm"> 13 + {{ template "user/fragments/repoCard" (list $ . true) }} 14 + </div> 15 + {{ else }} 16 + <p class="px-6 dark:text-white">This user does not have any starred repos yet.</p> 17 + {{ end }} 18 + </div> 19 + {{ end }}
+45
appview/pages/templates/user/strings.html
··· 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท strings {{ end }} 2 + 3 + {{ define "profileContent" }} 4 + <div id="all-strings" class="md:col-span-8 order-2 md:order-2"> 5 + {{ block "allStrings" . }}{{ end }} 6 + </div> 7 + {{ end }} 8 + 9 + {{ define "allStrings" }} 10 + <div id="strings" class="grid grid-cols-1 gap-4 mb-6"> 11 + {{ range .Strings }} 12 + <div class="border border-gray-200 dark:border-gray-700 rounded-sm"> 13 + {{ template "singleString" (list $ .) }} 14 + </div> 15 + {{ else }} 16 + <p class="px-6 dark:text-white">This user does not have any strings yet.</p> 17 + {{ end }} 18 + </div> 19 + {{ end }} 20 + 21 + {{ define "singleString" }} 22 + {{ $root := index . 0 }} 23 + {{ $s := index . 1 }} 24 + <div class="py-4 px-6 rounded bg-white dark:bg-gray-800"> 25 + <div class="font-medium dark:text-white flex gap-2 items-center"> 26 + <a href="/strings/{{ or $root.Card.UserHandle $root.Card.UserDid }}/{{ $s.Rkey }}">{{ $s.Filename }}</a> 27 + </div> 28 + {{ with $s.Description }} 29 + <div class="text-gray-600 dark:text-gray-300 text-sm"> 30 + {{ . }} 31 + </div> 32 + {{ end }} 33 + 34 + {{ $stat := $s.Stats }} 35 + <div class="text-gray-400 pt-4 text-sm font-mono inline-flex gap-2 mt-auto"> 36 + <span>{{ $stat.LineCount }} line{{if ne $stat.LineCount 1}}s{{end}}</span> 37 + <span class="select-none [&:before]:content-['ยท']"></span> 38 + {{ with $s.Edited }} 39 + <span>edited {{ template "repo/fragments/shortTimeAgo" . }}</span> 40 + {{ else }} 41 + {{ template "repo/fragments/shortTimeAgo" $s.Created }} 42 + {{ end }} 43 + </div> 44 + </div> 45 + {{ end }}
+24 -21
appview/pulls/pulls.go
··· 605 605 defer tx.Rollback() 606 606 607 607 createdAt := time.Now().Format(time.RFC3339) 608 - ownerDid := user.Did 609 608 610 609 pullAt, err := db.GetPullAt(s.db, f.RepoAt(), pull.PullId) 611 610 if err != nil { ··· 614 613 return 615 614 } 616 615 617 - atUri := f.RepoAt().String() 618 616 client, err := s.oauth.AuthorizedClient(r) 619 617 if err != nil { 620 618 log.Println("failed to get authorized client", err) ··· 627 625 Rkey: tid.TID(), 628 626 Record: &lexutil.LexiconTypeDecoder{ 629 627 Val: &tangled.RepoPullComment{ 630 - Repo: &atUri, 631 628 Pull: string(pullAt), 632 - Owner: &ownerDid, 633 629 Body: body, 634 630 CreatedAt: createdAt, 635 631 }, ··· 854 850 } 855 851 856 852 func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 857 - fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 853 + repoString := strings.SplitN(forkRepo, "/", 2) 854 + forkOwnerDid := repoString[0] 855 + repoName := repoString[1] 856 + fork, err := db.GetForkByDid(s.db, forkOwnerDid, repoName) 858 857 if errors.Is(err, sql.ErrNoRows) { 859 858 s.pages.Notice(w, "pull", "No such fork.") 860 859 return ··· 912 911 // hiddenRef: hidden/feature-1/main (on repo-fork) 913 912 // targetBranch: main (on repo-1) 914 913 // sourceBranch: feature-1 (on repo-fork) 915 - comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch) 914 + comparison, err := us.Compare(fork.Did, fork.Name, hiddenRef, sourceBranch) 916 915 if err != nil { 917 916 log.Println("failed to compare across branches", err) 918 917 s.pages.Notice(w, "pull", err.Error()) ··· 1038 1037 Rkey: rkey, 1039 1038 Record: &lexutil.LexiconTypeDecoder{ 1040 1039 Val: &tangled.RepoPull{ 1041 - Title: title, 1042 - PullId: int64(pullId), 1043 - TargetRepo: string(f.RepoAt()), 1044 - TargetBranch: targetBranch, 1045 - Patch: patch, 1046 - Source: recordPullSource, 1040 + Title: title, 1041 + Target: &tangled.RepoPull_Target{ 1042 + Repo: string(f.RepoAt()), 1043 + Branch: targetBranch, 1044 + }, 1045 + Patch: patch, 1046 + Source: recordPullSource, 1047 1047 }, 1048 1048 }, 1049 1049 }) ··· 1274 1274 } 1275 1275 1276 1276 forkVal := r.URL.Query().Get("fork") 1277 - 1277 + repoString := strings.SplitN(forkVal, "/", 2) 1278 + forkOwnerDid := repoString[0] 1279 + forkName := repoString[1] 1278 1280 // fork repo 1279 - repo, err := db.GetRepo(s.db, user.Did, forkVal) 1281 + repo, err := db.GetRepo(s.db, forkOwnerDid, forkName) 1280 1282 if err != nil { 1281 1283 log.Println("failed to get repo", user.Did, forkVal) 1282 1284 return ··· 1289 1291 return 1290 1292 } 1291 1293 1292 - sourceResult, err := sourceBranchesClient.Branches(user.Did, repo.Name) 1294 + sourceResult, err := sourceBranchesClient.Branches(forkOwnerDid, repo.Name) 1293 1295 if err != nil { 1294 1296 log.Println("failed to reach knotserver for source branches", err) 1295 1297 return ··· 1609 1611 SwapRecord: ex.Cid, 1610 1612 Record: &lexutil.LexiconTypeDecoder{ 1611 1613 Val: &tangled.RepoPull{ 1612 - Title: pull.Title, 1613 - PullId: int64(pull.PullId), 1614 - TargetRepo: string(f.RepoAt()), 1615 - TargetBranch: pull.TargetBranch, 1616 - Patch: patch, // new patch 1617 - Source: recordPullSource, 1614 + Title: pull.Title, 1615 + Target: &tangled.RepoPull_Target{ 1616 + Repo: string(f.RepoAt()), 1617 + Branch: pull.TargetBranch, 1618 + }, 1619 + Patch: patch, // new patch 1620 + Source: recordPullSource, 1618 1621 }, 1619 1622 }, 1620 1623 })
+191 -132
appview/state/profile.go
··· 17 17 "github.com/gorilla/feeds" 18 18 "tangled.sh/tangled.sh/core/api/tangled" 19 19 "tangled.sh/tangled.sh/core/appview/db" 20 - "tangled.sh/tangled.sh/core/appview/oauth" 20 + // "tangled.sh/tangled.sh/core/appview/oauth" 21 21 "tangled.sh/tangled.sh/core/appview/pages" 22 22 ) 23 23 24 24 func (s *State) Profile(w http.ResponseWriter, r *http.Request) { 25 25 tabVal := r.URL.Query().Get("tab") 26 26 switch tabVal { 27 - case "": 28 - s.profileHomePage(w, r) 29 27 case "repos": 30 28 s.reposPage(w, r) 31 29 case "followers": 32 30 s.followersPage(w, r) 33 31 case "following": 34 32 s.followingPage(w, r) 33 + case "starred": 34 + s.starredPage(w, r) 35 + case "strings": 36 + s.stringsPage(w, r) 37 + default: 38 + s.profileOverview(w, r) 35 39 } 36 40 } 37 41 38 - type ProfilePageParams struct { 39 - Id identity.Identity 40 - LoggedInUser *oauth.User 41 - Card pages.ProfileCard 42 - } 43 - 44 - func (s *State) profilePage(w http.ResponseWriter, r *http.Request) *ProfilePageParams { 42 + func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { 45 43 didOrHandle := chi.URLParam(r, "user") 46 44 if didOrHandle == "" { 47 - http.Error(w, "bad request", http.StatusBadRequest) 48 - return nil 45 + return nil, fmt.Errorf("empty DID or handle") 49 46 } 50 47 51 48 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 52 49 if !ok { 53 - log.Printf("malformed middleware") 54 - w.WriteHeader(http.StatusInternalServerError) 55 - return nil 50 + return nil, fmt.Errorf("failed to resolve ID") 56 51 } 57 52 did := ident.DID.String() 58 53 59 54 profile, err := db.GetProfile(s.db, did) 60 55 if err != nil { 61 - log.Printf("getting profile data for %s: %s", did, err) 62 - s.pages.Error500(w) 63 - return nil 56 + return nil, fmt.Errorf("failed to get profile: %w", err) 57 + } 58 + 59 + repoCount, err := db.CountRepos(s.db, db.FilterEq("did", did)) 60 + if err != nil { 61 + return nil, fmt.Errorf("failed to get repo count: %w", err) 62 + } 63 + 64 + stringCount, err := db.CountStrings(s.db, db.FilterEq("did", did)) 65 + if err != nil { 66 + return nil, fmt.Errorf("failed to get string count: %w", err) 67 + } 68 + 69 + starredCount, err := db.CountStars(s.db, db.FilterEq("starred_by_did", did)) 70 + if err != nil { 71 + return nil, fmt.Errorf("failed to get starred repo count: %w", err) 64 72 } 65 73 66 74 followStats, err := db.GetFollowerFollowingCount(s.db, did) 67 75 if err != nil { 68 - log.Printf("getting follow stats for %s: %s", did, err) 76 + return nil, fmt.Errorf("failed to get follower stats: %w", err) 69 77 } 70 78 71 79 loggedInUser := s.oauth.GetUser(r) ··· 74 82 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 75 83 } 76 84 77 - return &ProfilePageParams{ 78 - Id: ident, 79 - LoggedInUser: loggedInUser, 80 - Card: pages.ProfileCard{ 81 - UserDid: did, 82 - UserHandle: ident.Handle.String(), 83 - Profile: profile, 84 - FollowStatus: followStatus, 85 + now := time.Now() 86 + startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 87 + punchcard, err := db.MakePunchcard( 88 + s.db, 89 + db.FilterEq("did", did), 90 + db.FilterGte("date", startOfYear.Format(time.DateOnly)), 91 + db.FilterLte("date", now.Format(time.DateOnly)), 92 + ) 93 + if err != nil { 94 + return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err) 95 + } 96 + 97 + return &pages.ProfileCard{ 98 + UserDid: did, 99 + UserHandle: ident.Handle.String(), 100 + Profile: profile, 101 + FollowStatus: followStatus, 102 + Stats: pages.ProfileStats{ 103 + RepoCount: repoCount, 104 + StringCount: stringCount, 105 + StarredCount: starredCount, 85 106 FollowersCount: followStats.Followers, 86 107 FollowingCount: followStats.Following, 87 108 }, 88 - } 109 + Punchcard: punchcard, 110 + }, nil 89 111 } 90 112 91 - func (s *State) profileHomePage(w http.ResponseWriter, r *http.Request) { 92 - pageWithProfile := s.profilePage(w, r) 93 - if pageWithProfile == nil { 113 + func (s *State) profileOverview(w http.ResponseWriter, r *http.Request) { 114 + l := s.logger.With("handler", "profileHomePage") 115 + 116 + profile, err := s.profile(r) 117 + if err != nil { 118 + l.Error("failed to build profile card", "err", err) 119 + s.pages.Error500(w) 94 120 return 95 121 } 122 + l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 96 123 97 - id := pageWithProfile.Id 98 124 repos, err := db.GetRepos( 99 125 s.db, 100 126 0, 101 - db.FilterEq("did", id.DID), 127 + db.FilterEq("did", profile.UserDid), 102 128 ) 103 129 if err != nil { 104 - log.Printf("getting repos for %s: %s", id.DID, err) 130 + l.Error("failed to fetch repos", "err", err) 105 131 } 106 132 107 - profile := pageWithProfile.Card.Profile 108 133 // filter out ones that are pinned 109 134 pinnedRepos := []db.Repo{} 110 135 for i, r := range repos { 111 136 // if this is a pinned repo, add it 112 - if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) { 137 + if slices.Contains(profile.Profile.PinnedRepos[:], r.RepoAt()) { 113 138 pinnedRepos = append(pinnedRepos, r) 114 139 } 115 140 116 141 // if there are no saved pins, add the first 4 repos 117 - if profile.IsPinnedReposEmpty() && i < 4 { 142 + if profile.Profile.IsPinnedReposEmpty() && i < 4 { 118 143 pinnedRepos = append(pinnedRepos, r) 119 144 } 120 145 } 121 146 122 - collaboratingRepos, err := db.CollaboratingIn(s.db, id.DID.String()) 147 + collaboratingRepos, err := db.CollaboratingIn(s.db, profile.UserDid) 123 148 if err != nil { 124 - log.Printf("getting collaborating repos for %s: %s", id.DID, err) 149 + l.Error("failed to fetch collaborating repos", "err", err) 125 150 } 126 151 127 152 pinnedCollaboratingRepos := []db.Repo{} 128 153 for _, r := range collaboratingRepos { 129 154 // if this is a pinned repo, add it 130 - if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) { 155 + if slices.Contains(profile.Profile.PinnedRepos[:], r.RepoAt()) { 131 156 pinnedCollaboratingRepos = append(pinnedCollaboratingRepos, r) 132 157 } 133 158 } 134 159 135 - timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 160 + timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid) 136 161 if err != nil { 137 - log.Printf("failed to create profile timeline for %s: %s", id.DID, err) 162 + l.Error("failed to create timeline", "err", err) 138 163 } 139 164 140 - var didsToResolve []string 141 - for _, r := range collaboratingRepos { 142 - didsToResolve = append(didsToResolve, r.Did) 143 - } 144 - for _, byMonth := range timeline.ByMonth { 145 - for _, pe := range byMonth.PullEvents.Items { 146 - didsToResolve = append(didsToResolve, pe.Repo.Did) 147 - } 148 - for _, ie := range byMonth.IssueEvents.Items { 149 - didsToResolve = append(didsToResolve, ie.Metadata.Repo.Did) 150 - } 151 - for _, re := range byMonth.RepoEvents { 152 - didsToResolve = append(didsToResolve, re.Repo.Did) 153 - if re.Source != nil { 154 - didsToResolve = append(didsToResolve, re.Source.Did) 155 - } 156 - } 165 + s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 166 + LoggedInUser: s.oauth.GetUser(r), 167 + Card: profile, 168 + Repos: pinnedRepos, 169 + CollaboratingRepos: pinnedCollaboratingRepos, 170 + ProfileTimeline: timeline, 171 + }) 172 + } 173 + 174 + func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 175 + l := s.logger.With("handler", "reposPage") 176 + 177 + profile, err := s.profile(r) 178 + if err != nil { 179 + l.Error("failed to build profile card", "err", err) 180 + s.pages.Error500(w) 181 + return 157 182 } 183 + l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 158 184 159 - now := time.Now() 160 - startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 161 - punchcard, err := db.MakePunchcard( 185 + repos, err := db.GetRepos( 162 186 s.db, 163 - db.FilterEq("did", id.DID), 164 - db.FilterGte("date", startOfYear.Format(time.DateOnly)), 165 - db.FilterLte("date", now.Format(time.DateOnly)), 187 + 0, 188 + db.FilterEq("did", profile.UserDid), 166 189 ) 167 190 if err != nil { 168 - log.Println("failed to get punchcard for did", "did", id.DID, "err", err) 191 + l.Error("failed to get repos", "err", err) 192 + s.pages.Error500(w) 193 + return 169 194 } 170 195 171 - s.pages.ProfileHomePage(w, pages.ProfileHomePageParams{ 172 - LoggedInUser: pageWithProfile.LoggedInUser, 173 - Repos: pinnedRepos, 174 - CollaboratingRepos: pinnedCollaboratingRepos, 175 - Card: pageWithProfile.Card, 176 - Punchcard: punchcard, 177 - ProfileTimeline: timeline, 196 + err = s.pages.ProfileRepos(w, pages.ProfileReposParams{ 197 + LoggedInUser: s.oauth.GetUser(r), 198 + Repos: repos, 199 + Card: profile, 178 200 }) 179 201 } 180 202 181 - func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 182 - pageWithProfile := s.profilePage(w, r) 183 - if pageWithProfile == nil { 203 + func (s *State) starredPage(w http.ResponseWriter, r *http.Request) { 204 + l := s.logger.With("handler", "starredPage") 205 + 206 + profile, err := s.profile(r) 207 + if err != nil { 208 + l.Error("failed to build profile card", "err", err) 209 + s.pages.Error500(w) 210 + return 211 + } 212 + l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 213 + 214 + stars, err := db.GetStars(s.db, 0, db.FilterEq("starred_by_did", profile.UserDid)) 215 + if err != nil { 216 + l.Error("failed to get stars", "err", err) 217 + s.pages.Error500(w) 184 218 return 185 219 } 220 + var repoAts []string 221 + for _, s := range stars { 222 + repoAts = append(repoAts, string(s.RepoAt)) 223 + } 186 224 187 - id := pageWithProfile.Id 188 225 repos, err := db.GetRepos( 189 226 s.db, 190 227 0, 191 - db.FilterEq("did", id.DID), 228 + db.FilterIn("at_uri", repoAts), 192 229 ) 193 230 if err != nil { 194 - log.Printf("getting repos for %s: %s", id.DID, err) 231 + l.Error("failed to get repos", "err", err) 232 + s.pages.Error500(w) 233 + return 195 234 } 196 235 197 - s.pages.ReposPage(w, pages.ReposPageParams{ 198 - LoggedInUser: pageWithProfile.LoggedInUser, 236 + err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{ 237 + LoggedInUser: s.oauth.GetUser(r), 199 238 Repos: repos, 200 - Card: pageWithProfile.Card, 239 + Card: profile, 240 + }) 241 + } 242 + 243 + func (s *State) stringsPage(w http.ResponseWriter, r *http.Request) { 244 + l := s.logger.With("handler", "stringsPage") 245 + 246 + profile, err := s.profile(r) 247 + if err != nil { 248 + l.Error("failed to build profile card", "err", err) 249 + s.pages.Error500(w) 250 + return 251 + } 252 + l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 253 + 254 + strings, err := db.GetStrings(s.db, 0, db.FilterEq("did", profile.UserDid)) 255 + if err != nil { 256 + l.Error("failed to get strings", "err", err) 257 + s.pages.Error500(w) 258 + return 259 + } 260 + 261 + err = s.pages.ProfileStrings(w, pages.ProfileStringsParams{ 262 + LoggedInUser: s.oauth.GetUser(r), 263 + Strings: strings, 264 + Card: profile, 201 265 }) 202 266 } 203 267 204 268 type FollowsPageParams struct { 205 - LoggedInUser *oauth.User 206 - Follows []pages.FollowCard 207 - Card pages.ProfileCard 269 + Follows []pages.FollowCard 270 + Card *pages.ProfileCard 208 271 } 209 272 210 - func (s *State) followPage(w http.ResponseWriter, r *http.Request, fetchFollows func(db.Execer, string) ([]db.Follow, error), extractDid func(db.Follow) string) (FollowsPageParams, error) { 211 - pageWithProfile := s.profilePage(w, r) 212 - if pageWithProfile == nil { 213 - return FollowsPageParams{}, nil 273 + func (s *State) followPage( 274 + r *http.Request, 275 + fetchFollows func(db.Execer, string) ([]db.Follow, error), 276 + extractDid func(db.Follow) string, 277 + ) (*FollowsPageParams, error) { 278 + l := s.logger.With("handler", "reposPage") 279 + 280 + profile, err := s.profile(r) 281 + if err != nil { 282 + return nil, err 214 283 } 284 + l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 215 285 216 - id := pageWithProfile.Id 217 - loggedInUser := pageWithProfile.LoggedInUser 286 + loggedInUser := s.oauth.GetUser(r) 218 287 219 - follows, err := fetchFollows(s.db, id.DID.String()) 288 + follows, err := fetchFollows(s.db, profile.UserDid) 220 289 if err != nil { 221 - log.Printf("getting followers for %s: %s", id.DID, err) 222 - return FollowsPageParams{}, err 290 + l.Error("failed to fetch follows", "err", err) 291 + return nil, err 223 292 } 224 293 225 294 if len(follows) == 0 { 226 - return FollowsPageParams{ 227 - LoggedInUser: loggedInUser, 228 - Follows: []pages.FollowCard{}, 229 - Card: pageWithProfile.Card, 230 - }, nil 295 + return nil, nil 231 296 } 232 297 233 298 followDids := make([]string, 0, len(follows)) ··· 237 302 238 303 profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids)) 239 304 if err != nil { 240 - log.Printf("getting profile for %s: %s", followDids, err) 241 - return FollowsPageParams{}, err 305 + l.Error("failed to get profiles", "followDids", followDids, "err", err) 306 + return nil, err 242 307 } 243 308 244 309 followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids) ··· 246 311 log.Printf("getting follow counts for %s: %s", followDids, err) 247 312 } 248 313 249 - var loggedInUserFollowing map[string]struct{} 314 + loggedInUserFollowing := make(map[string]struct{}) 250 315 if loggedInUser != nil { 251 316 following, err := db.GetFollowing(s.db, loggedInUser.Did) 252 317 if err != nil { 253 - return FollowsPageParams{}, err 318 + l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did) 319 + return nil, err 254 320 } 255 - if len(following) > 0 { 256 - loggedInUserFollowing = make(map[string]struct{}, len(following)) 257 - for _, follow := range following { 258 - loggedInUserFollowing[follow.SubjectDid] = struct{}{} 259 - } 321 + loggedInUserFollowing = make(map[string]struct{}, len(following)) 322 + for _, follow := range following { 323 + loggedInUserFollowing[follow.SubjectDid] = struct{}{} 260 324 } 261 325 } 262 326 263 - followCards := make([]pages.FollowCard, 0, len(follows)) 264 - for _, did := range followDids { 265 - followStats, exists := followStatsMap[did] 266 - if !exists { 267 - followStats = db.FollowStats{} 268 - } 327 + followCards := make([]pages.FollowCard, len(follows)) 328 + for i, did := range followDids { 329 + followStats := followStatsMap[did] 269 330 followStatus := db.IsNotFollowing 270 - if loggedInUserFollowing != nil { 271 - if _, exists := loggedInUserFollowing[did]; exists { 272 - followStatus = db.IsFollowing 273 - } else if loggedInUser.Did == did { 274 - followStatus = db.IsSelf 275 - } 331 + if _, exists := loggedInUserFollowing[did]; exists { 332 + followStatus = db.IsFollowing 333 + } else if loggedInUser != nil && loggedInUser.Did == did { 334 + followStatus = db.IsSelf 276 335 } 336 + 277 337 var profile *db.Profile 278 338 if p, exists := profiles[did]; exists { 279 339 profile = p ··· 281 341 profile = &db.Profile{} 282 342 profile.Did = did 283 343 } 284 - followCards = append(followCards, pages.FollowCard{ 344 + followCards[i] = pages.FollowCard{ 285 345 UserDid: did, 286 346 FollowStatus: followStatus, 287 347 FollowersCount: followStats.Followers, 288 348 FollowingCount: followStats.Following, 289 349 Profile: profile, 290 - }) 350 + } 291 351 } 292 352 293 - return FollowsPageParams{ 294 - LoggedInUser: loggedInUser, 295 - Follows: followCards, 296 - Card: pageWithProfile.Card, 353 + return &FollowsPageParams{ 354 + Follows: followCards, 355 + Card: profile, 297 356 }, nil 298 357 } 299 358 300 359 func (s *State) followersPage(w http.ResponseWriter, r *http.Request) { 301 - followPage, err := s.followPage(w, r, db.GetFollowers, func(f db.Follow) string { return f.UserDid }) 360 + followPage, err := s.followPage(r, db.GetFollowers, func(f db.Follow) string { return f.UserDid }) 302 361 if err != nil { 303 362 s.pages.Notice(w, "all-followers", "Failed to load followers") 304 363 return 305 364 } 306 365 307 - s.pages.FollowersPage(w, pages.FollowersPageParams{ 308 - LoggedInUser: followPage.LoggedInUser, 366 + s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{ 367 + LoggedInUser: s.oauth.GetUser(r), 309 368 Followers: followPage.Follows, 310 369 Card: followPage.Card, 311 370 }) 312 371 } 313 372 314 373 func (s *State) followingPage(w http.ResponseWriter, r *http.Request) { 315 - followPage, err := s.followPage(w, r, db.GetFollowing, func(f db.Follow) string { return f.SubjectDid }) 374 + followPage, err := s.followPage(r, db.GetFollowing, func(f db.Follow) string { return f.SubjectDid }) 316 375 if err != nil { 317 376 s.pages.Notice(w, "all-following", "Failed to load following") 318 377 return 319 378 } 320 379 321 - s.pages.FollowingPage(w, pages.FollowingPageParams{ 322 - LoggedInUser: followPage.LoggedInUser, 380 + s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{ 381 + LoggedInUser: s.oauth.GetUser(r), 323 382 Following: followPage.Follows, 324 383 Card: followPage.Card, 325 384 })
+2
appview/state/state.go
··· 99 99 tangled.SpindleMemberNSID, 100 100 tangled.SpindleNSID, 101 101 tangled.StringNSID, 102 + tangled.RepoIssueNSID, 103 + tangled.RepoIssueCommentNSID, 102 104 }, 103 105 nil, 104 106 slog.Default(),
+1 -59
appview/strings/strings.go
··· 5 5 "log/slog" 6 6 "net/http" 7 7 "path" 8 - "slices" 9 8 "strconv" 10 9 "time" 11 10 ··· 161 160 } 162 161 163 162 func (s *Strings) dashboard(w http.ResponseWriter, r *http.Request) { 164 - l := s.Logger.With("handler", "dashboard") 165 - 166 - id, ok := r.Context().Value("resolvedId").(identity.Identity) 167 - if !ok { 168 - l.Error("malformed middleware") 169 - w.WriteHeader(http.StatusInternalServerError) 170 - return 171 - } 172 - l = l.With("did", id.DID, "handle", id.Handle) 173 - 174 - all, err := db.GetStrings( 175 - s.Db, 176 - 0, 177 - db.FilterEq("did", id.DID), 178 - ) 179 - if err != nil { 180 - l.Error("failed to fetch strings", "err", err) 181 - w.WriteHeader(http.StatusInternalServerError) 182 - return 183 - } 184 - 185 - slices.SortFunc(all, func(a, b db.String) int { 186 - if a.Created.After(b.Created) { 187 - return -1 188 - } else { 189 - return 1 190 - } 191 - }) 192 - 193 - profile, err := db.GetProfile(s.Db, id.DID.String()) 194 - if err != nil { 195 - l.Error("failed to fetch user profile", "err", err) 196 - w.WriteHeader(http.StatusInternalServerError) 197 - return 198 - } 199 - loggedInUser := s.OAuth.GetUser(r) 200 - followStatus := db.IsNotFollowing 201 - if loggedInUser != nil { 202 - followStatus = db.GetFollowStatus(s.Db, loggedInUser.Did, id.DID.String()) 203 - } 204 - 205 - followStats, err := db.GetFollowerFollowingCount(s.Db, id.DID.String()) 206 - if err != nil { 207 - l.Error("failed to get follow stats", "err", err) 208 - } 209 - 210 - s.Pages.StringsDashboard(w, pages.StringsDashboardParams{ 211 - LoggedInUser: s.OAuth.GetUser(r), 212 - Card: pages.ProfileCard{ 213 - UserDid: id.DID.String(), 214 - UserHandle: id.Handle.String(), 215 - Profile: profile, 216 - FollowStatus: followStatus, 217 - FollowersCount: followStats.Followers, 218 - FollowingCount: followStats.Following, 219 - }, 220 - Strings: all, 221 - }) 163 + http.Redirect(w, r, fmt.Sprintf("/%s?tab=strings", chi.URLParam(r, "user")), http.StatusFound) 222 164 } 223 165 224 166 func (s *Strings) edit(w http.ResponseWriter, r *http.Request) {
+5 -4
cmd/gen.go
··· 18 18 tangled.FeedReaction{}, 19 19 tangled.FeedStar{}, 20 20 tangled.GitRefUpdate{}, 21 + tangled.GitRefUpdate_CommitCountBreakdown{}, 22 + tangled.GitRefUpdate_IndividualEmailCommitCount{}, 23 + tangled.GitRefUpdate_LangBreakdown{}, 24 + tangled.GitRefUpdate_IndividualLanguageSize{}, 21 25 tangled.GitRefUpdate_Meta{}, 22 - tangled.GitRefUpdate_Meta_CommitCount{}, 23 - tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem{}, 24 - tangled.GitRefUpdate_Meta_LangBreakdown{}, 25 - tangled.GitRefUpdate_Pair{}, 26 26 tangled.GraphFollow{}, 27 27 tangled.Knot{}, 28 28 tangled.KnotMember{}, ··· 47 47 tangled.RepoPullComment{}, 48 48 tangled.RepoPull_Source{}, 49 49 tangled.RepoPullStatus{}, 50 + tangled.RepoPull_Target{}, 50 51 tangled.Spindle{}, 51 52 tangled.SpindleMember{}, 52 53 tangled.String{},
+3 -3
docs/contributing.md
··· 11 11 ### message format 12 12 13 13 ``` 14 - <service/top-level directory>: <affected package/directory>: <short summary of change> 14 + <service/top-level directory>/<affected package/directory>: <short summary of change> 15 15 16 16 17 17 Optional longer description can go here, if necessary. Explain what the ··· 23 23 Here are some examples: 24 24 25 25 ``` 26 - appview: state: fix token expiry check in middleware 26 + appview/state: fix token expiry check in middleware 27 27 28 28 The previous check did not account for clock drift, leading to premature 29 29 token invalidation. 30 30 ``` 31 31 32 32 ``` 33 - knotserver: git/service: improve error checking in upload-pack 33 + knotserver/git/service: improve error checking in upload-pack 34 34 ``` 35 35 36 36
+53 -12
docs/hacking.md
··· 48 48 redis-server 49 49 ``` 50 50 51 - ## running a knot 51 + ## running knots and spindles 52 52 53 53 An end-to-end knot setup requires setting up a machine with 54 54 `sshd`, `AuthorizedKeysCommand`, and git user, which is 55 55 quite cumbersome. So the nix flake provides a 56 56 `nixosConfiguration` to do so. 57 57 58 - To begin, grab your DID from http://localhost:3000/settings. 59 - Then, set `TANGLED_VM_KNOT_OWNER` and 60 - `TANGLED_VM_SPINDLE_OWNER` to your DID. 58 + <details> 59 + <summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary> 60 + 61 + In order to build Tangled's dev VM on macOS, you will 62 + first need to set up a Linux Nix builder. The recommended 63 + way to do so is to run a [`darwin.linux-builder` 64 + VM](https://nixos.org/manual/nixpkgs/unstable/#sec-darwin-builder) 65 + and to register it in `nix.conf` as a builder for Linux 66 + with the same architecture as your Mac (`linux-aarch64` if 67 + you are using Apple Silicon). 68 + 69 + > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside 70 + > the tangled repo so that it doesn't conflict with the other VM. For example, 71 + > you can do 72 + > 73 + > ```shell 74 + > cd $(mktemp -d buildervm.XXXXX) && nix run nixpkgs#darwin.linux-builder 75 + > ``` 76 + > 77 + > to store the builder VM in a temporary dir. 78 + > 79 + > You should read and follow [all the other intructions][darwin builder vm] to 80 + > avoid subtle problems. 81 + 82 + Alternatively, you can use any other method to set up a 83 + Linux machine with `nix` installed that you can `sudo ssh` 84 + into (in other words, root user on your Mac has to be able 85 + to ssh into the Linux machine without entering a password) 86 + and that has the same architecture as your Mac. See 87 + [remote builder 88 + instructions](https://nix.dev/manual/nix/2.28/advanced-topics/distributed-builds.html#requirements) 89 + for how to register such a builder in `nix.conf`. 61 90 62 - If you don't want to [set up a spindle](#running-a-spindle), 63 - you can use any placeholder value. 91 + > WARNING: If you'd like to use 92 + > [`nixos-lima`](https://github.com/nixos-lima/nixos-lima) or 93 + > [Orbstack](https://orbstack.dev/), note that setting them up so that `sudo 94 + > ssh` works can be tricky. It seems to be [possible with 95 + > Orbstack](https://github.com/orgs/orbstack/discussions/1669). 64 96 65 - You can now start a lightweight NixOS VM like so: 97 + </details> 98 + 99 + To begin, grab your DID from http://localhost:3000/settings. 100 + Then, set `TANGLED_VM_KNOT_OWNER` and 101 + `TANGLED_VM_SPINDLE_OWNER` to your DID. You can now start a 102 + lightweight NixOS VM like so: 66 103 67 104 ```bash 68 105 nix run --impure .#vm ··· 74 111 with `ssh` exposed on port 2222. 75 112 76 113 Once the services are running, head to 77 - http://localhost:3000/knots and hit verify (and similarly, 78 - http://localhost:3000/spindles to verify your spindle). It 79 - should verify the ownership of the services instantly if 80 - everything went smoothly. 114 + http://localhost:3000/knots and hit verify. It should 115 + verify the ownership of the services instantly if everything 116 + went smoothly. 81 117 82 118 You can push repositories to this VM with this ssh config 83 119 block on your main machine: ··· 97 133 git push local-dev main 98 134 ``` 99 135 100 - ## running a spindle 136 + ### running a spindle 101 137 102 138 The above VM should already be running a spindle on 103 139 `localhost:6555`. Head to http://localhost:3000/spindles and ··· 119 155 # litecli has a nicer REPL interface: 120 156 litecli /var/lib/spindle/spindle.db 121 157 ``` 158 + 159 + If for any reason you wish to disable either one of the 160 + services in the VM, modify [nix/vm.nix](/nix/vm.nix) and set 161 + `services.tangled-spindle.enable` (or 162 + `services.tangled-knot.enable`) to `false`.
+130 -54
docs/spindle/pipeline.md
··· 1 - # spindle pipeline manifest 1 + # spindle pipelines 2 + 3 + Spindle workflows allow you to write CI/CD pipelines in a simple format. They're located in the `.tangled/workflows` directory at the root of your repository, and are defined using YAML. 4 + 5 + The fields are: 2 6 3 - Spindle pipelines are defined under the `.tangled/workflows` directory in a 4 - repo. Generally: 7 + - [Trigger](#trigger): A **required** field that defines when a workflow should be triggered. 8 + - [Engine](#engine): A **required** field that defines which engine a workflow should run on. 9 + - [Clone options](#clone-options): An **optional** field that defines how the repository should be cloned. 10 + - [Dependencies](#dependencies): An **optional** field that allows you to list dependencies you may need. 11 + - [Environment](#environment): An **optional** field that allows you to define environment variables. 12 + - [Steps](#steps): An **optional** field that allows you to define what steps should run in the workflow. 5 13 6 - * Pipelines are defined in YAML. 7 - * Workflows can run using different *engines*. 14 + ## Trigger 8 15 9 - The most barebones workflow looks like this: 16 + The first thing to add to a workflow is the trigger, which defines when a workflow runs. This is defined using a `when` field, which takes in a list of conditions. Each condition has the following fields: 17 + 18 + - `event`: This is a **required** field that defines when your workflow should run. It's a list that can take one or more of the following values: 19 + - `push`: The workflow should run every time a commit is pushed to the repository. 20 + - `pull_request`: The workflow should run every time a pull request is made or updated. 21 + - `manual`: The workflow can be triggered manually. 22 + - `branch`: This is a **required** field that defines which branches the workflow should run for. If used with the `push` event, commits to the branch(es) listed here will trigger the workflow. If used with the `pull_request` event, updates to pull requests targeting the branch(es) listed here will trigger the workflow. This field has no effect with the `manual` event. 23 + 24 + For example, if you'd like define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with: 10 25 11 26 ```yaml 12 27 when: 13 - - event: ["push"] 28 + - event: ["push", "manual"] 29 + branch: ["main", "develop"] 30 + - event: ["pull_request"] 14 31 branch: ["main"] 32 + ``` 15 33 34 + ## Engine 35 + 36 + Next is the engine on which the workflow should run, defined using the **required** `engine` field. The currently supported engines are: 37 + 38 + - `nixery`: This uses an instance of [Nixery](https://nixery.dev) to run steps, which allows you to add [dependencies](#dependencies) from [Nixpkgs](https://github.com/NixOS/nixpkgs). You can search for packages on https://search.nixos.org, and there's a pretty good chance the package(s) you're looking for will be there. 39 + 40 + Example: 41 + 42 + ```yaml 16 43 engine: "nixery" 44 + ``` 45 + 46 + ## Clone options 17 47 18 - # optional 48 + When a workflow starts, the first step is to clone the repository. You can customize this behavior using the **optional** `clone` field. It has the following fields: 49 + 50 + - `skip`: Setting this to `true` will skip cloning the repository. This can be useful if your workflow is doing something that doesn't require anything from the repository itself. This is `false` by default. 51 + - `depth`: This sets the number of commits, or the "clone depth", to fetch from the repository. For example, if you set this to 2, the last 2 commits will be fetched. By default, the depth is set to 1, meaning only the most recent commit will be fetched, which is the commit that triggered the workflow. 52 + - `submodules`: If you use [git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your repository, setting this field to `true` will recursively fetch all submodules. This is `false` by default. 53 + 54 + The default settings are: 55 + 56 + ```yaml 19 57 clone: 20 58 skip: false 21 - depth: 50 22 - submodules: true 59 + depth: 1 60 + submodules: false 23 61 ``` 24 62 25 - The `when` and `engine` fields are required, while every other aspect 26 - of how the definition is parsed is up to the engine. Currently, a spindle 27 - provides at least one of these built-in engines: 63 + ## Dependencies 28 64 29 - ## `nixery` 65 + Usually when you're running a workflow, you'll need additional dependencies. The `dependencies` field lets you define which dependencies to get, and from where. It's a key-value map, with the key being the registry to fetch dependencies from, and the value being the list of dependencies to fetch. 30 66 31 - The Nixery engine uses an instance of [Nixery](https://nixery.dev) to run 32 - steps that use dependencies from [Nixpkgs](https://github.com/NixOS/nixpkgs). 33 - 34 - Here's an example that uses all fields: 67 + Say you want to fetch Node.js and Go from `nixpkgs`, and a package called `my_pkg` you've made from your own registry at your repository at `https://tangled.sh/@example.com/my_pkg`. You can define those dependencies like so: 35 68 36 69 ```yaml 37 - # build_and_test.yaml 38 - when: 39 - - event: ["push", "pull_request"] 40 - branch: ["main", "develop"] 41 - - event: ["manual"] 42 - 43 70 dependencies: 44 - ## from nixpkgs 71 + # nixpkgs 45 72 nixpkgs: 46 73 - nodejs 47 - ## custom registry 48 - git+https://tangled.sh/@oppi.li/statix: 49 - - statix 74 + - go 75 + # custom registry 76 + git+https://tangled.sh/@example.com/my_pkg: 77 + - my_pkg 78 + ``` 50 79 51 - steps: 52 - - name: "Install dependencies" 53 - command: "npm install" 54 - environment: 55 - NODE_ENV: "development" 56 - CI: "true" 80 + Now these dependencies are available to use in your workflow! 57 81 58 - - name: "Run linter" 59 - command: "npm run lint" 82 + ## Environment 83 + 84 + The `environment` field allows you define environment variables that will be available throughout the entire workflow. **Do not put secrets here, these environment variables are visible to anyone viewing the repository. You can add secrets for pipelines in your repository's settings.** 85 + 86 + Example: 87 + 88 + ```yaml 89 + environment: 90 + GOOS: "linux" 91 + GOARCH: "arm64" 92 + NODE_ENV: "production" 93 + MY_ENV_VAR: "MY_ENV_VALUE" 94 + ``` 60 95 61 - - name: "Run tests" 62 - command: "npm test" 63 - environment: 64 - NODE_ENV: "test" 65 - JEST_WORKERS: "2" 96 + ## Steps 66 97 67 - - name: "Build application" 98 + The `steps` field allows you to define what steps should run in the workflow. It's a list of step objects, each with the following fields: 99 + 100 + - `name`: This field allows you to give your step a name. This name is visible in your workflow runs, and is used to describe what the step is doing. 101 + - `command`: This field allows you to define a command to run in that step. The step is run in a Bash shell, and the logs from the command will be visible in the pipelines page on the Tangled website. The [dependencies](#dependencies) you added will be available to use here. 102 + - `environment`: Similar to the global [environment](#environment) config, this **optional** field is a key-value map that allows you to set environment variables for the step. **Do not put secrets here, these environment variables are visible to anyone viewing the repository. You can add secrets for pipelines in your repository's settings.** 103 + 104 + Example: 105 + 106 + ```yaml 107 + steps: 108 + - name: "Build backend" 109 + command: "go build" 110 + environment: 111 + GOOS: "darwin" 112 + GOARCH: "arm64" 113 + - name: "Build frontend" 68 114 command: "npm run build" 69 115 environment: 70 116 NODE_ENV: "production" 117 + ``` 71 118 72 - environment: 73 - BUILD_NUMBER: "123" 74 - GIT_BRANCH: "main" 119 + ## Complete workflow 75 120 76 - ## current repository is cloned and checked out at the target ref 77 - ## by default. 121 + ```yaml 122 + # .tangled/workflows/build.yml 123 + 124 + when: 125 + - event: ["push", "manual"] 126 + branch: ["main", "develop"] 127 + - event: ["pull_request"] 128 + branch: ["main"] 129 + 130 + engine: "nixery" 131 + 132 + # using the default values 78 133 clone: 79 134 skip: false 80 - depth: 50 81 - submodules: true 82 - ``` 135 + depth: 1 136 + submodules: false 83 137 84 - ## git push options 138 + dependencies: 139 + # nixpkgs 140 + nixpkgs: 141 + - nodejs 142 + - go 143 + # custom registry 144 + git+https://tangled.sh/@example.com/my_pkg: 145 + - my_pkg 85 146 86 - These are push options that can be used with the `--push-option (-o)` flag of git push: 147 + environment: 148 + GOOS: "linux" 149 + GOARCH: "arm64" 150 + NODE_ENV: "production" 151 + MY_ENV_VAR: "MY_ENV_VALUE" 152 + 153 + steps: 154 + - name: "Build backend" 155 + command: "go build" 156 + environment: 157 + GOOS: "darwin" 158 + GOARCH: "arm64" 159 + - name: "Build frontend" 160 + command: "npm run build" 161 + environment: 162 + NODE_ENV: "production" 163 + ``` 87 164 88 - - `verbose-ci`, `ci-verbose`: enables diagnostics reporting for the CI pipeline, allowing you to see any issues when you push. 89 - - `skip-ci`, `ci-skip`: skips triggering the CI pipeline. 165 + If you want another example of a workflow, you can look at the one [Tangled uses to build the project](https://tangled.sh/@tangled.sh/core/blob/master/.tangled/workflows/build.yml).
+3 -1
go.mod
··· 39 39 github.com/stretchr/testify v1.10.0 40 40 github.com/urfave/cli/v3 v3.3.3 41 41 github.com/whyrusleeping/cbor-gen v0.3.1 42 - github.com/yuin/goldmark v1.4.15 42 + github.com/wyatt915/goldmark-treeblood v0.0.0-20250825231212-5dcbdb2f4b57 43 + github.com/yuin/goldmark v1.7.12 43 44 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 44 45 golang.org/x/crypto v0.40.0 45 46 golang.org/x/net v0.42.0 ··· 154 155 github.com/vmihailenco/go-tinylfu v0.2.2 // indirect 155 156 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 156 157 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 158 + github.com/wyatt915/treeblood v0.1.15 // indirect 157 159 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 158 160 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 159 161 go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+6 -1
go.sum
··· 426 426 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 427 427 github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= 428 428 github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 429 + github.com/wyatt915/goldmark-treeblood v0.0.0-20250825231212-5dcbdb2f4b57 h1:UqtQdzLXnvdBdqn/go53qGyncw1wJ7Mq5SQdieM1/Ew= 430 + github.com/wyatt915/goldmark-treeblood v0.0.0-20250825231212-5dcbdb2f4b57/go.mod h1:BxSCWByWSRSuembL3cDG1IBUbkBoO/oW/6tF19aA4hs= 431 + github.com/wyatt915/treeblood v0.1.15 h1:3KZ3o2LpcKZAzOLqMoW9qeUzKEaKArKpbcPpTkNfQC8= 432 + github.com/wyatt915/treeblood v0.1.15/go.mod h1:i7+yhhmzdDP17/97pIsOSffw74EK/xk+qJ0029cSXUY= 429 433 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 430 434 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 431 435 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 432 436 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 433 437 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 434 - github.com/yuin/goldmark v1.4.15 h1:CFa84T0goNn/UIXYS+dmjjVxMyTAvpOmzld40N/nfK0= 435 438 github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 439 + github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY= 440 + github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 436 441 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 437 442 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 438 443 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
+9 -10
knotserver/git/post_receive.go
··· 145 145 } 146 146 147 147 func (m RefUpdateMeta) AsRecord() tangled.GitRefUpdate_Meta { 148 - var byEmail []*tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem 148 + var byEmail []*tangled.GitRefUpdate_IndividualEmailCommitCount 149 149 for e, v := range m.CommitCount.ByEmail { 150 - byEmail = append(byEmail, &tangled.GitRefUpdate_Meta_CommitCount_ByEmail_Elem{ 150 + byEmail = append(byEmail, &tangled.GitRefUpdate_IndividualEmailCommitCount{ 151 151 Email: e, 152 152 Count: int64(v), 153 153 }) 154 154 } 155 155 156 - var langs []*tangled.GitRefUpdate_Pair 156 + var langs []*tangled.GitRefUpdate_IndividualLanguageSize 157 157 for lang, size := range m.LangBreakdown { 158 - langs = append(langs, &tangled.GitRefUpdate_Pair{ 158 + langs = append(langs, &tangled.GitRefUpdate_IndividualLanguageSize{ 159 159 Lang: lang, 160 160 Size: size, 161 161 }) 162 162 } 163 - langBreakdown := &tangled.GitRefUpdate_Meta_LangBreakdown{ 164 - Inputs: langs, 165 - } 166 163 167 164 return tangled.GitRefUpdate_Meta{ 168 - CommitCount: &tangled.GitRefUpdate_Meta_CommitCount{ 165 + CommitCount: &tangled.GitRefUpdate_CommitCountBreakdown{ 169 166 ByEmail: byEmail, 170 167 }, 171 - IsDefaultRef: m.IsDefaultRef, 172 - LangBreakdown: langBreakdown, 168 + IsDefaultRef: m.IsDefaultRef, 169 + LangBreakdown: &tangled.GitRefUpdate_LangBreakdown{ 170 + Inputs: langs, 171 + }, 173 172 } 174 173 }
+4 -4
knotserver/ingester.go
··· 98 98 l := log.FromContext(ctx) 99 99 l = l.With("handler", "processPull") 100 100 l = l.With("did", did) 101 - l = l.With("target_repo", record.TargetRepo) 102 - l = l.With("target_branch", record.TargetBranch) 101 + l = l.With("target_repo", record.Target.Repo) 102 + l = l.With("target_branch", record.Target.Branch) 103 103 104 104 if record.Source == nil { 105 105 return fmt.Errorf("ignoring pull record: not a branch-based pull request") ··· 109 109 return fmt.Errorf("ignoring pull record: fork based pull") 110 110 } 111 111 112 - repoAt, err := syntax.ParseATURI(record.TargetRepo) 112 + repoAt, err := syntax.ParseATURI(record.Target.Repo) 113 113 if err != nil { 114 114 return fmt.Errorf("failed to parse ATURI: %w", err) 115 115 } ··· 178 178 Action: "create", 179 179 SourceBranch: record.Source.Branch, 180 180 SourceSha: record.Source.Sha, 181 - TargetBranch: record.TargetBranch, 181 + TargetBranch: record.Target.Branch, 182 182 } 183 183 184 184 compiler := workflow.Compiler{
+59 -52
lexicons/git/refUpdate.json
··· 51 51 "maxLength": 40 52 52 }, 53 53 "meta": { 54 - "type": "object", 55 - "required": [ 56 - "isDefaultRef", 57 - "commitCount" 58 - ], 59 - "properties": { 60 - "isDefaultRef": { 61 - "type": "boolean", 62 - "default": "false" 63 - }, 64 - "langBreakdown": { 65 - "type": "object", 66 - "properties": { 67 - "inputs": { 68 - "type": "array", 69 - "items": { 70 - "type": "ref", 71 - "ref": "#pair" 72 - } 73 - } 74 - } 75 - }, 76 - "commitCount": { 77 - "type": "object", 78 - "required": [], 79 - "properties": { 80 - "byEmail": { 81 - "type": "array", 82 - "items": { 83 - "type": "object", 84 - "required": [ 85 - "email", 86 - "count" 87 - ], 88 - "properties": { 89 - "email": { 90 - "type": "string" 91 - }, 92 - "count": { 93 - "type": "integer" 94 - } 95 - } 96 - } 97 - } 98 - } 99 - } 100 - } 54 + "type": "ref", 55 + "ref": "#meta" 56 + } 57 + } 58 + } 59 + }, 60 + "meta": { 61 + "type": "object", 62 + "required": ["isDefaultRef", "commitCount"], 63 + "properties": { 64 + "isDefaultRef": { 65 + "type": "boolean", 66 + "default": false 67 + }, 68 + "langBreakdown": { 69 + "type": "ref", 70 + "ref": "#langBreakdown" 71 + }, 72 + "commitCount": { 73 + "type": "ref", 74 + "ref": "#commitCountBreakdown" 75 + } 76 + } 77 + }, 78 + "langBreakdown": { 79 + "type": "object", 80 + "properties": { 81 + "inputs": { 82 + "type": "array", 83 + "items": { 84 + "type": "ref", 85 + "ref": "#individualLanguageSize" 101 86 } 102 87 } 103 88 } 104 89 }, 105 - "pair": { 90 + "individualLanguageSize": { 106 91 "type": "object", 107 - "required": [ 108 - "lang", 109 - "size" 110 - ], 92 + "required": ["lang", "size"], 111 93 "properties": { 112 94 "lang": { 113 95 "type": "string" 114 96 }, 115 97 "size": { 98 + "type": "integer" 99 + } 100 + } 101 + }, 102 + "commitCountBreakdown": { 103 + "type": "object", 104 + "required": [], 105 + "properties": { 106 + "byEmail": { 107 + "type": "array", 108 + "items": { 109 + "type": "ref", 110 + "ref": "#individualEmailCommitCount" 111 + } 112 + } 113 + } 114 + }, 115 + "individualEmailCommitCount": { 116 + "type": "object", 117 + "required": ["email", "count"], 118 + "properties": { 119 + "email": { 120 + "type": "string" 121 + }, 122 + "count": { 116 123 "type": "integer" 117 124 } 118 125 }
+1 -8
lexicons/issue/comment.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": [ 13 - "issue", 14 - "body", 15 - "createdAt" 16 - ], 12 + "required": ["issue", "body", "createdAt"], 17 13 "properties": { 18 14 "issue": { 19 15 "type": "string", ··· 22 18 "repo": { 23 19 "type": "string", 24 20 "format": "at-uri" 25 - }, 26 - "commentId": { 27 - "type": "integer" 28 21 }, 29 22 "owner": { 30 23 "type": "string",
+1 -14
lexicons/issue/issue.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": [ 13 - "repo", 14 - "issueId", 15 - "owner", 16 - "title", 17 - "createdAt" 18 - ], 12 + "required": ["repo", "title", "createdAt"], 19 13 "properties": { 20 14 "repo": { 21 15 "type": "string", 22 16 "format": "at-uri" 23 - }, 24 - "issueId": { 25 - "type": "integer" 26 - }, 27 - "owner": { 28 - "type": "string", 29 - "format": "did" 30 17 }, 31 18 "title": { 32 19 "type": "string"
-11
lexicons/pulls/comment.json
··· 19 19 "type": "string", 20 20 "format": "at-uri" 21 21 }, 22 - "repo": { 23 - "type": "string", 24 - "format": "at-uri" 25 - }, 26 - "commentId": { 27 - "type": "integer" 28 - }, 29 - "owner": { 30 - "type": "string", 31 - "format": "did" 32 - }, 33 22 "body": { 34 23 "type": "string" 35 24 },
+20 -12
lexicons/pulls/pull.json
··· 10 10 "record": { 11 11 "type": "object", 12 12 "required": [ 13 - "targetRepo", 14 - "targetBranch", 15 - "pullId", 13 + "target", 16 14 "title", 17 15 "patch", 18 16 "createdAt" 19 17 ], 20 18 "properties": { 21 - "targetRepo": { 22 - "type": "string", 23 - "format": "at-uri" 24 - }, 25 - "targetBranch": { 26 - "type": "string" 27 - }, 28 - "pullId": { 29 - "type": "integer" 19 + "target": { 20 + "type": "ref", 21 + "ref": "#target" 30 22 }, 31 23 "title": { 32 24 "type": "string" ··· 45 37 "type": "string", 46 38 "format": "datetime" 47 39 } 40 + } 41 + } 42 + }, 43 + "target": { 44 + "type": "object", 45 + "required": [ 46 + "repo", 47 + "branch" 48 + ], 49 + "properties": { 50 + "repo": { 51 + "type": "string", 52 + "format": "at-uri" 53 + }, 54 + "branch": { 55 + "type": "string" 48 56 } 49 57 } 50 58 },
+16
nix/modules/spindle.nix
··· 55 55 description = "DID of owner (required)"; 56 56 }; 57 57 58 + maxJobCount = mkOption { 59 + type = types.int; 60 + default = 2; 61 + example = 5; 62 + description = "Maximum number of concurrent jobs to run"; 63 + }; 64 + 65 + queueSize = mkOption { 66 + type = types.int; 67 + default = 100; 68 + example = 100; 69 + description = "Maximum number of jobs queue up"; 70 + }; 71 + 58 72 secrets = { 59 73 provider = mkOption { 60 74 type = types.str; ··· 108 122 "SPINDLE_SERVER_JETSTREAM=${cfg.server.jetstreamEndpoint}" 109 123 "SPINDLE_SERVER_DEV=${lib.boolToString cfg.server.dev}" 110 124 "SPINDLE_SERVER_OWNER=${cfg.server.owner}" 125 + "SPINDLE_SERVER_MAX_JOB_COUNT=${toString cfg.server.maxJobCount}" 126 + "SPINDLE_SERVER_QUEUE_SIZE=${toString cfg.server.queueSize}" 111 127 "SPINDLE_SERVER_SECRETS_PROVIDER=${cfg.server.secrets.provider}" 112 128 "SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR=${cfg.server.secrets.openbao.proxyAddr}" 113 129 "SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=${cfg.server.secrets.openbao.mount}"
+2
nix/vm.nix
··· 89 89 hostname = "localhost:6555"; 90 90 listenAddr = "0.0.0.0:6555"; 91 91 dev = true; 92 + queueSize = 100; 93 + maxJobCount = 2; 92 94 secrets = { 93 95 provider = "sqlite"; 94 96 };
+2
spindle/config/config.go
··· 17 17 Owner string `env:"OWNER, required"` 18 18 Secrets Secrets `env:",prefix=SECRETS_"` 19 19 LogDir string `env:"LOG_DIR, default=/var/log/spindle"` 20 + QueueSize int `env:"QUEUE_SIZE, default=100"` 21 + MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time 20 22 } 21 23 22 24 func (s Server) Did() syntax.DID {
+2 -1
spindle/server.go
··· 100 100 return err 101 101 } 102 102 103 - jq := queue.NewQueue(100, 5) 103 + jq := queue.NewQueue(cfg.Server.QueueSize, cfg.Server.MaxJobCount) 104 + logger.Info("initialized queue", "queueSize", cfg.Server.QueueSize, "numWorkers", cfg.Server.MaxJobCount) 104 105 105 106 collections := []string{ 106 107 tangled.SpindleMemberNSID,