diff --git a/.sqlx/query-0ba3585fd6ddf3c941d59b360fe177083607758c56bd1d8279c2bdb1590ae3d6.json b/.sqlx/query-0ba3585fd6ddf3c941d59b360fe177083607758c56bd1d8279c2bdb1590ae3d6.json new file mode 100644 index 0000000..d3144de --- /dev/null +++ b/.sqlx/query-0ba3585fd6ddf3c941d59b360fe177083607758c56bd1d8279c2bdb1590ae3d6.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE projects\n SET workflow_id = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "0ba3585fd6ddf3c941d59b360fe177083607758c56bd1d8279c2bdb1590ae3d6" +} diff --git a/.sqlx/query-0ca422ccf34a2f1a8d15f5f620821c1a65f7678ea1929b61885dde31bc4b6f24.json b/.sqlx/query-0ca422ccf34a2f1a8d15f5f620821c1a65f7678ea1929b61885dde31bc4b6f24.json new file mode 100644 index 0000000..3dd641e --- /dev/null +++ b/.sqlx/query-0ca422ccf34a2f1a8d15f5f620821c1a65f7678ea1929b61885dde31bc4b6f24.json @@ -0,0 +1,68 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT frogress_slug,\n frogress_version,\n frogress_category,\n frogress_measure,\n project_id,\n version,\n category,\n category_name,\n measure\n FROM frogress_mappings\n ORDER BY frogress_slug, frogress_version, frogress_category\n ", + "describe": { + "columns": [ + { + "name": "frogress_slug", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "frogress_version", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "frogress_category", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "frogress_measure", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "project_id", + "ordinal": 4, + "type_info": "Integer" + }, + { + "name": "version", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "category", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "category_name", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "measure", + "ordinal": 8, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "0ca422ccf34a2f1a8d15f5f620821c1a65f7678ea1929b61885dde31bc4b6f24" +} diff --git a/.sqlx/query-15bbe4fa503d79dfa792cfa473fabe7696222b06744bc83049e8f83a25f469a6.json b/.sqlx/query-15bbe4fa503d79dfa792cfa473fabe7696222b06744bc83049e8f83a25f469a6.json new file mode 100644 index 0000000..35acb7e --- /dev/null +++ b/.sqlx/query-15bbe4fa503d79dfa792cfa473fabe7696222b06744bc83049e8f83a25f469a6.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT git_commit, git_commit_message, timestamp, version\n FROM reports\n WHERE project_id = ? AND timestamp = (\n SELECT MAX(timestamp)\n FROM reports\n WHERE project_id = ?\n )\n ORDER BY version\n ", + "describe": { + "columns": [ + { + "name": "git_commit", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "git_commit_message", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "timestamp", + "ordinal": 2, + "type_info": "Datetime" + }, + { + "name": "version", + "ordinal": 3, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + true, + false, + false + ] + }, + "hash": "15bbe4fa503d79dfa792cfa473fabe7696222b06744bc83049e8f83a25f469a6" +} diff --git a/.sqlx/query-269859ed391626ddbc2af37381a13c36976f48e6a89aaaa03da8ea66baccddff.json b/.sqlx/query-269859ed391626ddbc2af37381a13c36976f48e6a89aaaa03da8ea66baccddff.json new file mode 100644 index 0000000..e97d556 --- /dev/null +++ b/.sqlx/query-269859ed391626ddbc2af37381a13c36976f48e6a89aaaa03da8ea66baccddff.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT owner, repo, name, short_name, default_category, default_version, platform, workflow_id\n FROM projects\n WHERE id = ?\n ", + "describe": { + "columns": [ + { + "name": "owner", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "repo", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "short_name", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "default_category", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "default_version", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "platform", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "workflow_id", + "ordinal": 7, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + true, + true, + true, + true, + true + ] + }, + "hash": "269859ed391626ddbc2af37381a13c36976f48e6a89aaaa03da8ea66baccddff" +} diff --git a/.sqlx/query-21108206934d9a3b1e895e8413c319da09b55b6667b0d12efa7422babd5ec584.json b/.sqlx/query-2d55fd406b948029e83a7ad21ab5f8186e44638d8b5a2eb1135300b8ff14d864.json similarity index 53% rename from .sqlx/query-21108206934d9a3b1e895e8413c319da09b55b6667b0d12efa7422babd5ec584.json rename to .sqlx/query-2d55fd406b948029e83a7ad21ab5f8186e44638d8b5a2eb1135300b8ff14d864.json index 0e2aae1..64c117f 100644 --- a/.sqlx/query-21108206934d9a3b1e895e8413c319da09b55b6667b0d12efa7422babd5ec584.json +++ b/.sqlx/query-2d55fd406b948029e83a7ad21ab5f8186e44638d8b5a2eb1135300b8ff14d864.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT EXISTS (\n SELECT 1\n FROM reports JOIN projects ON reports.project_id = projects.id\n WHERE projects.owner = ? COLLATE NOCASE AND projects.repo = ? COLLATE NOCASE\n AND git_commit = ? COLLATE NOCASE\n ) AS \"exists!\"\n ", + "query": "\n SELECT EXISTS (\n SELECT 1\n FROM reports JOIN projects ON reports.project_id = projects.id\n WHERE projects.id = ? AND git_commit = ? COLLATE NOCASE\n ) AS \"exists!\"\n ", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 3 + "Right": 2 }, "nullable": [ false ] }, - "hash": "21108206934d9a3b1e895e8413c319da09b55b6667b0d12efa7422babd5ec584" + "hash": "2d55fd406b948029e83a7ad21ab5f8186e44638d8b5a2eb1135300b8ff14d864" } diff --git a/.sqlx/query-364ea7a0b23d5edfbf4810659d41b0ceaeaa4fb6fd200dacf15030063ec7909d.json b/.sqlx/query-40064a6137d5177432a13ba71255b4a836c567139892ab997a80ea40158e5082.json similarity index 55% rename from .sqlx/query-364ea7a0b23d5edfbf4810659d41b0ceaeaa4fb6fd200dacf15030063ec7909d.json rename to .sqlx/query-40064a6137d5177432a13ba71255b4a836c567139892ab997a80ea40158e5082.json index 7483745..bbc5688 100644 --- a/.sqlx/query-364ea7a0b23d5edfbf4810659d41b0ceaeaa4fb6fd200dacf15030063ec7909d.json +++ b/.sqlx/query-40064a6137d5177432a13ba71255b4a836c567139892ab997a80ea40158e5082.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT git_commit, timestamp, version\n FROM reports\n WHERE project_id = ? AND git_commit = ? COLLATE NOCASE\n ORDER BY version\n ", + "query": "\n SELECT git_commit, git_commit_message, timestamp\n FROM reports\n WHERE project_id = ? AND version = ? COLLATE NOCASE\n ORDER BY timestamp DESC\n ", "describe": { "columns": [ { @@ -9,14 +9,14 @@ "type_info": "Text" }, { - "name": "timestamp", + "name": "git_commit_message", "ordinal": 1, - "type_info": "Datetime" + "type_info": "Text" }, { - "name": "version", + "name": "timestamp", "ordinal": 2, - "type_info": "Text" + "type_info": "Datetime" } ], "parameters": { @@ -24,9 +24,9 @@ }, "nullable": [ false, - false, + true, false ] }, - "hash": "364ea7a0b23d5edfbf4810659d41b0ceaeaa4fb6fd200dacf15030063ec7909d" + "hash": "40064a6137d5177432a13ba71255b4a836c567139892ab997a80ea40158e5082" } diff --git a/.sqlx/query-40da4ebeb3450a8c78a890e359bcca067142cbc16805c73f186bca873a8e9c98.json b/.sqlx/query-40da4ebeb3450a8c78a890e359bcca067142cbc16805c73f186bca873a8e9c98.json new file mode 100644 index 0000000..3473186 --- /dev/null +++ b/.sqlx/query-40da4ebeb3450a8c78a890e359bcca067142cbc16805c73f186bca873a8e9c98.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO reports (project_id, version, git_commit, timestamp, data, data_version)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (project_id, version COLLATE NOCASE, git_commit COLLATE NOCASE) DO UPDATE\n SET timestamp = EXCLUDED.timestamp\n RETURNING id\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 6 + }, + "nullable": [ + false + ] + }, + "hash": "40da4ebeb3450a8c78a890e359bcca067142cbc16805c73f186bca873a8e9c98" +} diff --git a/.sqlx/query-50e7e6303ba6fa91e2f527c7fa864a28ad702456c58e950c3d5d58e4e8d9b59e.json b/.sqlx/query-50e7e6303ba6fa91e2f527c7fa864a28ad702456c58e950c3d5d58e4e8d9b59e.json deleted file mode 100644 index 160f1dc..0000000 --- a/.sqlx/query-50e7e6303ba6fa91e2f527c7fa864a28ad702456c58e950c3d5d58e4e8d9b59e.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT git_commit, timestamp\n FROM reports\n WHERE project_id = ? AND version = ? COLLATE NOCASE\n ORDER BY timestamp DESC\n ", - "describe": { - "columns": [ - { - "name": "git_commit", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "timestamp", - "ordinal": 1, - "type_info": "Datetime" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false - ] - }, - "hash": "50e7e6303ba6fa91e2f527c7fa864a28ad702456c58e950c3d5d58e4e8d9b59e" -} diff --git a/.sqlx/query-4f3b7670e663abfba2a3c1b41f11293425d9701e3583fb1ce5851092dae89f10.json b/.sqlx/query-563bea47238272cf883abc1662ee37ce3154c075d8a6f2d4db48c95f31033cc2.json similarity index 51% rename from .sqlx/query-4f3b7670e663abfba2a3c1b41f11293425d9701e3583fb1ce5851092dae89f10.json rename to .sqlx/query-563bea47238272cf883abc1662ee37ce3154c075d8a6f2d4db48c95f31033cc2.json index 2c50761..5ce0b34 100644 --- a/.sqlx/query-4f3b7670e663abfba2a3c1b41f11293425d9701e3583fb1ce5851092dae89f10.json +++ b/.sqlx/query-563bea47238272cf883abc1662ee37ce3154c075d8a6f2d4db48c95f31033cc2.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT git_commit, timestamp, version\n FROM reports\n WHERE project_id = ? AND timestamp = (\n SELECT MAX(timestamp)\n FROM reports\n WHERE project_id = ?\n )\n ORDER BY version\n ", + "query": "\n SELECT git_commit, git_commit_message, timestamp, version\n FROM reports\n WHERE project_id = ? AND git_commit = ? COLLATE NOCASE\n ORDER BY version\n ", "describe": { "columns": [ { @@ -9,13 +9,18 @@ "type_info": "Text" }, { - "name": "timestamp", + "name": "git_commit_message", "ordinal": 1, + "type_info": "Text" + }, + { + "name": "timestamp", + "ordinal": 2, "type_info": "Datetime" }, { "name": "version", - "ordinal": 2, + "ordinal": 3, "type_info": "Text" } ], @@ -24,9 +29,10 @@ }, "nullable": [ false, + true, false, false ] }, - "hash": "4f3b7670e663abfba2a3c1b41f11293425d9701e3583fb1ce5851092dae89f10" + "hash": "563bea47238272cf883abc1662ee37ce3154c075d8a6f2d4db48c95f31033cc2" } diff --git a/.sqlx/query-56c994f1c82f796c9924b571d5442a2694c3a1dbf469c88dfb8df8f283e18a3d.json b/.sqlx/query-56c994f1c82f796c9924b571d5442a2694c3a1dbf469c88dfb8df8f283e18a3d.json deleted file mode 100644 index ef4fdf0..0000000 --- a/.sqlx/query-56c994f1c82f796c9924b571d5442a2694c3a1dbf469c88dfb8df8f283e18a3d.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO reports (project_id, version, git_commit, timestamp, data)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (project_id, version COLLATE NOCASE, git_commit COLLATE NOCASE) DO UPDATE\n SET timestamp = EXCLUDED.timestamp\n RETURNING id\n ", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 5 - }, - "nullable": [ - false - ] - }, - "hash": "56c994f1c82f796c9924b571d5442a2694c3a1dbf469c88dfb8df8f283e18a3d" -} diff --git a/.sqlx/query-5de3235fb918d2e691289b998a5899e71f5f491f72864b134b4debe85bb57fa9.json b/.sqlx/query-5de3235fb918d2e691289b998a5899e71f5f491f72864b134b4debe85bb57fa9.json new file mode 100644 index 0000000..497ee2a --- /dev/null +++ b/.sqlx/query-5de3235fb918d2e691289b998a5899e71f5f491f72864b134b4debe85bb57fa9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO projects (id, owner, repo, name, short_name, default_category, default_version, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\n ON CONFLICT (id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "5de3235fb918d2e691289b998a5899e71f5f491f72864b134b4debe85bb57fa9" +} diff --git a/.sqlx/query-b5c315558b6853de6aeb2f3338ec8448f2e8d9aca51ac488d55d526bef7147e3.json b/.sqlx/query-60a01415ef25ea6a2fc7f3287e0e6972404e351401f8aeaedbc74b4c934c777c.json similarity index 65% rename from .sqlx/query-b5c315558b6853de6aeb2f3338ec8448f2e8d9aca51ac488d55d526bef7147e3.json rename to .sqlx/query-60a01415ef25ea6a2fc7f3287e0e6972404e351401f8aeaedbc74b4c934c777c.json index ec28cec..363f1f2 100644 --- a/.sqlx/query-b5c315558b6853de6aeb2f3338ec8448f2e8d9aca51ac488d55d526bef7147e3.json +++ b/.sqlx/query-60a01415ef25ea6a2fc7f3287e0e6972404e351401f8aeaedbc74b4c934c777c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT id AS \"id!\", owner, repo, name, short_name, default_version, platform\n FROM projects\n WHERE owner = ? COLLATE NOCASE AND repo = ? COLLATE NOCASE\n ", + "query": "\n SELECT id AS \"id!\", owner, repo, name, short_name, default_category, default_version, platform, workflow_id\n FROM projects\n WHERE owner = ? COLLATE NOCASE AND repo = ? COLLATE NOCASE\n ", "describe": { "columns": [ { @@ -29,14 +29,24 @@ "type_info": "Text" }, { - "name": "default_version", + "name": "default_category", "ordinal": 5, "type_info": "Text" }, { - "name": "platform", + "name": "default_version", "ordinal": 6, "type_info": "Text" + }, + { + "name": "platform", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "workflow_id", + "ordinal": 8, + "type_info": "Text" } ], "parameters": { @@ -49,8 +59,10 @@ true, true, true, + true, + true, true ] }, - "hash": "b5c315558b6853de6aeb2f3338ec8448f2e8d9aca51ac488d55d526bef7147e3" + "hash": "60a01415ef25ea6a2fc7f3287e0e6972404e351401f8aeaedbc74b4c934c777c" } diff --git a/.sqlx/query-756e4c1b9b1d5e0bf5100247a18e8998ecc0596dd1b9f388f1d6116c2209ef96.json b/.sqlx/query-756e4c1b9b1d5e0bf5100247a18e8998ecc0596dd1b9f388f1d6116c2209ef96.json new file mode 100644 index 0000000..5d16ba9 --- /dev/null +++ b/.sqlx/query-756e4c1b9b1d5e0bf5100247a18e8998ecc0596dd1b9f388f1d6116c2209ef96.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE projects\n SET owner = ?, repo = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "756e4c1b9b1d5e0bf5100247a18e8998ecc0596dd1b9f388f1d6116c2209ef96" +} diff --git a/.sqlx/query-a4130f021284bfab1562647ecf4b2967a8d70a1e1f02f92c4f792d71ace021e5.json b/.sqlx/query-a4130f021284bfab1562647ecf4b2967a8d70a1e1f02f92c4f792d71ace021e5.json new file mode 100644 index 0000000..ec75478 --- /dev/null +++ b/.sqlx/query-a4130f021284bfab1562647ecf4b2967a8d70a1e1f02f92c4f792d71ace021e5.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE reports\n SET git_commit_message = ?\n WHERE project_id = ? AND git_commit = ? COLLATE NOCASE\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "a4130f021284bfab1562647ecf4b2967a8d70a1e1f02f92c4f792d71ace021e5" +} diff --git a/.sqlx/query-1c5ea45ed729197e046adfd9a9129de3479799b64556826043d6dd909c314429.json b/.sqlx/query-b492f0562501ae39110cf2df7a545cd0c676b438b91d63b915988c17d5bb29b6.json similarity index 51% rename from .sqlx/query-1c5ea45ed729197e046adfd9a9129de3479799b64556826043d6dd909c314429.json rename to .sqlx/query-b492f0562501ae39110cf2df7a545cd0c676b438b91d63b915988c17d5bb29b6.json index 30b194e..117eb8a 100644 --- a/.sqlx/query-1c5ea45ed729197e046adfd9a9129de3479799b64556826043d6dd909c314429.json +++ b/.sqlx/query-b492f0562501ae39110cf2df7a545cd0c676b438b91d63b915988c17d5bb29b6.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n projects.id AS \"project_id!\",\n owner AS \"owner!\",\n repo AS \"repo!\",\n name,\n short_name,\n default_version,\n platform,\n git_commit,\n MAX(timestamp) AS \"timestamp: chrono::NaiveDateTime\",\n JSON_GROUP_ARRAY(version ORDER BY version)\n FILTER (WHERE version IS NOT NULL) AS versions\n FROM projects LEFT JOIN reports ON (\n reports.project_id = projects.id\n AND reports.timestamp = (\n SELECT MAX(timestamp)\n FROM reports\n WHERE project_id = projects.id\n )\n )\n GROUP BY projects.id\n ORDER BY MAX(timestamp) DESC\n ", + "query": "\n SELECT\n projects.id AS \"project_id!\",\n owner AS \"owner!\",\n repo AS \"repo!\",\n name,\n short_name,\n default_category,\n default_version,\n platform,\n workflow_id,\n git_commit,\n git_commit_message,\n MAX(timestamp) AS \"timestamp: time::OffsetDateTime\",\n JSON_GROUP_ARRAY(version ORDER BY version)\n FILTER (WHERE version IS NOT NULL) AS versions\n FROM projects LEFT JOIN reports ON (\n reports.project_id = projects.id\n AND reports.timestamp = (\n SELECT MAX(timestamp)\n FROM reports\n WHERE project_id = projects.id\n )\n )\n GROUP BY projects.id\n ORDER BY MAX(timestamp) DESC\n ", "describe": { "columns": [ { @@ -29,29 +29,44 @@ "type_info": "Text" }, { - "name": "default_version", + "name": "default_category", "ordinal": 5, "type_info": "Text" }, { - "name": "platform", + "name": "default_version", "ordinal": 6, "type_info": "Text" }, { - "name": "git_commit", + "name": "platform", "ordinal": 7, "type_info": "Text" }, { - "name": "timestamp: chrono::NaiveDateTime", + "name": "workflow_id", "ordinal": 8, "type_info": "Text" }, { - "name": "versions", + "name": "git_commit", "ordinal": 9, "type_info": "Text" + }, + { + "name": "git_commit_message", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "timestamp: time::OffsetDateTime", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "versions", + "ordinal": 12, + "type_info": "Text" } ], "parameters": { @@ -67,8 +82,11 @@ true, true, true, + true, + true, + true, true ] }, - "hash": "1c5ea45ed729197e046adfd9a9129de3479799b64556826043d6dd909c314429" + "hash": "b492f0562501ae39110cf2df7a545cd0c676b438b91d63b915988c17d5bb29b6" } diff --git a/.sqlx/query-b529ab4eadd4de8acf149fde3ee5e3064e7392bc5b3d647b6723c66169933812.json b/.sqlx/query-b529ab4eadd4de8acf149fde3ee5e3064e7392bc5b3d647b6723c66169933812.json new file mode 100644 index 0000000..d3ea40a --- /dev/null +++ b/.sqlx/query-b529ab4eadd4de8acf149fde3ee5e3064e7392bc5b3d647b6723c66169933812.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT owner, repo FROM projects WHERE id = ?\n ", + "describe": { + "columns": [ + { + "name": "owner", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "repo", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "b529ab4eadd4de8acf149fde3ee5e3064e7392bc5b3d647b6723c66169933812" +} diff --git a/.sqlx/query-c0adc5fae9d742173927a8f48d01d02add2ea3b8abae862a144c4607a62cc196.json b/.sqlx/query-c0adc5fae9d742173927a8f48d01d02add2ea3b8abae862a144c4607a62cc196.json deleted file mode 100644 index 6327e1e..0000000 --- a/.sqlx/query-c0adc5fae9d742173927a8f48d01d02add2ea3b8abae862a144c4607a62cc196.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO projects (id, owner, repo, name, short_name, default_version, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\n ON CONFLICT (id) DO NOTHING\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "c0adc5fae9d742173927a8f48d01d02add2ea3b8abae862a144c4607a62cc196" -} diff --git a/.sqlx/query-d4bb3705fccd553cb29ac7f77382bd61adeef31df221b3bbddef34ce8bcda70e.json b/.sqlx/query-d4bb3705fccd553cb29ac7f77382bd61adeef31df221b3bbddef34ce8bcda70e.json new file mode 100644 index 0000000..d445f85 --- /dev/null +++ b/.sqlx/query-d4bb3705fccd553cb29ac7f77382bd61adeef31df221b3bbddef34ce8bcda70e.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT version FROM reports WHERE project_id = ? AND git_commit = ? COLLATE NOCASE\n ", + "describe": { + "columns": [ + { + "name": "version", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "d4bb3705fccd553cb29ac7f77382bd61adeef31df221b3bbddef34ce8bcda70e" +} diff --git a/.sqlx/query-4756303132114bcbac47dd560381c0cc2883a7708f5da51fdc6f0ec81e1ce292.json b/.sqlx/query-e66a8c18b0637fd37084ad02dac3e8a9d6e9a24aa0d3051cee977f13f780e0af.json similarity index 54% rename from .sqlx/query-4756303132114bcbac47dd560381c0cc2883a7708f5da51fdc6f0ec81e1ce292.json rename to .sqlx/query-e66a8c18b0637fd37084ad02dac3e8a9d6e9a24aa0d3051cee977f13f780e0af.json index 80f6f84..20f2c69 100644 --- a/.sqlx/query-4756303132114bcbac47dd560381c0cc2883a7708f5da51fdc6f0ec81e1ce292.json +++ b/.sqlx/query-e66a8c18b0637fd37084ad02dac3e8a9d6e9a24aa0d3051cee977f13f780e0af.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n reports.id as \"report_id!\",\n git_commit,\n timestamp,\n version,\n data\n FROM reports JOIN projects ON reports.project_id = projects.id\n WHERE projects.owner = ? COLLATE NOCASE AND projects.repo = ? COLLATE NOCASE\n AND version = ? COLLATE NOCASE AND git_commit = ? COLLATE NOCASE\n ", + "query": "\n SELECT\n reports.id as \"report_id!\",\n git_commit,\n git_commit_message,\n timestamp,\n version,\n data\n FROM reports JOIN projects ON reports.project_id = projects.id\n WHERE projects.owner = ? COLLATE NOCASE AND projects.repo = ? COLLATE NOCASE\n AND version = ? COLLATE NOCASE AND git_commit = ? COLLATE NOCASE\n ", "describe": { "columns": [ { @@ -14,18 +14,23 @@ "type_info": "Text" }, { - "name": "timestamp", + "name": "git_commit_message", "ordinal": 2, + "type_info": "Text" + }, + { + "name": "timestamp", + "ordinal": 3, "type_info": "Datetime" }, { "name": "version", - "ordinal": 3, + "ordinal": 4, "type_info": "Text" }, { "name": "data", - "ordinal": 4, + "ordinal": 5, "type_info": "Blob" } ], @@ -35,10 +40,11 @@ "nullable": [ true, false, + true, false, false, false ] }, - "hash": "4756303132114bcbac47dd560381c0cc2883a7708f5da51fdc6f0ec81e1ce292" + "hash": "e66a8c18b0637fd37084ad02dac3e8a9d6e9a24aa0d3051cee977f13f780e0af" } diff --git a/Cargo.lock b/Cargo.lock index 1e78c49..4413093 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] @@ -41,7 +41,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] @@ -194,7 +194,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -204,7 +204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44055e597c674aef7cb903b2b9f6e4cba1277ed0d2d61dae7cd52d7ffa81f8e2" dependencies = [ "concolor", - "unicode-width", + "unicode-width 0.1.14", "yansi", ] @@ -253,6 +253,43 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-openai" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d76e2f5af19477d6254415acc95ba97c6cc6f3b1e3cb4676b7f0fab8194298" +dependencies = [ + "async-openai-macros", + "backoff", + "base64 0.22.1", + "bytes", + "derive_builder", + "eventsource-stream", + "futures", + "rand 0.8.5", + "reqwest", + "reqwest-eventsource", + "secrecy", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "async-openai-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0289cba6d5143bfe8251d57b4a8cac036adf158525a76533a7082ba65ec76398" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -261,7 +298,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -294,7 +331,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 7.1.3", "num-rational", "v_frame", ] @@ -308,42 +345,16 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "aws-lc-rs" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" -dependencies = [ - "aws-lc-sys", - "mirai-annotations", - "paste", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", - "libc", - "paste", -] - [[package]] name = "axum" -version = "0.7.7" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" dependencies = [ - "async-trait", "axum-core", + "axum-macros", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", @@ -361,9 +372,9 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper", "tokio", - "tower 0.5.1", + "tower", "tower-layer", "tower-service", "tracing", @@ -371,25 +382,49 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ - "async-trait", "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.15", + "instant", + "pin-project-lite", + "rand 0.8.5", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -399,7 +434,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide 0.8.0", + "miniz_oxide 0.8.8", "object", "rustc-demangle", "windows-targets 0.52.6", @@ -419,7 +454,7 @@ dependencies = [ "regex", "seahash", "serde", - "thiserror", + "thiserror 1.0.64", ] [[package]] @@ -474,29 +509,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.6.0", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.79", - "which", -] - [[package]] name = "bit_field" version = "0.10.2" @@ -511,9 +523,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" dependencies = [ "serde", ] @@ -587,9 +599,9 @@ checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" dependencies = [ "allocator-api2", ] @@ -642,9 +654,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.7.2" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "castaway" @@ -666,15 +678,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-expr" version = "0.15.8" @@ -691,6 +694,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -706,17 +715,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.5.20" @@ -744,15 +742,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" -[[package]] -name = "cmake" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" -dependencies = [ - "cc", -] - [[package]] name = "codemap" version = "0.1.3" @@ -773,9 +762,9 @@ checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "compact_str" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" dependencies = [ "castaway", "cfg-if", @@ -855,6 +844,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -967,9 +967,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -996,7 +996,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf", "smallvec", ] @@ -1016,7 +1016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1025,6 +1025,41 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b7ef7a4be509357f4804d0a22e830daddb48f19fd604e4ad32ddce04a94c36" +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.87", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -1079,17 +1114,20 @@ version = "0.1.0" dependencies = [ "anyhow", "ariadne", + "async-openai", "axum", "badge-maker", + "base64 0.22.1", "blake3", "bytes", - "chrono", "futures-util", "grass", "hex", + "hmac", "human-size", "image", - "itertools 0.13.0", + "itertools 0.14.0", + "jsonwebtoken", "lightningcss", "mime", "minijinja", @@ -1100,19 +1138,24 @@ dependencies = [ "oxc", "palette", "prost", + "rand 0.9.1", "regex", "reqwest", "resvg", "serde", "serde_json", "serde_yaml", + "sha2", "sqlx", "streemap", + "time", "timeago", "tokio", "tokio-cron-scheduler", - "tower 0.5.1", + "tower", "tower-http", + "tower-sessions", + "tower-sessions-sqlx-store", "tracing", "tracing-subscriber", "url", @@ -1133,22 +1176,54 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", ] [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.87", ] [[package]] @@ -1163,17 +1238,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "dotenvy" version = "0.15.7" @@ -1195,12 +1259,6 @@ dependencies = [ "dtoa", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.13.0" @@ -1267,6 +1325,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom 7.1.3", + "pin-project-lite", +] + [[package]] name = "exr" version = "1.72.0" @@ -1322,20 +1391,14 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "flagset" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" - [[package]] name = "flate2" -version = "1.0.34" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide 0.8.8", ] [[package]] @@ -1372,9 +1435,9 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" dependencies = [ "fontconfig-parser", "log", @@ -1408,12 +1471,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1496,7 +1553,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1511,6 +1568,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -1529,15 +1592,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1557,7 +1611,21 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] @@ -1577,12 +1645,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "grass" version = "0.13.4" @@ -1590,7 +1652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7a68216437ef68f0738e48d6c7bb9e6e6a92237e001b03d838314b068f33c94" dependencies = [ "clap", - "getrandom", + "getrandom 0.2.15", "grass_compiler", ] @@ -1604,8 +1666,8 @@ dependencies = [ "indexmap", "lasso", "once_cell", - "phf 0.11.2", - "rand", + "phf", + "rand 0.8.5", ] [[package]] @@ -1658,9 +1720,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hashlink" @@ -1792,9 +1854,9 @@ checksum = "9994b79e8c1a39b3166c63ae7823bb2b00831e2a96a31399c50fe69df408eaeb" [[package]] name = "hyper" -version = "1.4.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -1861,9 +1923,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", @@ -1871,6 +1933,7 @@ dependencies = [ "http", "http-body", "hyper", + "libc", "pin-project-lite", "socket2", "tokio", @@ -1901,6 +1964,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1922,7 +1991,7 @@ dependencies = [ "color_quant", "exr", "gif", - "image-webp", + "image-webp 0.1.3", "num-traits", "png", "qoi", @@ -1944,6 +2013,16 @@ dependencies = [ "quick-error", ] +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "imagesize" version = "0.13.0" @@ -1963,7 +2042,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", + "serde", ] [[package]] @@ -1986,6 +2066,15 @@ dependencies = [ "libc", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -1994,7 +2083,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2058,10 +2147,19 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.11" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" @@ -2080,10 +2178,11 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.71" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -2150,12 +2249,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lebe" version = "0.5.2" @@ -2164,9 +2257,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libfuzzer-sys" @@ -2179,16 +2272,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "libloading" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" -dependencies = [ - "cfg-if", - "windows-targets 0.52.6", -] - [[package]] name = "libm" version = "0.2.8" @@ -2201,7 +2284,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "libc", "redox_syscall", ] @@ -2219,18 +2302,19 @@ dependencies = [ [[package]] name = "lightningcss" -version = "1.0.0-alpha.59" +version = "1.0.0-alpha.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e225b3fa0a8bd5562c8833b1a32afa88761c4e661d3177b8cdc4e13cbf078e" +checksum = "c84f971730745f4aaac013b6cf4328baf1548efc973c0d95cfd843a3c1ca07af" dependencies = [ "ahash 0.8.11", - "bitflags 2.6.0", + "bitflags 2.9.0", "const-str", "cssparser", "cssparser-color", "dashmap 5.5.3", "data-encoding", - "getrandom", + "getrandom 0.2.15", + "indexmap", "itertools 0.10.5", "lazy_static", "lightningcss-derive", @@ -2269,6 +2353,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2303,9 +2388,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "maybe-rayon" @@ -2358,27 +2443,27 @@ dependencies = [ [[package]] name = "miette" -version = "7.2.0" +version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" +checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" dependencies = [ "cfg-if", "miette-derive", "owo-colors", "textwrap", - "thiserror", - "unicode-width", + "thiserror 1.0.64", + "unicode-width 0.1.14", ] [[package]] name = "miette-derive" -version = "7.2.0" +version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" +checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2399,9 +2484,9 @@ dependencies = [ [[package]] name = "minijinja" -version = "2.3.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1028b628753a7e1a88fc59c9ba4b02ecc3bc0bd3c7af23df667bc28df9b3310e" +checksum = "98642a6dfca91122779a307b77cd07a4aa951fbe32232aaf5bad9febc66be754" dependencies = [ "memo-map", "self_cell", @@ -2411,9 +2496,9 @@ dependencies = [ [[package]] name = "minijinja-autoreload" -version = "2.3.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "641ba8dc6cafd35a2f9f141f4593bdd01438cfad120ff679a91a9dce92510e38" +checksum = "2bfa27f77378f5239b971abf45c16b56f6f207c0186f5b62ee2aee1be19dd6ce" dependencies = [ "minijinja", "notify", @@ -2436,9 +2521,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", "simd-adler32", @@ -2452,7 +2537,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2464,16 +2549,10 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] -[[package]] -name = "mirai-annotations" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" - [[package]] name = "moka" version = "0.12.8" @@ -2493,7 +2572,7 @@ dependencies = [ "rustc_version", "smallvec", "tagptr", - "thiserror", + "thiserror 1.0.64", "triomphe", "uuid", ] @@ -2537,6 +2616,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nonmax" version = "0.5.5" @@ -2598,7 +2686,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -2617,7 +2705,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2662,18 +2750,21 @@ dependencies = [ ] [[package]] -name = "objdiff-core" -version = "2.2.2" +name = "num_threads" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec5e2ba3ab275d85ec500ed6dd2c617015d7bf72435b423785457def47217fc" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objdiff-core" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b114e70a6ede280511c1690609afbe802f84f7aaf27c71486c0510df07e647b8" dependencies = [ "anyhow", - "byteorder", - "filetime", - "flagset", - "log", - "memmap2", - "num-traits", "object", "pbjson", "pbjson-build", @@ -2681,10 +2772,6 @@ dependencies = [ "prost-build", "serde", "serde_json", - "similar", - "strum", - "tsify-next", - "wasm-bindgen", ] [[package]] @@ -2698,9 +2785,9 @@ dependencies = [ [[package]] name = "octocrab" -version = "0.41.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dada65f156e4100376ceeaeadae1b4d35ab9cdf4236cfc92fde0d06b930c3b7" +checksum = "aaf799a9982a4d0b4b3fa15b4c1ff7daf5bd0597f46456744dcbb6ddc2e4c827" dependencies = [ "arc-swap", "async-trait", @@ -2710,6 +2797,7 @@ dependencies = [ "chrono", "either", "futures", + "futures-core", "futures-util", "http", "http-body", @@ -2729,10 +2817,11 @@ dependencies = [ "serde_urlencoded", "snafu", "tokio", - "tower 0.4.13", + "tower", "tower-http", "tracing", "url", + "web-time", ] [[package]] @@ -2747,7 +2836,7 @@ version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "cfg-if", "foreign-types", "libc", @@ -2764,7 +2853,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2832,15 +2921,15 @@ dependencies = [ [[package]] name = "oxc-browserslist" -version = "1.0.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b5ad390a7cf69d5f574ea41eada88a7c0947272647703fe1ec6ce23bfe1714" +checksum = "ba2705c663f10d23e2c1600ef89834a979afc1756a88197061bdbb275c608cd1" dependencies = [ - "nom", - "rustc-hash 2.0.0", + "nom 8.0.0", + "rustc-hash", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", "time", ] @@ -2860,7 +2949,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d90e2d0a935fa2a9271a37e180e0725a902450474a0f2e8d104fb643f29cce67" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "num-bigint", "oxc_allocator", "oxc_ast_macros", @@ -2877,7 +2966,7 @@ checksum = "a2b1072cb32335b47745c9ea9d614a6d5d36ba9f528fce1da428d5f7e20c3ed1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2886,13 +2975,13 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10ead2bcd74b8b7f6dae07b698fd516c28cecb988ccc0d53389929ebe753b40e" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "itertools 0.13.0", "nonmax", "oxc_index", "oxc_syntax", "petgraph", - "rustc-hash 2.0.0", + "rustc-hash", ] [[package]] @@ -2901,7 +2990,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11ba2b4b1f0dac2bc6cbb172d53bf8685153d56938682716ed1bf96489b86048" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "cow-utils", "daachorse", "nonmax", @@ -2913,7 +3002,7 @@ dependencies = [ "oxc_sourcemap", "oxc_span", "oxc_syntax", - "rustc-hash 2.0.0", + "rustc-hash", ] [[package]] @@ -2933,9 +3022,9 @@ checksum = "da1421cd82c209a7dfc830fdd09b5c6372e28dfc419e9e67868c6159ab5c127b" dependencies = [ "miette", "owo-colors", - "rustc-hash 2.0.0", + "rustc-hash", "textwrap", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -2985,7 +3074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afe7b80a710daa7a07eec92bc46ed5a2d08581f15e833ee1eded5d929283893" dependencies = [ "assert-unchecked", - "bitflags 2.6.0", + "bitflags 2.9.0", "cow-utils", "memchr", "num-bigint", @@ -2997,7 +3086,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_syntax_operations", - "rustc-hash 2.0.0", + "rustc-hash", "seq-macro", ] @@ -3011,8 +3100,8 @@ dependencies = [ "oxc_ast_macros", "oxc_diagnostics", "oxc_span", - "phf 0.11.2", - "rustc-hash 2.0.0", + "phf", + "rustc-hash", "unicode-id-start", ] @@ -3033,8 +3122,8 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_syntax_operations", - "phf 0.11.2", - "rustc-hash 2.0.0", + "phf", + "rustc-hash", ] [[package]] @@ -3046,7 +3135,7 @@ dependencies = [ "base64-simd 0.8.0", "cfg-if", "cow-utils", - "rustc-hash 2.0.0", + "rustc-hash", "serde", "serde_json", ] @@ -3070,15 +3159,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e1b7860133b880b357bb400e12a791e86dd8165ebcbfda1f3b7f31dd2f645ab" dependencies = [ "assert-unchecked", - "bitflags 2.6.0", + "bitflags 2.9.0", "dashmap 6.1.0", "nonmax", "oxc_allocator", "oxc_ast_macros", "oxc_index", "oxc_span", - "phf 0.11.2", - "rustc-hash 2.0.0", + "phf", + "rustc-hash", "ryu-js", "unicode-id-start", ] @@ -3116,7 +3205,7 @@ dependencies = [ "oxc_syntax_operations", "oxc_traverse", "ropey", - "rustc-hash 2.0.0", + "rustc-hash", "serde", "serde_json", "sha1", @@ -3138,7 +3227,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_syntax_operations", - "rustc-hash 2.0.0", + "rustc-hash", ] [[package]] @@ -3150,7 +3239,7 @@ dependencies = [ "approx", "fast-srgb8", "palette_derive", - "phf 0.11.2", + "phf", ] [[package]] @@ -3162,22 +3251,22 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] name = "parcel_selectors" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4d26c18a8377a64728c04bf3b2e48ec43b0c77e687a18e03eb837d65e08a14" +checksum = "dccbc6fb560df303a44e511618256029410efbc87779018f751ef12c488271fe" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "cssparser", - "fxhash", "log", - "phf 0.10.1", + "phf", "phf_codegen", "precomputed-hash", + "rustc-hash", "smallvec", ] @@ -3295,41 +3384,22 @@ dependencies = [ [[package]] name = "phf" -version = "0.10.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_shared 0.10.0", -] - -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", - "phf_shared 0.11.2", + "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand", + "phf_generator", + "phf_shared", ] [[package]] @@ -3338,39 +3408,30 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ - "phf_shared 0.11.2", - "rand", + "phf_shared", + "rand 0.8.5", ] [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] name = "phf_shared" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] @@ -3396,7 +3457,7 @@ checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3448,7 +3509,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.8.0", + "miniz_oxide 0.8.8", ] [[package]] @@ -3479,7 +3540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3507,7 +3568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3537,7 +3598,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.79", + "syn 2.0.87", "tempfile", ] @@ -3551,7 +3612,7 @@ dependencies = [ "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3602,7 +3663,7 @@ dependencies = [ "libc", "once_cell", "raw-cpuid", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "web-sys", "winapi", ] @@ -3613,6 +3674,60 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -3622,6 +3737,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radium" version = "0.7.0" @@ -3635,8 +3756,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -3646,7 +3777,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -3655,7 +3796,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", ] [[package]] @@ -3684,11 +3834,11 @@ dependencies = [ "once_cell", "paste", "profiling", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "simd_helpers", "system-deps", - "thiserror", + "thiserror 1.0.64", "v_frame", "wasm-bindgen", ] @@ -3713,7 +3863,7 @@ version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", ] [[package]] @@ -3742,7 +3892,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", ] [[package]] @@ -3800,9 +3950,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.8" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "async-compression", "base64 0.22.1", @@ -3822,35 +3972,59 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", "tokio-util", + "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "windows-registry", ] [[package]] -name = "resvg" -version = "0.44.0" +name = "reqwest-eventsource" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" +checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom 7.1.3", + "pin-project-lite", + "reqwest", + "thiserror 1.0.64", +] + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" dependencies = [ "gif", - "image-webp", + "image-webp 0.2.1", "log", "pico-args", "rgb", @@ -3877,7 +4051,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -3913,6 +4087,28 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "ropey" version = "1.6.1" @@ -3942,7 +4138,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -3955,12 +4151,6 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.0.0" @@ -3982,7 +4172,7 @@ version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", @@ -3995,9 +4185,9 @@ version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ - "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -4031,6 +4221,9 @@ name = "rustls-pki-types" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -4038,7 +4231,6 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4052,11 +4244,11 @@ checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rustybuzz" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "bytemuck", "core_maths", "log", @@ -4076,9 +4268,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "ryu-js" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad97d4ce1560a5e27cec89519dc8300d1aa6035b099821261c651486a19e44d5" +checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" [[package]] name = "same-file" @@ -4112,10 +4304,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "secrecy" -version = "0.8.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" dependencies = [ + "serde", "zeroize", ] @@ -4125,7 +4318,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "core-foundation", "core-foundation-sys", "libc", @@ -4144,9 +4337,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semver" @@ -4156,57 +4349,35 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "seq-macro" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] -[[package]] -name = "serde-wasm-bindgen" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -4311,7 +4482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -4344,12 +4515,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "similar" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" - [[package]] name = "simple_asn1" version = "0.6.2" @@ -4358,7 +4523,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.64", "time", ] @@ -4371,12 +4536,6 @@ dependencies = [ "log", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" @@ -4434,14 +4593,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -4472,7 +4631,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ - "nom", + "nom 7.1.3", "unicode_categories", ] @@ -4522,7 +4681,8 @@ dependencies = [ "sha2", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.64", + "time", "tokio", "tokio-stream", "tracing", @@ -4539,7 +4699,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4562,7 +4722,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.79", + "syn 2.0.87", "tempfile", "tokio", "url", @@ -4576,7 +4736,7 @@ checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.9.0", "byteorder", "bytes", "chrono", @@ -4598,7 +4758,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "serde", "sha1", @@ -4606,7 +4766,8 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.64", + "time", "tracing", "whoami", ] @@ -4619,7 +4780,7 @@ checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.9.0", "byteorder", "chrono", "crc", @@ -4638,14 +4799,15 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha2", "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.64", + "time", "tracing", "whoami", ] @@ -4670,6 +4832,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", + "time", "tracing", "url", ] @@ -4721,28 +4884,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.79", -] - [[package]] name = "subtle" version = "2.6.1" @@ -4751,12 +4892,12 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "svgtypes" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo", - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -4772,21 +4913,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.1" @@ -4802,7 +4937,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "core-foundation", "system-configuration-sys", ] @@ -4863,13 +4998,13 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -4878,7 +5013,16 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.64", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -4889,7 +5033,18 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -4915,13 +5070,15 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -4930,15 +5087,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -4996,9 +5153,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -5029,13 +5186,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5061,9 +5218,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -5072,9 +5229,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", @@ -5119,14 +5276,17 @@ dependencies = [ [[package]] name = "tower" -version = "0.4.13" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "pin-project", + "hdrhistogram", + "indexmap", "pin-project-lite", + "slab", + "sync_wrapper", "tokio", "tokio-util", "tower-layer", @@ -5135,23 +5295,19 @@ dependencies = [ ] [[package]] -name = "tower" -version = "0.5.1" +name = "tower-cookies" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" dependencies = [ - "futures-core", + "axum-core", + "cookie", "futures-util", - "hdrhistogram", - "indexmap", + "http", + "parking_lot", "pin-project-lite", - "slab", - "sync_wrapper 0.1.2", - "tokio", - "tokio-util", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -5162,7 +5318,7 @@ checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" dependencies = [ "async-compression", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.9.0", "bytes", "futures-core", "futures-util", @@ -5178,7 +5334,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower 0.5.1", + "tower", "tower-layer", "tower-service", "tracing", @@ -5198,10 +5354,75 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] -name = "tracing" -version = "0.1.40" +name = "tower-sessions" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3" +dependencies = [ + "async-trait", + "http", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.22.1", + "futures", + "http", + "parking_lot", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror 2.0.12", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + +[[package]] +name = "tower-sessions-sqlx-store" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e054622079f57fc1a7d6a6089c9334f963d62028fe21dc9eddd58af9a78480b3" +dependencies = [ + "async-trait", + "rmp-serde", + "sqlx", + "thiserror 1.0.64", + "time", + "tower-sessions-core", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -5211,20 +5432,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -5271,35 +5492,11 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tsify-next" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4a645dca4ee0800f5ab60ce166deba2db6a0315de795a2691e138a3d55d756" -dependencies = [ - "serde", - "serde-wasm-bindgen", - "tsify-next-macros", - "wasm-bindgen", -] - -[[package]] -name = "tsify-next-macros" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d5c06f8a51d759bb58129e30b2631739e7e1e4579fad1f30ac09a6c88e488a6" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.79", -] - [[package]] name = "ttf-parser" -version = "0.24.1" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" dependencies = [ "core_maths", ] @@ -5327,15 +5524,15 @@ checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-bidi-mirroring" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" [[package]] name = "unicode-ccc" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-id-start" @@ -5394,6 +5591,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -5426,9 +5629,9 @@ dependencies = [ [[package]] name = "usvg" -version = "0.44.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" dependencies = [ "base64 0.22.1", "data-url 0.3.1", @@ -5441,7 +5644,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher 1.0.1", + "siphasher", "strict-num", "svgtypes", "tiny-skia-path", @@ -5463,7 +5666,7 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -5538,6 +5741,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" @@ -5546,47 +5758,48 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.94" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.94" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.44" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65471f79c1022ffa5291d33520cbbb53b7687b01c2f8e83b57d102eed7ed479d" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.94" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5594,51 +5807,66 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.94" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.94" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] name = "web-sys" -version = "0.3.71" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44188d185b5bdcae1052d08bcbcf9091a5524038d4572cc4f4f2bb9d5554ddd9" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - [[package]] name = "whoami" version = "1.5.2" @@ -5690,33 +5918,38 @@ dependencies = [ ] [[package]] -name = "windows-registry" -version = "0.2.0" +name = "windows-link" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", "windows-strings", - "windows-targets 0.52.6", + "windows-targets 0.53.0", ] [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -5794,13 +6027,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -5819,6 +6068,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -5837,6 +6092,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -5855,12 +6116,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -5879,6 +6152,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -5897,6 +6176,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -5915,6 +6200,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -5933,6 +6224,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.6.20" @@ -5942,6 +6239,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "wyz" version = "0.5.1" @@ -5981,7 +6287,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5992,18 +6298,16 @@ checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zip" -version = "2.2.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", - "displaydoc", "flate2", "indexmap", "memchr", - "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3c0232a..a0ff043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,31 +6,30 @@ edition = "2021" [dependencies] anyhow = "1.0" ariadne = { version = "0.4", features = ["auto-color"] } -axum = "0.7" +axum = { version = "0.8", features = ["macros"] } badge-maker = "0.3" blake3 = "1.5" bytes = "1.7" -chrono = "0.4" futures-util = "0.3.31" grass = "0.13" hex = "0.4" human-size = "0.4" image = "0.25" -itertools = "0.13" -lightningcss = "1.0.0-alpha.59" +itertools = "0.14" +lightningcss = "1.0.0-alpha.65" mime = "0.3" -minijinja = { version = "2.3", features = ["loader", "json"] } -minijinja-autoreload = "2.3" +minijinja = { version = "2.9", features = ["loader", "json"] } +minijinja-autoreload = "2.9" moka = { version = "0.12", features = ["future"] } -objdiff-core = { version = "2.2", features = ["bindings"] } +objdiff-core = { version = "2.5", features = ["bindings"] } #objdiff-core = { path = "../objdiff/objdiff-core", features = ["bindings"] } -octocrab = "0.41" +octocrab = { version = "0.44", features = ["stream"] } oxc = { version = "0.31", features = ["codegen", "minifier", "transformer", "semantic"] } palette = "0.7" prost = "0.13" regex = "1.11" reqwest = { version = "0.12", features = ["json", "gzip", "zstd"] } -resvg = "0.44" +resvg = "0.45" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" @@ -44,5 +43,14 @@ tower-http = { version = "0.6", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = { version = "2.5", features = ["serde"] } -zip = { version = "2.2", default-features = false, features = ["deflate-flate2"] } +zip = { version = "2.6", default-features = false, features = ["flate2", "deflate-flate2"] } zstd = "0.13" +jsonwebtoken = "9.3" +hmac = "0.12" +sha2 = "0.10" +async-openai = "0.28" +tower-sessions = "0.14" +tower-sessions-sqlx-store = { version = "0.15", features = ["sqlite"] } +time = { version = "0.3", features = ["local-offset"] } +rand = "0.9" +base64 = "0.22" diff --git a/config.example.yml b/config.example.yml index 26e6fce..6fd1f82 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,6 +1,12 @@ server: port: 3000 -app: - db_url: sqlite:./db.sqlite - github_token: ghp_abcd1234abcd1234abcd1234abcd1234abcd +db: + url: sqlite:./db.sqlite + +github: + # Personal access token for public repo access + token: ghp_abcd1234abcd1234abcd1234abcd1234abcd + +openai: + api_key: sk-proj-abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234 diff --git a/css/main.scss b/css/main.scss index a256fd3..7ca775e 100644 --- a/css/main.scss +++ b/css/main.scss @@ -294,6 +294,22 @@ $unit-size: 0.75rem; margin-top: calc(var(--pico-block-spacing-vertical) * 2); } +footer { + display: flex; + justify-content: space-between; + + // Log out button + input[type="submit"] { + padding: .3em; + margin: 0; + background: none; + border: none; + width: auto; + font-size: .875em; + height: auto; + } +} + #tooltip { --arrow-size: 0.3rem; position: absolute; diff --git a/migrations/15_project_workflow_id.sql b/migrations/15_project_workflow_id.sql new file mode 100644 index 0000000..e52f0da --- /dev/null +++ b/migrations/15_project_workflow_id.sql @@ -0,0 +1 @@ +ALTER TABLE projects ADD COLUMN workflow_id TEXT; diff --git a/src/config.rs b/src/config.rs index 0999fb5..a4fa003 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { pub server: ServerConfig, - pub app: AppConfig, + pub db: DbConfig, + pub github: GitHubConfig, + pub openai: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -12,7 +14,32 @@ pub struct ServerConfig { } #[derive(Debug, Clone, Deserialize, Serialize)] -pub struct AppConfig { - pub db_url: String, - pub github_token: String, +pub struct DbConfig { + pub url: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GitHubConfig { + pub token: String, + pub app: Option, + pub oauth: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GitHubAppConfig { + pub id: u64, + pub webhook_secret: String, + pub private_key: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GitHubOAuthConfig { + pub client_id: String, + pub client_secret: String, + pub redirect_uri: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct OpenAiConfig { + pub api_key: String, } diff --git a/src/cron.rs b/src/cron.rs index e649a76..34737fd 100644 --- a/src/cron.rs +++ b/src/cron.rs @@ -1,30 +1,65 @@ use anyhow::Result; use tokio_cron_scheduler::{Job, JobScheduler}; +use tower_sessions::{ExpiredDeletion}; use tracing::log; use crate::{github, AppState}; pub type Scheduler = JobScheduler; -pub async fn create(state: AppState) -> Result { +pub async fn create( + state: AppState, + session_store: impl ExpiredDeletion + Clone, +) -> Result { let sched = JobScheduler::new().await?; - sched - .add(Job::new_async("0 0/5 * * * *", move |_uuid, _l| { - let mut state = state.clone(); - Box::pin(async move { - refresh_projects(&mut state).await.expect("Failed to refresh projects"); - }) - })?) - .await?; + { + let state = state.clone(); + sched + .add(Job::new_async("0 0/5 * * * *", move |_uuid, _l| { + let state = state.clone(); + Box::pin(async move { + refresh_projects(&state).await.expect("Failed to refresh projects"); + }) + })?) + .await?; + } + { + sched + .add(Job::new_async("0 0 0/24 * * *", move |_uuid, _l| { + let state = state.clone(); + Box::pin(async move { + state.db.cleanup_report_units().await.expect("Failed to clean up report units"); + }) + })?) + .await?; + } + { + sched + .add(Job::new_async("0 0/1 * * * *", move |_uuid, _l| { + let session_store = session_store.clone(); + Box::pin(async move { + session_store + .delete_expired() + .await + .expect("Failed to delete expired sessions"); + }) + })?) + .await?; + } sched.start().await?; Ok(sched) } -pub async fn refresh_projects(state: &mut AppState) -> Result<()> { +pub async fn refresh_projects(state: &AppState) -> Result<()> { for project_info in state.db.get_projects().await? { - if let Err(e) = - github::run(state, &project_info.project.owner, &project_info.project.repo, 0).await - { + // Skip projects with active app installations + if let Some(installations) = &state.github.installations { + let installations = installations.lock().await; + if installations.owner_to_installation.contains_key(&project_info.project.owner) { + continue; + } + } + if let Err(e) = github::run(state, project_info.project.id, 0).await { log::error!( "Failed to refresh {}/{}: {:?}", project_info.project.owner, diff --git a/src/db/mod.rs b/src/db/mod.rs index d471b67..dd4bd7c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -9,9 +9,9 @@ use prost::Message; use sqlx::{ migrate::MigrateDatabase, Connection, Executor, Pool, Row, Sqlite, SqliteConnection, SqlitePool, }; - +use time::{OffsetDateTime, UtcOffset}; use crate::{ - config::AppConfig, + config::DbConfig, models::{ CachedReport, CachedReportFile, Commit, FrogressMapping, FullReport, FullReportFile, Project, ProjectInfo, @@ -20,7 +20,7 @@ use crate::{ #[derive(Clone)] pub struct Database { - pool: Pool, + pub pool: Pool, report_cache: Cache, report_unit_cache: Cache>, } @@ -40,14 +40,14 @@ pub type UnitKey = [u8; 32]; const BIND_LIMIT: usize = 32766; impl Database { - pub async fn new(config: &AppConfig) -> Result { - if !Sqlite::database_exists(&config.db_url).await.unwrap_or(false) { - tracing::info!(db_url = %config.db_url, "Creating database"); - Sqlite::create_database(&config.db_url).await.context("Failed to create database")?; + pub async fn new(config: &DbConfig) -> Result { + if !Sqlite::database_exists(&config.url).await.unwrap_or(false) { + tracing::info!(url = %config.url, "Creating database"); + Sqlite::create_database(&config.url).await.context("Failed to create database")?; tracing::info!("Database created"); } let pool = - SqlitePool::connect(&config.db_url).await.context("Failed to connect to database")?; + SqlitePool::connect(&config.url).await.context("Failed to connect to database")?; sqlx::migrate!("./migrations") .run(&pool) .await @@ -74,14 +74,14 @@ impl Database { let db = Self { pool, report_cache, report_unit_cache }; db.fixup_report_units().await?; db.migrate_reports().await?; - db.cleanup_report_units().await?; + // db.cleanup_report_units().await?; Ok(db) } pub async fn close(&self) { self.pool.close().await } pub async fn insert_report( - &mut self, + &self, project: &Project, commit: &Commit, version: &str, @@ -108,6 +108,7 @@ impl Database { report.migrate()?; let units = mem::take(&mut report.units); let data = compress(&report.encode_to_vec()); + let timestamp = commit.timestamp.to_offset(UtcOffset::UTC); let report_id = sqlx::query!( r#" INSERT INTO reports (project_id, version, git_commit, timestamp, data, data_version) @@ -119,7 +120,7 @@ impl Database { project_id, version, commit.sha, - commit.timestamp, + timestamp, data, report.version, ) @@ -221,7 +222,7 @@ impl Database { Commit { sha: row.git_commit, message: row.git_commit_message, - timestamp: row.timestamp.and_utc(), + timestamp: row.timestamp.to_utc(), }, row.version, CachedReport { @@ -309,7 +310,7 @@ impl Database { version: file.version.clone(), report: FullReport { version: file.report.version, - measures: file.report.measures.clone(), + measures: file.report.measures, units: units.into_iter().map(Option::unwrap).collect(), categories: file.report.categories.clone(), }, @@ -346,7 +347,7 @@ impl Database { let mut conn = self.pool.acquire().await?; let project = match sqlx::query!( r#" - SELECT id AS "id!", owner, repo, name, short_name, default_category, default_version, platform + SELECT id AS "id!", owner, repo, name, short_name, default_category, default_version, platform, workflow_id FROM projects WHERE owner = ? COLLATE NOCASE AND repo = ? COLLATE NOCASE "#, @@ -365,6 +366,7 @@ impl Database { default_category: row.default_category, default_version: row.default_version, platform: row.platform, + workflow_id: row.workflow_id, }, None => return Ok(None), }; @@ -380,7 +382,7 @@ impl Database { let project_id_db = project_id as i64; let project = match sqlx::query!( r#" - SELECT owner, repo, name, short_name, default_category, default_version, platform + SELECT owner, repo, name, short_name, default_category, default_version, platform, workflow_id FROM projects WHERE id = ? "#, @@ -398,6 +400,7 @@ impl Database { default_category: row.default_category, default_version: row.default_version, platform: row.platform, + workflow_id: row.workflow_id, }, None => return Ok(None), }; @@ -414,7 +417,7 @@ impl Database { struct ReportInfo { git_commit: String, git_commit_message: Option, - timestamp: chrono::NaiveDateTime, + timestamp: OffsetDateTime, version: String, } let reports = if let Some(commit) = commit { @@ -475,7 +478,7 @@ impl Database { }; if let Some(first_report) = reports.first() { // Fetch previous and next commits - let timestamp = first_report.timestamp.and_utc(); + let timestamp = first_report.timestamp; let prev_commit = sqlx::query!( r#" SELECT git_commit @@ -507,7 +510,7 @@ impl Database { info.commit = Some(Commit { sha: first_report.git_commit.clone(), - timestamp: first_report.timestamp.and_utc(), + timestamp: first_report.timestamp.to_utc(), message: first_report.git_commit_message.clone(), }); info.prev_commit = prev_commit; @@ -529,9 +532,10 @@ impl Database { default_category, default_version, platform, + workflow_id, git_commit, git_commit_message, - MAX(timestamp) AS "timestamp: chrono::NaiveDateTime", + MAX(timestamp) AS "timestamp: time::OffsetDateTime", JSON_GROUP_ARRAY(version ORDER BY version) FILTER (WHERE version IS NOT NULL) AS versions FROM projects LEFT JOIN reports ON ( @@ -559,11 +563,12 @@ impl Database { default_category: row.default_category, default_version: row.default_version, platform: row.platform, + workflow_id: row.workflow_id, }, commit: match (row.git_commit, row.timestamp) { (Some(sha), Some(timestamp)) => Some(Commit { sha, - timestamp: timestamp.and_utc(), + timestamp: timestamp.to_utc(), message: row.git_commit_message, }), _ => None, @@ -630,7 +635,7 @@ impl Database { .into_iter() .map(|row| Commit { sha: row.git_commit, - timestamp: row.timestamp.and_utc(), + timestamp: row.timestamp.to_utc(), message: row.git_commit_message, }) .collect::>(); @@ -743,7 +748,7 @@ impl Database { Ok(()) } - async fn cleanup_report_units(&self) -> Result<()> { + pub async fn cleanup_report_units(&self) -> Result<()> { let mut conn = self.pool.acquire().await?; let deleted_report_report_units = sqlx::query!( r#" @@ -874,6 +879,50 @@ impl Database { } Ok(()) } + + pub async fn update_project_workflow_id( + &self, + project_id: u64, + workflow_id: &str, + ) -> Result<()> { + let mut conn = self.pool.acquire().await?; + let project_id_db = project_id as i64; + sqlx::query!( + r#" + UPDATE projects + SET workflow_id = ? + WHERE id = ? + "#, + workflow_id, + project_id_db, + ) + .execute(&mut *conn) + .await?; + Ok(()) + } + + pub async fn update_project_owner_repo( + &self, + project_id: u64, + owner: &str, + repo: &str, + ) -> Result<()> { + let mut conn = self.pool.acquire().await?; + let project_id_db = project_id as i64; + sqlx::query!( + r#" + UPDATE projects + SET owner = ?, repo = ? + WHERE id = ? + "#, + owner, + repo, + project_id_db, + ) + .execute(&mut *conn) + .await?; + Ok(()) + } } thread_local! { diff --git a/src/frogress/mod.rs b/src/frogress/mod.rs index 529dc29..aa44e69 100644 --- a/src/frogress/mod.rs +++ b/src/frogress/mod.rs @@ -1,9 +1,10 @@ +#![allow(unused)] use std::collections::HashMap; -use anyhow::{anyhow, bail, Result}; -use chrono::{MappedLocalTime, TimeZone, Utc}; +use anyhow::{anyhow, bail, Context, Result}; use itertools::Itertools; use objdiff_core::bindings::report::{Measures, Report, ReportCategory}; +use time::UtcDateTime; use tracing::log; use crate::{models::Commit, AppState}; @@ -97,19 +98,13 @@ pub async fn migrate_data(state: &mut AppState) -> Result<()> { ); continue; } - let timestamp = match Utc.timestamp_opt(entry.timestamp as i64, 0) { - MappedLocalTime::Single(t) => t, - MappedLocalTime::Ambiguous(t, _) => t, - MappedLocalTime::None => { - bail!( + let timestamp = UtcDateTime::from_unix_timestamp(entry.timestamp as i64) + .with_context(|| { + format!( "Invalid timestamp {} for {}/{}/{}", - entry.timestamp, - slug, - version, - category - ); - } - }; + entry.timestamp, slug, version, category + ) + })?; let commit = commits.entry(entry.git_hash.clone()).or_insert_with(|| Commit { sha: entry.git_hash.clone(), message: (!entry.description.is_empty()).then(|| entry.description.clone()), @@ -146,7 +141,7 @@ pub async fn migrate_data(state: &mut AppState) -> Result<()> { let percent = if total == 0 { 100.0 } else { (value as f32 / total as f32) * 100.0 }; let measures = if mapping.project_category == "all" { - report.measures.get_or_insert_with(|| Default::default()) + report.measures.get_or_insert_with(Default::default) } else { let mut category = report .categories @@ -161,7 +156,7 @@ pub async fn migrate_data(state: &mut AppState) -> Result<()> { category = report.categories.last_mut(); } let category = category.unwrap(); - category.measures.get_or_insert_with(|| Default::default()) + category.measures.get_or_insert_with(Default::default) }; match mapping.project_measure.as_str() { "matched_code" => { diff --git a/src/github/mod.rs b/src/github/mod.rs index 967e7e7..e2957c3 100644 --- a/src/github/mod.rs +++ b/src/github/mod.rs @@ -1,23 +1,28 @@ use std::{ + collections::{hash_map::Entry, HashMap}, ffi::OsStr, io::{Cursor, Read}, + pin::pin, sync::{Arc, OnceLock}, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use axum::http::StatusCode; +use futures_util::TryStreamExt; use objdiff_core::bindings::report::Report; use octocrab::{ - models::{repos::RepoCommitPage, ArtifactId, Author, RunId}, + models::{repos::RepoCommitPage, ArtifactId, InstallationId, RunId}, params::actions::ArchiveFormat, GitHubError, Octocrab, }; use regex::Regex; -use tokio::{sync::Semaphore, task::JoinSet}; -use tracing::log; +use tokio::{ + sync::{Mutex, Semaphore}, + task::JoinSet, +}; use crate::{ - config::AppConfig, + config::GitHubConfig, models::{Commit, Project}, AppState, }; @@ -25,8 +30,41 @@ use crate::{ #[derive(Clone)] pub struct GitHub { pub client: Octocrab, - #[allow(dead_code)] - pub profile: Author, + pub installations: Option>>, +} + +pub struct Installations { + pub app_client: Octocrab, + pub owner_to_installation: HashMap, + pub clients: HashMap, +} + +impl Installations { + pub fn client_for_installation( + &mut self, + installation_id: InstallationId, + owner: Option<&str>, + ) -> Result { + match self.clients.entry(installation_id) { + Entry::Occupied(entry) => Ok(entry.get().clone()), + Entry::Vacant(entry) => { + // Create a new client for the installation + let client = self.app_client.installation(installation_id)?; + entry.insert(client.clone()); + if let Some(owner) = owner { + self.owner_to_installation.insert(owner.to_string(), installation_id); + } + Ok(client) + } + } + } + + pub fn client_for_owner(&mut self, owner: &str) -> Result> { + if let Some(installation_id) = self.owner_to_installation.get(owner) { + return self.client_for_installation(*installation_id, None).map(Some); + } + Ok(None) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -36,16 +74,55 @@ pub struct GetCommit { sha: String, } +async fn list_installations(app_client: Octocrab) -> Result { + let mut owner_to_installation = HashMap::new(); + let mut clients = HashMap::new(); + { + let mut stream = + pin!(app_client.apps().installations().send().await?.into_stream(&app_client)); + while let Some(installation) = stream.try_next().await? { + let owner = installation.account.login; + if owner_to_installation.contains_key(&owner) { + tracing::warn!("Duplicate installation for {}", owner); + continue; + } + let client = app_client.installation(installation.id)?; + owner_to_installation.insert(owner.clone(), installation.id); + clients.insert(installation.id, client); + } + } + Ok(Installations { app_client, owner_to_installation, clients }) +} + impl GitHub { - pub async fn new(config: &AppConfig) -> Result { + pub async fn new(config: &GitHubConfig) -> Result { let client = Octocrab::builder() - .personal_token(config.github_token.clone()) + .personal_token(config.token.clone()) .build() .context("Failed to create GitHub client")?; octocrab::initialise(client.clone()); let profile = client.current().user().await.context("Failed to fetch current user")?; tracing::info!("Logged in as {}", profile.login); - Ok(Self { client, profile }) + + let installations = if let Some(app_config) = &config.app { + let app_client = Octocrab::builder() + .app( + app_config.id.into(), + jsonwebtoken::EncodingKey::from_rsa_pem(app_config.private_key.as_bytes())?, + ) + .build() + .context("Failed to create GitHub client")?; + let result = + list_installations(app_client).await.context("Failed to fetch installations")?; + tracing::info!("Found {} installations", result.clients.len()); + for (owner, installation_id) in &result.owner_to_installation { + tracing::info!(" - {}: {}", owner, installation_id); + } + Some(Arc::new(Mutex::new(result))) + } else { + None + }; + Ok(Self { client, installations }) } pub async fn get_commit( @@ -56,210 +133,246 @@ impl GitHub { ) -> Result> { match self.client.repos(owner, repo).list_commits().sha(sha).per_page(1).send().await { Ok(page) => Ok(page.items.into_iter().next().map(|c| c.commit)), - Err(octocrab::Error::GitHub { - source: GitHubError { status_code: StatusCode::NOT_FOUND, .. }, - .. - }) => Ok(None), + Err(octocrab::Error::GitHub { source, .. }) + if matches!(*source, GitHubError { status_code: StatusCode::NOT_FOUND, .. }) => + { + Ok(None) + } Err(e) => Err(e.into()), } } + + pub async fn client_for(&self, owner: &str) -> Result { + if let Some(installations) = &self.installations { + let mut installations = installations.lock().await; + if let Some(client) = installations.client_for_owner(owner)? { + return Ok(client); + } + } + Ok(self.client.clone()) + } } -pub async fn run( - state: &mut AppState, - owner_name: &str, - repo_name: &str, - stop_run_id: u64, -) -> Result<()> { - tracing::debug!("Refreshing project {}/{}", owner_name, repo_name); - let existing = state +pub async fn run(state: &AppState, repo_id: u64, stop_run_id: u64) -> Result<()> { + let mut project_info = state .db - .get_project_info(owner_name, repo_name, None) + .get_project_info_by_id(repo_id, None) .await - .context("Failed to fetch project info")?; + .context("Failed to fetch project info")? + .with_context(|| format!("Failed to fetch project info for ID {}", repo_id))?; let repo = state .github .client - .repos(owner_name, repo_name) + .repos_by_id(project_info.project.id) .get() .await - .context("Failed to fetch repo")?; + .with_context(|| format!("Failed to fetch repo for ID {}", repo_id))?; let branch = repo.default_branch.as_deref().unwrap_or("main"); - let Some(owner) = repo.owner else { - return Err(anyhow!("Repo has no owner")); - }; - let project = existing.as_ref().map(|e| e.project.clone()).unwrap_or_else(|| Project { - id: repo.id.0, - owner: owner.login.clone(), - repo: repo.name.clone(), - name: None, - short_name: None, - default_category: None, - default_version: None, - platform: None, - }); - let workflows = state - .github - .client - .workflows(&project.owner, &project.repo) - .list() - .send() - .await - .context("Failed to fetch workflows")?; - let Some(workflow) = workflows.items.iter().find(|w| { - w.path.ends_with("build.yml") - || w.path.ends_with("build.yaml") - || w.path.ends_with("progress.yml") - || w.path.ends_with("progress.yaml") - }) else { - log::warn!("No known workflow found for {}/{}", owner_name, repo_name); - return Ok(()); - }; + let owner = repo.owner.context("Repository has no owner")?; + if project_info.project.owner != owner.login || project_info.project.repo != repo.name { + tracing::info!( + "Migrating project from {}/{} to {}/{}", + project_info.project.owner, + project_info.project.repo, + owner.login, + repo.name + ); + state + .db + .update_project_owner_repo(project_info.project.id, &owner.login, &repo.name) + .await?; + project_info = state + .db + .get_project_info_by_id(repo_id, None) + .await + .context("Failed to fetch project info")? + .with_context(|| format!("Failed to fetch project info for ID {}", repo_id))?; + } - let mut runs = vec![]; - let mut page = 1u32; - 'outer: loop { - let result = state - .github - .client + let project = &project_info.project; + tracing::debug!("Refreshing project {}/{}", project.owner, project.repo); + let client = state.github.client_for(&project.owner).await?; + + let workflow_ids = if let Some(workflow_id) = &project.workflow_id { + vec![workflow_id.clone()] + } else { + let workflows = client .workflows(&project.owner, &project.repo) - .list_runs(workflow.id.to_string()) - .branch(branch) - .event("push") - .status("completed") - .exclude_pull_requests(true) - .page(page) + .list() .send() - .await; - let items = match result { - Ok(result) if result.items.is_empty() => break, - Ok(result) => result.items, - Err(octocrab::Error::GitHub { - source: GitHubError { status_code: StatusCode::NOT_FOUND, .. }, - .. - }) => break, - Err(e) => { - return Err(e).with_context(|| format!("Failed to fetch workflows page {}", page)); - } - }; - for run in items { - if let Some(commit) = existing.as_ref().and_then(|e| e.commit.as_ref()) { - if run.head_sha == commit.sha { + .await + .context("Failed to fetch workflows")?; + workflows.items.into_iter().map(|w| w.path).collect() + }; + if workflow_ids.is_empty() { + tracing::warn!("No workflows found for {}/{}", project.owner, project.repo); + return Ok(()); + } + for workflow_id in workflow_ids { + let workflow_id = + workflow_id.strip_prefix(".github/workflows/").unwrap_or(workflow_id.as_str()); + let mut runs = vec![]; + let mut page = 1u32; + 'outer: loop { + let result = client + .workflows(&project.owner, &project.repo) + .list_runs(workflow_id) + .branch(branch) + .event("push") + .status("completed") + .exclude_pull_requests(true) + .page(page) + .send() + .await; + let items = match result { + Ok(result) if result.items.is_empty() => break, + Ok(result) => result.items, + Err(octocrab::Error::GitHub { source, .. }) + if matches!(*source, GitHubError { + status_code: StatusCode::NOT_FOUND, + .. + }) => + { + break + } + Err(e) => { + return Err(e) + .with_context(|| format!("Failed to fetch workflows page {}", page)); + } + }; + for run in items { + if let Some(commit) = project_info.commit.as_ref() { + if run.head_sha == commit.sha { + break 'outer; + } + } + let run_id = run.id; + runs.push(run); + if run_id == RunId(stop_run_id) { break 'outer; } } - let run_id = run.id; - runs.push(run); - if run_id == RunId(stop_run_id) { - break 'outer; - } + page += 1; } - page += 1; - } - tracing::info!("Fetched {} runs for project {}/{}", runs.len(), owner_name, repo_name); + tracing::info!( + "Fetched {} runs for project {}/{} {}", + runs.len(), + project.owner, + project.repo, + workflow_id + ); - struct TaskResult { - run_id: RunId, - commit: Commit, - result: Result, - } - let sem = Arc::new(Semaphore::new(10)); - let mut set = JoinSet::new(); - for run in runs { - let sem = sem.clone(); - let project = project.clone(); - let github = state.github.clone(); - let db = state.db.clone(); - let run_id = run.id; - let commit = Commit::from(&run.head_commit); - set.spawn(async move { - let _permit = sem.acquire().await.unwrap(); - match db.report_exists(project.id, &commit.sha).await { - Ok(true) => { - return TaskResult { - run_id, - commit, - result: Ok(ProcessWorkflowRunResult { artifacts: vec![] }), - }; + struct TaskResult { + run_id: RunId, + commit: Commit, + result: Result, + } + let sem = Arc::new(Semaphore::new(10)); + let mut set = JoinSet::new(); + for run in runs { + let sem = sem.clone(); + let project = project.clone(); + let client = client.clone(); + let db = state.db.clone(); + let run_id = run.id; + let commit = Commit::from(&run.head_commit); + set.spawn(async move { + let _permit = sem.acquire().await.unwrap(); + match db.report_exists(project.id, &commit.sha).await { + Ok(true) => { + return TaskResult { + run_id, + commit, + result: Ok(ProcessWorkflowRunResult { artifacts: vec![] }), + }; + } + Ok(false) => {} + Err(e) => return TaskResult { run_id, commit, result: Err(e) }, } - Ok(false) => {} - Err(e) => return TaskResult { run_id, commit, result: Err(e) }, - } - let result = process_workflow_run(github, project, run.id).await; - TaskResult { run_id, commit, result } - }); - } - while let Some(join_result) = set.join_next().await { - match join_result { - Ok(TaskResult { - run_id, - commit, - result: Ok(ProcessWorkflowRunResult { artifacts }), - }) => { - tracing::debug!( - "Processed workflow run {} ({}) (artifacts {})", + let result = process_workflow_run(&client, &project, run.id).await; + TaskResult { run_id, commit, result } + }); + } + let mut found_artifacts = false; + while let Some(join_result) = set.join_next().await { + match join_result { + Ok(TaskResult { run_id, - commit.sha, - artifacts.len() - ); - for artifact in artifacts { - let start = std::time::Instant::now(); - state - .db - .insert_report(&project, &commit, &artifact.version, *artifact.report) - .await?; - let duration = start.elapsed(); - tracing::info!( - "Inserted report {} ({}) in {}ms", - artifact.version, + commit, + result: Ok(ProcessWorkflowRunResult { artifacts }), + }) => { + tracing::debug!( + "Processed workflow run {} ({}) (artifacts {})", + run_id, commit.sha, - duration.as_millis() + artifacts.len() + ); + for artifact in artifacts { + let start = std::time::Instant::now(); + state + .db + .insert_report(&project, &commit, &artifact.version, *artifact.report) + .await?; + let duration = start.elapsed(); + tracing::info!( + "Inserted report {} ({}) in {}ms", + artifact.version, + commit.sha, + duration.as_millis() + ); + found_artifacts = true; + } + } + Ok(TaskResult { run_id, commit, result: Err(e) }) => { + tracing::error!( + "Failed to process workflow run {} ({}): {:?}", + run_id, + commit.sha, + e ); } - } - Ok(TaskResult { run_id, commit, result: Err(e) }) => { - tracing::error!( - "Failed to process workflow run {} ({}): {:?}", - run_id, - commit.sha, - e - ); - } - Err(e) => { - tracing::error!("Failed to process workflow run: {:?}", e); + Err(e) => { + tracing::error!("Failed to process workflow run: {:?}", e); + } } } + + if found_artifacts { + if project.workflow_id.is_none() { + state.db.update_project_workflow_id(project.id, workflow_id).await?; + } + break; + } } + Ok(()) } -struct ProcessWorkflowRunResult { - artifacts: Vec, +pub struct ProcessWorkflowRunResult { + pub artifacts: Vec, } -struct ProcessArtifactResult { - version: String, - report: Box, +pub struct ProcessArtifactResult { + pub version: String, + pub report: Box, } -async fn process_workflow_run( - github: GitHub, - project: Project, +pub async fn process_workflow_run( + client: &Octocrab, + project: &Project, run_id: RunId, ) -> Result { - let artifacts = github - .client + let artifacts = client .all_pages( - github - .client + client .actions() .list_workflow_run_artifacts(&project.owner, &project.repo, run_id) .send() .await .context("Failed to fetch artifacts")? .value - .unwrap(), + .unwrap_or_default(), ) .await?; tracing::debug!("Run {} (artifacts {})", run_id, artifacts.len()); @@ -270,6 +383,9 @@ async fn process_workflow_run( static REGEX: OnceLock = OnceLock::new(); let regex = REGEX .get_or_init(|| Regex::new(r"^(?P[A-z0-9_.\-]+)[_-]report(?:[_-].*)?$").unwrap()); + static MAPS_REGEX: OnceLock = OnceLock::new(); + let maps_regex = + MAPS_REGEX.get_or_init(|| Regex::new(r"^(?P[A-z0-9_\-]+)_maps$").unwrap()); let sem = Arc::new(Semaphore::new(3)); let mut set = JoinSet::new(); struct TaskResult { @@ -283,9 +399,6 @@ async fn process_workflow_run( version.as_str().to_string() } else if artifact_name == "progress" || artifact_name == "progress.json" { // bfbb compatibility - static MAPS_REGEX: OnceLock = OnceLock::new(); - let maps_regex = MAPS_REGEX - .get_or_init(|| Regex::new(r"^(?P[A-z0-9_\-]+)_maps$").unwrap()); if let Some(version) = artifacts.iter().find_map(|a| { maps_regex .captures(&a.name) @@ -300,14 +413,14 @@ async fn process_workflow_run( continue; }; let sem = sem.clone(); - let github = github.clone(); + let client = client.clone(); let project = project.clone(); let artifact_id = artifact.id; set.spawn(async move { let _permit = sem.acquire().await.unwrap(); TaskResult { artifact_name, - result: download_artifact(github, project, artifact_id, version).await, + result: download_artifact(client, project, artifact_id, version).await, } }); } @@ -337,13 +450,12 @@ async fn process_workflow_run( type DownloadArtifactResult = Result)>>; async fn download_artifact( - github: GitHub, + client: Octocrab, project: Project, artifact_id: ArtifactId, version: String, ) -> DownloadArtifactResult { - let bytes = github - .client + let bytes = client .actions() .download_artifact(&project.owner, &project.repo, artifact_id, ArchiveFormat::Zip) .await?; diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs new file mode 100644 index 0000000..bae0f47 --- /dev/null +++ b/src/handlers/auth.rs @@ -0,0 +1,276 @@ +use anyhow::{anyhow, bail, Context}; +use axum::{ + extract::{FromRef, FromRequestParts, OptionalFromRequestParts, Query, State}, + http::{header::ACCEPT, request::Parts, StatusCode}, + response::{IntoResponse, Redirect, Response}, +}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use octocrab::{models::Author, Octocrab}; +use rand::{rngs::OsRng, TryRngCore}; +use time::{Duration, UtcDateTime}; +use tower_sessions::Session; + +use crate::{config::GitHubConfig, handlers::AppError, AppState}; + +const GITHUB_OAUTH_STATE: &str = "github_oauth_state"; +const CURRENT_USER: &str = "current_user"; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct StoredOAuth { + pub access_token: String, + pub token_type: String, + pub expires_at: Option, + pub refresh_token: Option, + pub refresh_token_expires_at: Option, +} + +impl From for octocrab::auth::OAuth { + fn from(value: StoredOAuth) -> Self { + octocrab::auth::OAuth { + access_token: value.access_token.into(), + token_type: value.token_type, + scope: Vec::new(), + expires_in: None, + refresh_token: None, + refresh_token_expires_in: None, + } + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct CurrentUser { + pub oauth: StoredOAuth, + pub profile: Author, +} + +pub async fn login( + session: Session, + State(state): State, + current_user: Option, +) -> Result { + if current_user.is_some() { + return Ok(Redirect::to("/").into_response()); + } + let Some(config) = &state.config.github.oauth else { + tracing::warn!("No GitHub OAuth config found"); + return Ok((StatusCode::INTERNAL_SERVER_ERROR, "No GitHub OAuth config").into_response()); + }; + let mut bytes = [0u8; 16]; + OsRng.try_fill_bytes(&mut bytes)?; + let nonce = URL_SAFE_NO_PAD.encode(bytes); + session.insert(GITHUB_OAUTH_STATE, nonce.clone()).await?; + let mut redirect_url = url::Url::parse("https://github.com/login/oauth/authorize")?; + let mut query = redirect_url.query_pairs_mut(); + query.append_pair("client_id", &config.client_id); + query.append_pair("redirect_uri", &config.redirect_uri); + query.append_pair("state", &nonce); + drop(query); + Ok(Redirect::to(redirect_url.as_str()).into_response()) +} + +pub async fn logout(session: Session) -> Result { + session.remove_value(CURRENT_USER).await?; + session.remove_value(GITHUB_OAUTH_STATE).await?; + Ok(Redirect::to("/").into_response()) +} + +#[derive(serde::Deserialize)] +pub struct OAuthQuery { + pub code: String, + pub state: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct OAuthResponse { + pub access_token: String, + pub token_type: String, + pub expires_in: Option, + pub refresh_token: Option, + pub refresh_token_expires_in: Option, +} + +impl From for octocrab::auth::OAuth { + fn from(value: OAuthResponse) -> Self { + octocrab::auth::OAuth { + access_token: value.access_token.into(), + token_type: value.token_type, + scope: Vec::new(), + expires_in: None, + refresh_token: None, + refresh_token_expires_in: None, + } + } +} + +impl From for StoredOAuth { + fn from(value: OAuthResponse) -> Self { + StoredOAuth { + access_token: value.access_token, + token_type: value.token_type, + expires_at: value.expires_in.map(|s| UtcDateTime::now() + Duration::seconds(s)), + refresh_token: value.refresh_token, + refresh_token_expires_at: value + .refresh_token_expires_in + .map(|s| UtcDateTime::now() + Duration::seconds(s)), + } + } +} + +#[derive(serde::Serialize)] +struct FetchAccessToken<'a> { + client_id: &'a str, + client_secret: &'a str, + code: &'a str, +} + +#[derive(serde::Serialize)] +struct RefreshAccessToken<'a> { + client_id: &'a str, + client_secret: &'a str, + grant_type: &'a str, + refresh_token: &'a str, +} + +pub async fn oauth( + session: Session, + Query(OAuthQuery { code, state: oauth_state }): Query, + State(state): State, +) -> Result { + let existing_state = session.get::(GITHUB_OAUTH_STATE).await?; + let Some(existing_state) = existing_state else { + tracing::warn!("No state found in session"); + return Ok((StatusCode::BAD_REQUEST, "No state found").into_response()); + }; + if existing_state != oauth_state { + tracing::warn!("State mismatch: expected {}, got {}", existing_state, oauth_state); + return Ok((StatusCode::BAD_REQUEST, "State mismatch").into_response()); + } + session.remove_value(GITHUB_OAUTH_STATE).await?; + + let current_user = fetch_access_token(&state.config.github, &code).await?; + session.insert(CURRENT_USER, current_user).await?; + + Ok(Redirect::to("/").into_response()) +} + +fn oauth_client() -> Octocrab { + Octocrab::builder() + .base_uri("https://github.com") + .expect("Failed to create base URI") + .add_header(ACCEPT, "application/json".to_string()) + .build() + .expect("Failed to create Octocrab client") +} + +async fn fetch_access_token(config: &GitHubConfig, code: &str) -> Result { + let Some(oauth_config) = &config.oauth else { + tracing::warn!("No GitHub OAuth config found"); + return Err(AppError::Internal(anyhow!("No GitHub OAuth config"))); + }; + let base_client = oauth_client(); + let oauth: OAuthResponse = base_client + .post( + "/login/oauth/access_token", + Some(&FetchAccessToken { + client_id: &oauth_config.client_id, + client_secret: &oauth_config.client_secret, + code, + }), + ) + .await?; + let oauth = StoredOAuth::from(oauth); + let client = Octocrab::builder().oauth(oauth.clone().into()).build()?; + let profile = client.current().user().await.context("Failed to fetch current user")?; + tracing::info!("Logged in as @{}", profile.login); + Ok(CurrentUser { oauth, profile }) +} + +async fn refresh_access_token( + config: &GitHubConfig, + refresh_token: &str, +) -> Result { + let Some(oauth_config) = &config.oauth else { + tracing::warn!("No GitHub OAuth config found"); + bail!("No GitHub OAuth config found"); + }; + let base_client = oauth_client(); + let oauth: OAuthResponse = base_client + .post( + "/login/oauth/access_token", + Some(&RefreshAccessToken { + client_id: &oauth_config.client_id, + client_secret: &oauth_config.client_secret, + grant_type: "refresh_token", + refresh_token, + }), + ) + .await?; + let oauth = StoredOAuth::from(oauth); + let client = Octocrab::builder().oauth(oauth.clone().into()).build()?; + let profile = client.current().user().await.context("Failed to fetch current user")?; + tracing::info!("Refreshed token for @{}", profile.login); + Ok(CurrentUser { oauth, profile }) +} + +impl FromRequestParts for CurrentUser +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + >::from_request_parts(parts, state) + .await? + .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized")) + } +} + +impl OptionalFromRequestParts for CurrentUser +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts( + parts: &mut Parts, + state: &S, + ) -> Result, Self::Rejection> { + let session = Session::from_request_parts(parts, state).await?; + let app_state = AppState::from_ref(state); + let Some(user) = session.get::(CURRENT_USER).await.ok().flatten() else { + return Ok(None); + }; + if let Some(expires_at) = user.oauth.expires_at { + if (UtcDateTime::now() + Duration::seconds(30)) > expires_at { + // Access token expired, attempt to refresh + if let Err(e) = session.remove_value(CURRENT_USER).await { + tracing::error!("Failed to remove user from session: {}", e); + }; + if let Some(refresh_token) = &user.oauth.refresh_token { + if let Some(refresh_token_expires_at) = user.oauth.refresh_token_expires_at { + if UtcDateTime::now() >= refresh_token_expires_at { + // Refresh token expired + return Ok(None); + } + } + let current_user = + match refresh_access_token(&app_state.config.github, refresh_token).await { + Ok(current_user) => current_user, + Err(e) => { + tracing::error!("Failed to refresh access token: {:?}", e); + return Ok(None); + } + }; + if let Err(e) = session.insert(CURRENT_USER, current_user.clone()).await { + tracing::error!("Failed to insert user into session: {}", e); + } + return Ok(Some(current_user)); + } + return Ok(None); + } + } + Ok(Some(user)) + } +} diff --git a/src/handlers/github.rs b/src/handlers/github.rs new file mode 100644 index 0000000..92ac34a --- /dev/null +++ b/src/handlers/github.rs @@ -0,0 +1,514 @@ +use std::{cmp::Ordering, fmt::Display}; + +use anyhow::{Context, Result}; +use axum::{ + body::Bytes, + extract::{FromRef, FromRequest, Request}, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use hmac::{Hmac, Mac}; +use objdiff_core::bindings::report::{ + ChangeItem, ChangeItemInfo, ChangeUnit, Changes, Report, ReportItem, ReportUnit, +}; +use octocrab::{ + models::{ + pulls::PullRequest, + webhook_events::{ + payload::{ + InstallationWebhookEventAction, PullRequestWebhookEventAction, + WorkflowRunWebhookEventAction, + }, + EventInstallation, WebhookEvent, WebhookEventPayload, + }, + workflows::{Run, WorkFlow}, + }, + Octocrab, +}; +use sha2::Sha256; + +use crate::{ + github::{process_workflow_run, ProcessArtifactResult, ProcessWorkflowRunResult}, + handlers::AppError, + models::{Commit, FullReportFile}, + AppState, +}; + +#[derive(Debug, Clone, serde::Deserialize)] +pub struct RunWithPullRequests { + #[serde(flatten)] + pub inner: Run, + pub pull_requests: Vec, +} + +pub async fn webhook( + GitHubEvent { event, state }: GitHubEvent, +) -> Result { + let Some(installations) = &state.github.installations else { + tracing::warn!("Received webhook event {:?} with no GitHub app config", event.kind); + return Ok((StatusCode::OK, "No app config").into_response()); + }; + let mut owner = None; + if let Some(repository) = event.repository { + owner = repository.owner.map(|o| o.login.clone()); + if let Some(full_name) = repository.full_name { + tracing::warn!("Received webhook event {:?} from repository {}", event.kind, full_name); + } else { + tracing::warn!( + "Received webhook event {:?} from repository ID {}", + event.kind, + repository.id.0 + ); + } + } else if let Some(organization) = event.organization { + owner = Some(organization.login.clone()); + tracing::warn!("Received webhook event {:?} from org {}", event.kind, organization.login); + } else if let Some(sender) = event.sender { + tracing::warn!("Received webhook event {:?} from @{}", event.kind, sender.login); + } else { + tracing::warn!("Received webhook event {:?} from unknown source", event.kind); + } + let installation_id = match event.installation { + Some(EventInstallation::Full(installation)) => { + owner = Some(installation.account.login.clone()); + Some(installation.id) + } + Some(EventInstallation::Minimal(installation)) => Some(installation.id), + None => None, + }; + let client = if let Some(installation_id) = installation_id { + let mut installations = installations.lock().await; + installations.client_for_installation(installation_id, owner.as_deref())? + } else { + state.github.client.clone() + }; + match event.specific { + WebhookEventPayload::WorkflowRun(inner) => { + if inner.action == WorkflowRunWebhookEventAction::Completed { + let Some(workflow) = inner.workflow else { + tracing::error!("Received workflow_run event with no workflow"); + return Ok((StatusCode::OK, "No workflow run").into_response()); + }; + let workflow: WorkFlow = match serde_json::from_value(workflow) { + Ok(workflow) => workflow, + Err(e) => { + tracing::error!("Received workflow_run event with invalid workflow: {e}"); + return Ok((StatusCode::OK, "Invalid workflow").into_response()); + } + }; + let workflow_run: RunWithPullRequests = + match serde_json::from_value(inner.workflow_run) { + Ok(workflow_run) => workflow_run, + Err(e) => { + tracing::error!( + "Received workflow_run event with invalid workflow_run: {e}" + ); + return Ok((StatusCode::OK, "Invalid workflow run").into_response()); + } + }; + if let Err(e) = + handle_workflow_run_completed(&state, client, workflow, workflow_run).await + { + tracing::error!("Error handling workflow_run event: {e}"); + return Ok((StatusCode::OK, "Internal error").into_response()); + } + } + } + WebhookEventPayload::PullRequest(inner) => { + if inner.action == PullRequestWebhookEventAction::Opened + || inner.action == PullRequestWebhookEventAction::Synchronize + { + if let Err(e) = handle_pull_request_update(&state, client, inner.pull_request).await + { + tracing::error!("Error handling pull_request event: {e}"); + return Ok((StatusCode::OK, "Internal error").into_response()); + } + } + } + WebhookEventPayload::Installation(inner) => { + tracing::info!( + "Installation {:?} for {}", + inner.action, + owner.as_deref().unwrap_or("[unknown]") + ); + match inner.action { + InstallationWebhookEventAction::Created => { + // Installation client is already created + } + InstallationWebhookEventAction::Deleted => { + // Remove the installation client + let mut installations = installations.lock().await; + if let Some(owner) = &owner { + installations.owner_to_installation.remove(owner); + } else { + tracing::warn!("Received installation deleted event with no owner"); + } + if let Some(installation_id) = installation_id { + installations.clients.remove(&installation_id); + } else { + tracing::warn!( + "Received installation deleted event with no installation ID" + ); + } + } + _ => {} + } + } + _ => {} + } + Ok((StatusCode::OK, "Event processed").into_response()) +} + +async fn handle_workflow_run_completed( + state: &AppState, + client: Octocrab, + _workflow: WorkFlow, + workflow_run: RunWithPullRequests, +) -> Result<()> { + let RunWithPullRequests { inner: workflow_run, pull_requests } = workflow_run; + let repository_id = workflow_run.repository.id.into_inner(); + let Some(project_info) = state.db.get_project_info_by_id(repository_id, None).await? else { + tracing::warn!("No project found for repository ID {}", repository_id); + return Ok(()); + }; + let repository = + client.repos_by_id(repository_id).get().await.context("Failed to fetch repository")?; + let ProcessWorkflowRunResult { artifacts } = + process_workflow_run(&client, &project_info.project, workflow_run.id).await?; + tracing::debug!( + "Processed workflow run {} ({}) (artifacts {})", + workflow_run.id, + workflow_run.head_sha, + artifacts.len() + ); + if artifacts.is_empty() { + return Ok(()); + } + + let commit = Commit::from(&workflow_run.head_commit); + if workflow_run.event == "push" + && match &repository.default_branch { + Some(default_branch) => default_branch == &workflow_run.head_branch, + None => matches!(workflow_run.head_branch.as_str(), "master" | "main"), + } + { + // Insert reports into the database + for artifact in artifacts { + let start = std::time::Instant::now(); + state + .db + .insert_report(&project_info.project, &commit, &artifact.version, *artifact.report) + .await?; + let duration = start.elapsed(); + tracing::info!( + "Inserted report {} ({}) in {}ms", + artifact.version, + commit.sha, + duration.as_millis() + ); + } + } else if workflow_run.event == "pull_request" { + // Fetch any associated pull requests + for pull_request in pull_requests { + tracing::info!("Processing pull request {}", pull_request.id); + let issues = client.issues_by_id(repository_id); + // Only fetch first page for now + let existing_comments = issues.list_comments(pull_request.number).send().await?; + for artifact in &artifacts { + let Some(cached_report) = state + .db + .get_report( + &project_info.project.owner, + &project_info.project.repo, + &pull_request.base.sha, + &artifact.version, + ) + .await? + else { + tracing::warn!( + "No report found for pull request {} (base {}) and version {}", + pull_request.id, + pull_request.base.sha, + artifact.version + ); + continue; + }; + let report_file = state.db.upgrade_report(&cached_report).await?; + let report = report_file.report.flatten(); + let changes = changes(&report, &artifact.report)?; + let comment_text = generate_comment(&report_file, artifact, &commit, changes); + let existing_comment = existing_comments + .items + .iter() + .find(|comment| { + // TODO check author ID + comment.body.as_ref().is_some_and(|body| { + body.contains(format!("Report for {}", artifact.version).as_str()) + }) + }) + .map(|comment| comment.id); + if let Some(existing_comment) = existing_comment { + issues + .update_comment(existing_comment, comment_text) + .await + .context("Failed to update existing comment")?; + } else { + issues + .create_comment(pull_request.number, comment_text) + .await + .context("Failed to create comment")?; + } + } + } + } + + Ok(()) +} + +async fn handle_pull_request_update( + _state: &AppState, + _client: Octocrab, + _pull_request: PullRequest, +) -> Result<()> { + // Handle the pull request update event here + // tracing::info!("Pull request updated: {:?}", pull_request); + Ok(()) +} + +/// Verify and extract GitHub Event Payload. +#[derive(Debug, Clone)] +#[must_use] +pub struct GitHubEvent { + pub event: WebhookEvent, + pub state: S, +} + +impl FromRequest for GitHubEvent +where + AppState: FromRef, + S: Send + Sync + Clone, +{ + type Rejection = Response; + + async fn from_request(req: Request, state: &S) -> Result { + fn err(m: impl Display) -> Response { + tracing::error!("{m}"); + (StatusCode::BAD_REQUEST, m.to_string()).into_response() + } + let event = req + .headers() + .get("X-GitHub-Event") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| err("X-GitHub-Event header missing"))? + .to_string(); + let app_state = AppState::from_ref(state); + let body = if let Some(app_config) = &app_state.config.github.app { + let signature_sha256 = req + .headers() + .get("X-Hub-Signature-256") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| err("X-Hub-Signature-256 missing"))? + .strip_prefix("sha256=") + .ok_or_else(|| err("X-Hub-Signature-256 sha256= prefix missing"))?; + let signature = + hex::decode(signature_sha256).map_err(|_| err("X-Hub-Signature-256 malformed"))?; + let body = + Bytes::from_request(req, state).await.map_err(|_| err("error reading body"))?; + let mut mac = Hmac::::new_from_slice(app_config.webhook_secret.as_bytes()) + .expect("HMAC can take key of any size"); + mac.update(&body); + if mac.verify_slice(&signature).is_err() { + return Err(err("signature mismatch")); + } + body + } else { + Bytes::from_request(req, state).await.map_err(|_| err("error reading body"))? + }; + let value = WebhookEvent::try_from_header_and_body(&event, &body) + .map_err(|_| err("error parsing body"))?; + Ok(GitHubEvent { event: value, state: state.clone() }) + } +} + +fn changes(previous: &Report, current: &Report) -> Result { + let mut changes = Changes { from: previous.measures, to: current.measures, units: vec![] }; + for prev_unit in &previous.units { + let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name); + let sections = process_items(prev_unit, curr_unit, |u| &u.sections); + let functions = process_items(prev_unit, curr_unit, |u| &u.functions); + + let prev_measures = prev_unit.measures; + let curr_measures = curr_unit.and_then(|u| u.measures); + if !functions.is_empty() || prev_measures != curr_measures { + changes.units.push(ChangeUnit { + name: prev_unit.name.clone(), + from: prev_measures, + to: curr_measures, + sections, + functions, + metadata: curr_unit + .as_ref() + .and_then(|u| u.metadata.clone()) + .or_else(|| prev_unit.metadata.clone()), + }); + } + } + for curr_unit in ¤t.units { + if !previous.units.iter().any(|u| u.name == curr_unit.name) { + changes.units.push(ChangeUnit { + name: curr_unit.name.clone(), + from: None, + to: curr_unit.measures, + sections: process_new_items(&curr_unit.sections), + functions: process_new_items(&curr_unit.functions), + metadata: curr_unit.metadata.clone(), + }); + } + } + Ok(changes) +} + +fn process_items &Vec>( + prev_unit: &ReportUnit, + curr_unit: Option<&ReportUnit>, + getter: F, +) -> Vec { + let prev_items = getter(prev_unit); + let mut items = vec![]; + if let Some(curr_unit) = curr_unit { + let curr_items = getter(curr_unit); + for prev_func in prev_items { + let prev_func_info = ChangeItemInfo::from(prev_func); + let prev_func_address = prev_func.metadata.as_ref().and_then(|m| m.virtual_address); + let curr_func = curr_items.iter().find(|f| { + f.name == prev_func.name + || prev_func_address.is_some_and(|a| { + f.metadata.as_ref().and_then(|m| m.virtual_address).is_some_and(|b| a == b) + }) + }); + if let Some(curr_func) = curr_func { + let curr_func_info = ChangeItemInfo::from(curr_func); + if prev_func_info != curr_func_info { + items.push(ChangeItem { + name: curr_func.name.clone(), + from: Some(prev_func_info), + to: Some(curr_func_info), + metadata: curr_func.metadata.clone(), + }); + } + } else { + items.push(ChangeItem { + name: prev_func.name.clone(), + from: Some(prev_func_info), + to: None, + metadata: prev_func.metadata.clone(), + }); + } + } + for curr_func in curr_items { + let curr_func_address = curr_func.metadata.as_ref().and_then(|m| m.virtual_address); + if !prev_items.iter().any(|f| { + f.name == curr_func.name + || curr_func_address.is_some_and(|a| { + f.metadata.as_ref().and_then(|m| m.virtual_address).is_some_and(|b| a == b) + }) + }) { + items.push(ChangeItem { + name: curr_func.name.clone(), + from: None, + to: Some(ChangeItemInfo::from(curr_func)), + metadata: curr_func.metadata.clone(), + }); + } + } + } else { + for prev_func in prev_items { + items.push(ChangeItem { + name: prev_func.name.clone(), + from: Some(ChangeItemInfo::from(prev_func)), + to: None, + metadata: prev_func.metadata.clone(), + }); + } + } + items +} + +fn process_new_items(items: &[ReportItem]) -> Vec { + items + .iter() + .map(|item| ChangeItem { + name: item.name.clone(), + from: None, + to: Some(ChangeItemInfo::from(item)), + metadata: item.metadata.clone(), + }) + .collect() +} + +fn generate_comment( + from: &FullReportFile, + to: &ProcessArtifactResult, + to_commit: &Commit, + changes: Changes, +) -> String { + let mut comment = format!( + "## Report for {} ({} - {})\n\n", + to.version, + &from.commit.sha[..7], + &to_commit.sha[..7] + ); + comment.push_str("### Changes\n"); + for unit in changes.units { + for function in unit.functions { + let (from, to) = match (function.from, function.to) { + (Some(from), Some(to)) => (from, to), + (None, Some(to)) => (ChangeItemInfo::default(), to), + (Some(from), None) => (from, ChangeItemInfo::default()), + (None, None) => continue, + }; + let emoji = if to.fuzzy_match_percent == 100.0 { + "✅" + } else if to.fuzzy_match_percent > from.fuzzy_match_percent { + "📈" + } else { + "📉" + }; + let from_bytes = ((from.fuzzy_match_percent as f64 / 100.0) * from.size as f64) as u64; + let to_bytes = ((to.fuzzy_match_percent as f64 / 100.0) * to.size as f64) as u64; + let bytes_diff = to_bytes as i64 - from_bytes as i64; + let bytes_str = match bytes_diff.cmp(&0) { + Ordering::Less => bytes_diff.to_string(), + Ordering::Equal => "0".to_string(), + Ordering::Greater => format!("+{}", bytes_diff), + }; + let name = function + .metadata + .as_ref() + .and_then(|m| m.demangled_name.as_deref()) + .unwrap_or(&function.name); + comment.push_str(&format!( + "- {} `{} | {}` {} bytes -> {:.2}%\n", + emoji, unit.name, name, bytes_str, to.fuzzy_match_percent + )); + } + } + comment +} + +#[allow(unused)] +fn platform_name(platform: &str) -> &str { + match platform { + "gc" => "GameCube", + "wii" => "Wii", + "n64" => "Nintendo 64", + "switch" => "Nintendo Switch", + "3ds" => "Nintendo 3DS", + "nds" => "Nintendo DS", + "gba" => "Game Boy Advance", + "gbc" => "Game Boy Color", + "ps" => "PlayStation", + "ps2" => "PlayStation 2", + _ => platform, + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 80f8ab3..34e8da9 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,11 +1,10 @@ use std::{convert::Infallible, net::SocketAddr, str::FromStr}; use axum::{ - async_trait, extract::{ConnectInfo, FromRequestParts, OriginalUri}, http::{header, request::Parts, HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, - routing::get, + routing::{get, post}, Extension, Router, }; use bytes::BytesMut; @@ -15,8 +14,10 @@ use prost::Message; use crate::AppState; mod assets; +mod auth; mod badge; mod css; +mod github; mod js; mod og; mod project; @@ -25,14 +26,18 @@ mod treemap; pub fn build_router() -> Router { Router::new() - .route("/css/*filename", get(css::get_css)) - .route("/js/*filename", get(js::get_js)) - .route("/assets/*filename", get(assets::get_asset)) + .route("/api/github/webhook", post(github::webhook)) + .route("/api/github/oauth", get(auth::oauth)) + .route("/login", get(auth::login)) + .route("/logout", post(auth::logout)) + .route("/css/{*filename}", get(css::get_css)) + .route("/js/{*filename}", get(js::get_js)) + .route("/assets/{*filename}", get(assets::get_asset)) .route("/og.png", get(og::get_og)) .route("/", get(project::get_projects)) - .route("/:owner/:repo", get(report::get_report)) - .route("/:owner/:repo/:version", get(report::get_report)) - .route("/:owner/:repo/:version/:commit", get(report::get_report)) + .route("/{owner}/{repo}", get(report::get_report)) + .route("/{owner}/{repo}/{version}", get(report::get_report)) + .route("/{owner}/{repo}/{version}/{commit}", get(report::get_report)) } pub enum AppError { @@ -112,7 +117,6 @@ impl IntoResponse for Protobuf<'_, T> { /// Uses the `x-forwarded-proto` and `x-forwarded-host` headers if present. pub struct FullUri(pub Uri); -#[async_trait] impl FromRequestParts for FullUri where S: Send + Sync { diff --git a/src/handlers/project.rs b/src/handlers/project.rs index b14757a..c9e9318 100644 --- a/src/handlers/project.rs +++ b/src/handlers/project.rs @@ -6,15 +6,16 @@ use axum::{ http::StatusCode, response::{Html, IntoResponse, Response}, }; -use chrono::{DateTime, Utc}; use objdiff_core::bindings::report::Measures; +use octocrab::models::Author; use serde::{Deserialize, Serialize}; +use time::UtcDateTime; use tokio::{sync::Semaphore, task::JoinSet}; use url::Url; use super::{AppError, FullUri}; use crate::{ - handlers::report::TemplateMeasures, + handlers::{auth::CurrentUser, report::TemplateMeasures}, templates::{render, size}, util::UrlExt, AppState, @@ -27,6 +28,7 @@ struct ProjectsTemplateContext<'a> { current_sort: SortOption, canonical_url: &'a str, image_url: &'a str, + current_user: Option, } #[derive(Serialize)] @@ -38,7 +40,7 @@ struct ProjectInfoContext { name: String, short_name: String, commit: String, - timestamp: DateTime, + timestamp: UtcDateTime, measures: TemplateMeasures, platform: Option, code_progress: Vec, @@ -74,6 +76,7 @@ pub async fn get_projects( State(state): State, Query(query): Query, FullUri(uri): FullUri, + current_user: Option, ) -> Result { let start = Instant::now(); let projects = state.db.get_projects().await?; @@ -183,6 +186,7 @@ pub async fn get_projects( current_sort, canonical_url: canonical_url.as_str(), image_url: image_url.as_str(), + current_user: current_user.map(|u| u.profile), })?; let elapsed = start.elapsed(); rendered = rendered.replace("[[time]]", &format!("{}ms", elapsed.as_millis())); diff --git a/src/handlers/report.rs b/src/handlers/report.rs index c006199..9c9f7e0 100644 --- a/src/handlers/report.rs +++ b/src/handlers/report.rs @@ -10,12 +10,17 @@ use axum::{ use image::ImageFormat; use mime::Mime; use objdiff_core::bindings::report::{Measures, ReportCategory, ReportUnit}; +use octocrab::models::Author; use serde::{Deserialize, Serialize}; +use time::format_description::well_known::Rfc3339; use url::Url; use super::{badge, parse_accept, treemap, AppError, FullUri, Protobuf, PROTOBUF}; use crate::{ - handlers::project::{code_progress_sections, data_progress_sections, ProgressSection}, + handlers::{ + auth::CurrentUser, + project::{code_progress_sections, data_progress_sections, ProgressSection}, + }, models::{FullReportFile, Project, ProjectInfo}, templates::render, util::UrlExt, @@ -78,6 +83,7 @@ struct ReportTemplateContext<'a> { source_file_url: Option<&'a str>, code_progress: Vec, data_progress: Vec, + current_user: Option, } #[derive(Serialize)] @@ -206,6 +212,7 @@ pub async fn get_report( headers: HeaderMap, FullUri(uri): FullUri, State(state): State, + current_user: Option, ) -> Result { let start = Instant::now(); let (params, ext) = extract_extension(params); @@ -246,7 +253,7 @@ pub async fn get_report( let scope = apply_scope(&report, &project_info, &query)?; match query.mode.as_deref().unwrap_or("report").to_ascii_lowercase().as_str() { "shield" => mode_shield(&scope, query, &acceptable), - "report" => mode_report(&scope, &state, uri, query, start, &acceptable).await, + "report" => mode_report(&scope, &state, uri, query, start, &acceptable, current_user).await, "measures" => mode_measures(&scope, &acceptable), "history" => mode_history(&scope, &state, query, &acceptable).await, _ => Err(AppError::Status(StatusCode::BAD_REQUEST)), @@ -261,12 +268,13 @@ async fn mode_report( query: ReportQuery, start: Instant, acceptable: &[Mime], + current_user: Option, ) -> Result { for mime in acceptable { if (mime.type_() == mime::STAR && mime.subtype() == mime::STAR) || (mime.type_() == mime::TEXT && mime.subtype() == mime::HTML) { - let mut rendered = render_template(scope, state, uri).await?; + let mut rendered = render_template(scope, state, uri, current_user).await?; let elapsed = start.elapsed(); rendered = rendered.replace("[[time]]", &format!("{}ms", elapsed.as_millis())); return Ok(Html(rendered).into_response()); @@ -385,7 +393,11 @@ async fn mode_history( continue; }; result.push(ReportHistoryEntry { - timestamp: report.commit.timestamp.to_rfc3339(), + timestamp: report + .commit + .timestamp + .format(&Rfc3339) + .unwrap_or_else(|_| "[invalid]".to_string()), commit_sha: report.commit.sha, measures: TemplateMeasures::from(measures.as_ref()), }); @@ -529,7 +541,12 @@ fn apply_scope<'a>( Ok(Scope { report, project_info, measures, current_category, current_unit, units, label }) } -async fn render_template(scope: &Scope<'_>, state: &AppState, uri: Uri) -> Result { +async fn render_template( + scope: &Scope<'_>, + state: &AppState, + uri: Uri, + current_user: Option, +) -> Result { let Scope { report, project_info, measures, current_category, current_unit, units, label } = scope; @@ -668,5 +685,6 @@ async fn render_template(scope: &Scope<'_>, state: &AppState, uri: Uri) -> Resul source_file_url: source_file_url.as_deref(), code_progress: code_progress_sections(measures), data_progress: data_progress_sections(measures), + current_user: current_user.map(|u| u.profile), }) } diff --git a/src/main.rs b/src/main.rs index 09fa91f..a970f3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,8 @@ use tower_http::{ trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}, ServiceBuilderExt, }; +use tower_sessions::{Expiry, SessionManagerLayer, SessionStore}; +use tower_sessions_sqlx_store::SqliteStore; use tracing_subscriber::{filter::LevelFilter, EnvFilter}; use crate::{ @@ -59,24 +61,30 @@ async fn main() { let file = BufReader::new(File::open("config.yml").expect("Failed to open config file")); serde_yaml::from_reader(file).expect("Failed to parse config file") }; - let db = Database::new(&config.app).await.expect("Failed to open database"); - let github = GitHub::new(&config.app).await.expect("Failed to create GitHub client"); + let db = Database::new(&config.db).await.expect("Failed to open database"); + let github = GitHub::new(&config.github).await.expect("Failed to create GitHub client"); let templates = templates::create("templates"); - let mut state = AppState { config, db: db.clone(), github, templates }; + let state = AppState { config, db: db.clone(), github, templates }; + + // Create session store + let session_store = SqliteStore::new(db.pool.clone()); + session_store.migrate().await.expect("Failed to migrate session store"); // Refresh before starting the server // cron::refresh_projects(&mut state).await.expect("Failed to refresh projects"); // frogress::migrate_data(&mut state).await.expect("Failed to migrate data"); // Start the task scheduler - let mut scheduler = cron::create(state.clone()).await.expect("Failed to create scheduler"); + let mut scheduler = cron::create(state.clone(), session_store.clone()) + .await + .expect("Failed to create scheduler"); // Run our service let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, state.config.server.port)); tracing::info!("Listening on {}", addr); axum::serve( TcpListener::bind(addr).await.expect("bind error"), - app(state).into_make_service_with_connect_info::(), + app(state, session_store).into_make_service_with_connect_info::(), ) .with_graceful_shutdown(shutdown_signal()) .await @@ -87,7 +95,7 @@ async fn main() { tracing::info!("Shut down gracefully"); } -fn app(state: AppState) -> Router { +fn app(state: AppState, session_store: impl SessionStore + Clone) -> Router { let sensitive_headers: Arc<[_]> = vec![header::AUTHORIZATION, header::COOKIE].into(); let middleware = ServiceBuilder::new() .sensitive_request_headers(sensitive_headers.clone()) @@ -99,6 +107,11 @@ fn app(state: AppState) -> Router { ) .layer(TimeoutLayer::new(Duration::from_secs(10))) .layer(CorsLayer::new().allow_methods([Method::GET]).allow_origin(cors::Any)) + .layer( + SessionManagerLayer::new(session_store) + .with_secure(false) + .with_expiry(Expiry::OnInactivity(time::Duration::days(1))), + ) .compression(); build_router().layer(middleware).with_state(state) } diff --git a/src/models.rs b/src/models.rs index 7ae47dc..db4e5b6 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,8 +1,8 @@ use std::{borrow::Cow, sync::Arc}; -use chrono::{DateTime, Utc}; use objdiff_core::bindings::report::{Measures, Report, ReportCategory, ReportUnit}; use serde::Serialize; +use time::UtcDateTime; use crate::db::UnitKey; @@ -16,6 +16,7 @@ pub struct Project { pub default_category: Option, pub default_version: Option, pub platform: Option, + pub workflow_id: Option, } impl Project { @@ -59,14 +60,17 @@ impl ProjectInfo { pub struct Commit { pub sha: String, pub message: Option, - pub timestamp: DateTime, + pub timestamp: UtcDateTime, } impl From<&octocrab::models::workflows::HeadCommit> for Commit { fn from(commit: &octocrab::models::workflows::HeadCommit) -> Self { Self { sha: commit.id.clone(), - timestamp: commit.timestamp, + timestamp: UtcDateTime::from_unix_timestamp( + commit.timestamp.to_utc().timestamp_millis(), + ) + .unwrap_or_else(|_| UtcDateTime::now()), message: (!commit.message.is_empty()).then(|| commit.message.clone()), } } diff --git a/src/templates.rs b/src/templates.rs index fc5fcc6..c7ee647 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,9 +1,9 @@ -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc, time::Duration}; use anyhow::{Context, Result}; -use chrono::{DateTime, Utc}; use minijinja::{path_loader, Environment}; use minijinja_autoreload::AutoReloader; +use time::{format_description, macros::format_description, UtcDateTime}; pub type Templates = Arc; @@ -31,20 +31,28 @@ where S: serde::Serialize { } fn timeago(value: String) -> String { - let Ok(value) = DateTime::parse_from_rfc3339(&value) else { - return "[invalid]".to_string(); + let Ok(value) = serde_json::from_str::(&value) else { + return format!("[invalid {}]", value); }; - let Ok(duration) = Utc::now().signed_duration_since(value).to_std() else { + let Ok(duration) = Duration::try_from(UtcDateTime::now() - value) else { return "[out of range]".to_string(); }; timeago::Formatter::new().convert(duration) } fn date(value: String, format: Option) -> String { - let Ok(value) = DateTime::parse_from_rfc3339(&value) else { - return "[invalid]".to_string(); + let Ok(value) = serde_json::from_str::(&value) else { + return format!("[invalid {}]", value); }; - value.format(format.as_deref().unwrap_or("%Y-%m-%d %H:%M:%S %Z")).to_string() + let format = if let Some(format) = format.as_deref() { + match format_description::parse_borrowed::<2>(format) { + Ok(format) => Cow::Owned(format), + Err(_) => return format!("[invalid format {}]", format), + } + } else { + Cow::Borrowed(format_description!("[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]:[offset_minute]")) + }; + value.format(&format).unwrap_or_else(|_| "[invalid]".to_string()) } /// Format a size in bytes to a human-readable string. diff --git a/templates/fragments/footer.html b/templates/fragments/footer.html index 1e7ab6c..f1d3e31 100644 --- a/templates/fragments/footer.html +++ b/templates/fragments/footer.html @@ -1,4 +1,14 @@