diff --git a/FrontEnd/styles/images.scss b/FrontEnd/styles/images.scss index 4d1662990..de3954111 100644 --- a/FrontEnd/styles/images.scss +++ b/FrontEnd/styles/images.scss @@ -34,6 +34,7 @@ --image-download: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iIzJmMmYyZiI+PHBhdGggZD0ibTM3LjkzIDI1LjAxLTIuOTgtNC4wMS03LjQ1IDUuNTN2LTIzLjA5aC01djIzLjA5bC03LjQ1LTUuNTMtMi45OCA0LjAxIDEyLjkzIDkuNjF6Ii8+PHBhdGggZD0ibTQyLjMxIDMzLjA5djguNDdoLTM0LjYydi04LjQ3aC01djEzLjQ3aDQ0LjYydi0xMy40N3oiLz48L2c+PC9zdmc+'); --image-error: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDFjLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjRzMTAuNzUgMjQgMjQgMjQgMjQtMTAuNzUgMjQtMjQtMTAuNzUtMjQtMjQtMjR6bTIuNjYgMzYuMThjLS42NC41OS0xLjUzLjg5LTIuNjguODlzLTIuMDctLjMtMi43Mi0uODktLjk4LTEuNDItLjk4LTIuNDcuMzItMS45Ljk1LTIuNDkgMS41NS0uODkgMi43Ni0uODkgMi4xLjMgMi43Mi44OWMuNjEuNTkuOTIgMS40Mi45MiAyLjQ5cy0uMzIgMS44OC0uOTYgMi40N3ptLjI0LTguNjJoLTUuOGwtLjktMTYuNjNoNy41OWwtLjkgMTYuNjN6IiBmaWxsPSIjYzQ0Ii8+PC9zdmc+'); --image-executables: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDFjLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjRzMTAuNzUgMjQgMjQgMjQgMjQtMTAuNzUgMjQtMjQtMTAuNzUtMjQtMjQtMjR6bS0xMi45NCAzMy45NSA3LjM5LTkuOTUtNy4zOS05Ljk1IDQuMDEtMi45OCA5LjYxIDEyLjkzLTkuNjEgMTIuOTN6bTI0Ljg4IDMuODNoLTEyLjV2LTVoMTIuNXoiIGZpbGw9IiMyZjJmMmYiLz48L3N2Zz4='); + --image-fork: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQzLjUgMTEuODdjMC0zLjU5LTIuOTEtNi41LTYuNS02LjVzLTYuNSAyLjkxLTYuNSA2LjVjMCAyLjQgMS4zMSA0LjUgMy4yNSA1LjYydjExLjM3bC0yMC4xNiA1Ljg1IDEuODEgNi4yNCAyNC44NS03LjIxdi0xNi4yNWMxLjk0LTEuMTIgMy4yNS0zLjIyIDMuMjUtNS42MnoiIGZpbGw9IiMyYjJiMmIiLz48cGF0aCBkPSJtMjIuNSAzNy45M2MwLTMuMjYtMS45NS02LjA2LTQuNzUtNy4zMXYtMjkuNjJoLTYuNXYyOS42M2MtMi44IDEuMjUtNC43NSA0LjA1LTQuNzUgNy4zMXMxLjk1IDYuMDYgNC43NSA3LjMxdjMuNzZoNi41di0zLjc2YzIuOC0xLjI1IDQuNzUtNC4wNSA0Ljc1LTcuMzF6IiBmaWxsPSIjNmI2YjZiIi8+PC9zdmc+'); --image-ghcta-header: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDUwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Im0yNSAxLjU5Yy0xMy4yNSAwLTI0IDEwLjc1LTI0IDI0IDAgMTAuNiA2Ljg4IDE5LjYgMTYuNDEgMjIuNzcgMS4yLjIyIDEuNjQtLjUyIDEuNjQtMS4xNiAwLS41Ny0uMDItMi4wOC0uMDMtNC4wOC02LjY4IDEuNDUtOC4wOC0zLjIyLTguMDgtMy4yMi0xLjA5LTIuNzctMi42Ni0zLjUxLTIuNjYtMy41MS0yLjE4LTEuNDkuMTYtMS40Ni4xNi0xLjQ2IDIuNDEuMTcgMy42OCAyLjQ3IDMuNjggMi40NyAyLjE0IDMuNjcgNS42MiAyLjYxIDYuOTggMS45OS4yMi0xLjU1Ljg0LTIuNjEgMS41Mi0zLjIxLTUuMzMtLjYxLTEwLjkzLTIuNjYtMTAuOTMtMTEuODYgMC0yLjYyLjk0LTQuNzYgMi40Ny02LjQ0LS4yNS0uNjEtMS4wNy0zLjA1LjI0LTYuMzUgMCAwIDIuMDItLjY1IDYuNiAyLjQ2IDEuOTEtLjUzIDMuOTctLjggNi4wMS0uODEgMi4wNCAwIDQuMDkuMjggNi4wMS44MSA0LjU4LTMuMTEgNi41OS0yLjQ2IDYuNTktMi40NiAxLjMxIDMuMy40OSA1Ljc0LjI0IDYuMzUgMS41NCAxLjY4IDIuNDcgMy44MiAyLjQ3IDYuNDQgMCA5LjIyLTUuNjEgMTEuMjUtMTAuOTYgMTEuODQuODYuNzQgMS42MyAyLjIxIDEuNjMgNC40NCAwIDMuMjEtLjAzIDUuOC0uMDMgNi41OCAwIC42NC40MyAxLjM5IDEuNjUgMS4xNSA5LjUzLTMuMTggMTYuNC0xMi4xNyAxNi40LTIyLjc3IDAtMTMuMjYtMTAuNzUtMjQtMjQtMjR6IiBmaWxsPSIjMmYyZjJmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz48Y2lyY2xlIGN4PSI5NSIgY3k9IjI1IiBmaWxsPSIjMmYyZjJmIiByPSIyNCIvPjxwYXRoIGQ9Im0xMDIuNjIgMTMuNTFjLTMuMTctLjItNS45OSAxLjMzLTcuNjIgMy43My0xLjY0LTIuNC00LjQ2LTMuOTMtNy42Mi0zLjczLTQuODEuMy04LjM0IDQuNzYtNy45NyA5LjU2LjE5IDIuNDcgMS41NCA3LjIzIDExLjE5IDEzLjQ3bDQuNCAyLjg1IDQuNC0yLjg1YzkuNjQtNi4yMyAxMS0xMSAxMS4xOS0xMy40Ny4zNy00LjgtMy4xNi05LjI2LTcuOTctOS41NnoiIGZpbGw9IiNmNDRlYjIiLz48cGF0aCBkPSJtNTQuNDMgMjMuN2g0LjI2di0zLjk1aDIuNjF2My45NWg0LjI3djIuNjJoLTQuMjd2My45NGgtMi42MXYtMy45NGgtNC4yNnoiIGZpbGw9IiMyZjJmMmYiLz48L3N2Zz4='); --image-github: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDEuNTljLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjQgMCAxMC42IDYuODggMTkuNiAxNi40MSAyMi43NyAxLjIuMjIgMS42NC0uNTIgMS42NC0xLjE2IDAtLjU3LS4wMi0yLjA4LS4wMy00LjA4LTYuNjggMS40NS04LjA4LTMuMjItOC4wOC0zLjIyLTEuMDktMi43Ny0yLjY2LTMuNTEtMi42Ni0zLjUxLTIuMTgtMS40OS4xNi0xLjQ2LjE2LTEuNDYgMi40MS4xNyAzLjY4IDIuNDcgMy42OCAyLjQ3IDIuMTQgMy42NyA1LjYyIDIuNjEgNi45OCAxLjk5LjIyLTEuNTUuODQtMi42MSAxLjUyLTMuMjEtNS4zMy0uNjEtMTAuOTMtMi42Ni0xMC45My0xMS44NiAwLTIuNjIuOTQtNC43NiAyLjQ3LTYuNDQtLjI1LS42MS0xLjA3LTMuMDUuMjQtNi4zNSAwIDAgMi4wMi0uNjUgNi42IDIuNDYgMS45MS0uNTMgMy45Ny0uOCA2LjAxLS44MSAyLjA0IDAgNC4wOS4yOCA2LjAxLjgxIDQuNTgtMy4xMSA2LjU5LTIuNDYgNi41OS0yLjQ2IDEuMzEgMy4zLjQ5IDUuNzQuMjQgNi4zNSAxLjU0IDEuNjggMi40NyAzLjgyIDIuNDcgNi40NCAwIDkuMjItNS42MSAxMS4yNS0xMC45NiAxMS44NC44Ni43NCAxLjYzIDIuMjEgMS42MyA0LjQ0IDAgMy4yMS0uMDMgNS44LS4wMyA2LjU4IDAgLjY0LjQzIDEuMzkgMS42NSAxLjE1IDkuNTMtMy4xOCAxNi40LTEyLjE3IDE2LjQtMjIuNzcgMC0xMy4yNi0xMC43NS0yNC0yNC0yNHoiIGZpbGw9IiMyZjJmMmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg=='); --image-heart: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTM2LjgyIDQuOTZjLTQuOTEtLjMxLTkuMjggMi4wNi0xMS44MiA1Ljc4LTIuNTQtMy43Mi02LjkxLTYuMDktMTEuODItNS43OC03LjQ1LjQ2LTEyLjkzIDcuMzctMTIuMzUgMTQuODIuMjkgMy44MyAyLjM5IDExLjIxIDE3LjM1IDIwLjg4bDYuODMgNC40MSA2LjgzLTQuNDFjMTQuOTUtOS42NiAxNy4wNS0xNy4wNSAxNy4zNS0yMC44OC41Ny03LjQ0LTQuOS0xNC4zNS0xMi4zNS0xNC44MnoiIGZpbGw9IiNmNDRlYjIiLz48L3N2Zz4='); @@ -73,6 +74,7 @@ --image-download: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iI2YxZjFmMSI+PHBhdGggZD0ibTM3LjkzIDI1LjAxLTIuOTgtNC4wMS03LjQ1IDUuNTN2LTIzLjA5aC01djIzLjA5bC03LjQ1LTUuNTMtMi45OCA0LjAxIDEyLjkzIDkuNjF6Ii8+PHBhdGggZD0ibTQyLjMxIDMzLjA5djguNDdoLTM0LjYydi04LjQ3aC01djEzLjQ3aDQ0LjYydi0xMy40N3oiLz48L2c+PC9zdmc+'); --image-error: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDFjLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjRzMTAuNzUgMjQgMjQgMjQgMjQtMTAuNzUgMjQtMjQtMTAuNzUtMjQtMjQtMjR6bTIuNjYgMzYuMThjLS42NC41OS0xLjUzLjg5LTIuNjguODlzLTIuMDctLjMtMi43Mi0uODktLjk4LTEuNDItLjk4LTIuNDcuMzItMS45Ljk1LTIuNDkgMS41NS0uODkgMi43Ni0uODkgMi4xLjMgMi43Mi44OWMuNjEuNTkuOTIgMS40Mi45MiAyLjQ5cy0uMzIgMS44OC0uOTYgMi40N3ptLjI0LTguNjJoLTUuOGwtLjktMTYuNjNoNy41OWwtLjkgMTYuNjN6IiBmaWxsPSIjZmY0MzQzIi8+PC9zdmc+'); --image-executables: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDFjLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjRzMTAuNzUgMjQgMjQgMjQgMjQtMTAuNzUgMjQtMjQtMTAuNzUtMjQtMjQtMjR6bS0xMi45NCAzMy45NSA3LjM5LTkuOTUtNy4zOS05Ljk1IDQuMDEtMi45OCA5LjYxIDEyLjkzLTkuNjEgMTIuOTN6bTI0Ljg4IDMuODNoLTEyLjV2LTVoMTIuNXoiIGZpbGw9IiNmMWYxZjEiLz48L3N2Zz4='); + --image-fork: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQzLjUgMTEuODdjMC0zLjU5LTIuOTEtNi41LTYuNS02LjVzLTYuNSAyLjkxLTYuNSA2LjVjMCAyLjQgMS4zMSA0LjUgMy4yNSA1LjYydjExLjM3bC0yMC4xNiA1Ljg1IDEuODEgNi4yNCAyNC44NS03LjIxdi0xNi4yNWMxLjk0LTEuMTIgMy4yNS0zLjIyIDMuMjUtNS42MnoiIGZpbGw9IiNmMWYxZjEiLz48cGF0aCBkPSJtMjIuNSAzNy45M2MwLTMuMjYtMS45NS02LjA2LTQuNzUtNy4zMXYtMjkuNjJoLTYuNXYyOS42M2MtMi44IDEuMjUtNC43NSA0LjA1LTQuNzUgNy4zMXMxLjk1IDYuMDYgNC43NSA3LjMxdjMuNzZoNi41di0zLjc2YzIuOC0xLjI1IDQuNzUtNC4wNSA0Ljc1LTcuMzF6IiBmaWxsPSIjYjFiMWIxIi8+PC9zdmc+'); --image-ghcta-header: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDUwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Im0yNSAxLjU5Yy0xMy4yNSAwLTI0IDEwLjc1LTI0IDI0IDAgMTAuNiA2Ljg4IDE5LjYgMTYuNDEgMjIuNzcgMS4yLjIyIDEuNjQtLjUyIDEuNjQtMS4xNiAwLS41Ny0uMDItMi4wOC0uMDMtNC4wOC02LjY4IDEuNDUtOC4wOC0zLjIyLTguMDgtMy4yMi0xLjA5LTIuNzctMi42Ni0zLjUxLTIuNjYtMy41MS0yLjE4LTEuNDkuMTYtMS40Ni4xNi0xLjQ2IDIuNDEuMTcgMy42OCAyLjQ3IDMuNjggMi40NyAyLjE0IDMuNjcgNS42MiAyLjYxIDYuOTggMS45OS4yMi0xLjU1Ljg0LTIuNjEgMS41Mi0zLjIxLTUuMzMtLjYxLTEwLjkzLTIuNjYtMTAuOTMtMTEuODYgMC0yLjYyLjk0LTQuNzYgMi40Ny02LjQ0LS4yNS0uNjEtMS4wNy0zLjA1LjI0LTYuMzUgMCAwIDIuMDItLjY1IDYuNiAyLjQ2IDEuOTEtLjUzIDMuOTctLjggNi4wMS0uODEgMi4wNCAwIDQuMDkuMjggNi4wMS44MSA0LjU4LTMuMTEgNi41OS0yLjQ2IDYuNTktMi40NiAxLjMxIDMuMy40OSA1Ljc0LjI0IDYuMzUgMS41NCAxLjY4IDIuNDcgMy44MiAyLjQ3IDYuNDQgMCA5LjIyLTUuNjEgMTEuMjUtMTAuOTYgMTEuODQuODYuNzQgMS42MyAyLjIxIDEuNjMgNC40NCAwIDMuMjEtLjAzIDUuOC0uMDMgNi41OCAwIC42NC40MyAxLjM5IDEuNjUgMS4xNSA5LjUzLTMuMTggMTYuNC0xMi4xNyAxNi40LTIyLjc3IDAtMTMuMjYtMTAuNzUtMjQtMjQtMjR6IiBmaWxsPSIjZjFmMWYxIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz48Y2lyY2xlIGN4PSI5NSIgY3k9IjI1IiBmaWxsPSIjZjFmMWYxIiByPSIyNCIvPjxwYXRoIGQ9Im0xMDIuNjIgMTMuNTFjLTMuMTctLjItNS45OSAxLjMzLTcuNjIgMy43My0xLjY0LTIuNC00LjQ2LTMuOTMtNy42Mi0zLjczLTQuODEuMy04LjM0IDQuNzYtNy45NyA5LjU2LjE5IDIuNDcgMS41NCA3LjIzIDExLjE5IDEzLjQ3bDQuNCAyLjg1IDQuNC0yLjg1YzkuNjQtNi4yMyAxMS0xMSAxMS4xOS0xMy40Ny4zNy00LjgtMy4xNi05LjI2LTcuOTctOS41NnoiIGZpbGw9IiNmNDRlYjIiLz48cGF0aCBkPSJtNTQuNDMgMjMuN2g0LjI2di0zLjk1aDIuNjF2My45NWg0LjI3djIuNjJoLTQuMjd2My45NGgtMi42MXYtMy45NGgtNC4yNnoiIGZpbGw9IiNmMWYxZjEiLz48L3N2Zz4='); --image-github: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTI1IDEuNTljLTEzLjI1IDAtMjQgMTAuNzUtMjQgMjQgMCAxMC42IDYuODggMTkuNiAxNi40MSAyMi43NyAxLjIuMjIgMS42NC0uNTIgMS42NC0xLjE2IDAtLjU3LS4wMi0yLjA4LS4wMy00LjA4LTYuNjggMS40NS04LjA4LTMuMjItOC4wOC0zLjIyLTEuMDktMi43Ny0yLjY2LTMuNTEtMi42Ni0zLjUxLTIuMTgtMS40OS4xNi0xLjQ2LjE2LTEuNDYgMi40MS4xNyAzLjY4IDIuNDcgMy42OCAyLjQ3IDIuMTQgMy42NyA1LjYyIDIuNjEgNi45OCAxLjk5LjIyLTEuNTUuODQtMi42MSAxLjUyLTMuMjEtNS4zMy0uNjEtMTAuOTMtMi42Ni0xMC45My0xMS44NiAwLTIuNjIuOTQtNC43NiAyLjQ3LTYuNDQtLjI1LS42MS0xLjA3LTMuMDUuMjQtNi4zNSAwIDAgMi4wMi0uNjUgNi42IDIuNDYgMS45MS0uNTMgMy45Ny0uOCA2LjAxLS44MSAyLjA0IDAgNC4wOS4yOCA2LjAxLjgxIDQuNTgtMy4xMSA2LjU5LTIuNDYgNi41OS0yLjQ2IDEuMzEgMy4zLjQ5IDUuNzQuMjQgNi4zNSAxLjU0IDEuNjggMi40NyAzLjgyIDIuNDcgNi40NCAwIDkuMjItNS42MSAxMS4yNS0xMC45NiAxMS44NC44Ni43NCAxLjYzIDIuMjEgMS42MyA0LjQ0IDAgMy4yMS0uMDMgNS44LS4wMyA2LjU4IDAgLjY0LjQzIDEuMzkgMS42NSAxLjE1IDkuNTMtMy4xOCAxNi40LTEyLjE3IDE2LjQtMjIuNzcgMC0xMy4yNi0xMC43NS0yNC0yNC0yNHoiIGZpbGw9IiNmMWYxZjEiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg=='); --image-heart: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTAgNTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTM2LjgyIDQuOTZjLTQuOTEtLjMxLTkuMjggMi4wNi0xMS44MiA1Ljc4LTIuNTQtMy43Mi02LjkxLTYuMDktMTEuODItNS43OC03LjQ1LjQ2LTEyLjkzIDcuMzctMTIuMzUgMTQuODIuMjkgMy44MyAyLjM5IDExLjIxIDE3LjM1IDIwLjg4bDYuODMgNC40MSA2LjgzLTQuNDFjMTQuOTUtOS42NiAxNy4wNS0xNy4wNSAxNy4zNS0yMC44OC41Ny03LjQ0LTQuOS0xNC4zNS0xMi4zNS0xNC44MnoiIGZpbGw9IiNmNDRlYjIiLz48L3N2Zz4='); diff --git a/FrontEnd/styles/package.scss b/FrontEnd/styles/package.scss index b32d43b54..28ee28c6b 100644 --- a/FrontEnd/styles/package.scss +++ b/FrontEnd/styles/package.scss @@ -75,6 +75,11 @@ background-image: var(--image-warning); } + li.forked { + grid-column-start: span 2; + background-image: var(--image-fork); + } + li.authors { grid-column-start: span 2; background-image: var(--image-authors); diff --git a/Resources/SVGs/fork~dark.svg b/Resources/SVGs/fork~dark.svg new file mode 100644 index 000000000..c4ba52a15 --- /dev/null +++ b/Resources/SVGs/fork~dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/SVGs/fork~light.svg b/Resources/SVGs/fork~light.svg new file mode 100644 index 000000000..534c94039 --- /dev/null +++ b/Resources/SVGs/fork~light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Sources/App/Controllers/API/API+PackageController+GetRoute+Model.swift b/Sources/App/Controllers/API/API+PackageController+GetRoute+Model.swift index 211e5fb17..f24c59228 100644 --- a/Sources/App/Controllers/API/API+PackageController+GetRoute+Model.swift +++ b/Sources/App/Controllers/API/API+PackageController+GetRoute+Model.swift @@ -50,6 +50,7 @@ extension API.PackageController.GetRoute { var releaseReferences: [App.Version.Kind: App.Reference] var fundingLinks: [FundingLink] var swift6Readiness: Swift6Readiness? + var forkedFromInfo: ForkedFromInfo? internal init(packageId: Package.Id, repositoryOwner: String, @@ -81,7 +82,8 @@ extension API.PackageController.GetRoute { releaseReference: App.Reference?, preReleaseReference: App.Reference?, fundingLinks: [FundingLink] = [], - swift6Readiness: Swift6Readiness? + swift6Readiness: Swift6Readiness?, + forkedFromInfo: ForkedFromInfo? ) { self.packageId = packageId self.repositoryOwner = repositoryOwner @@ -123,6 +125,7 @@ extension API.PackageController.GetRoute { }() self.fundingLinks = fundingLinks self.swift6Readiness = swift6Readiness + self.forkedFromInfo = forkedFromInfo } init?(result: API.PackageController.PackageResult, @@ -132,7 +135,8 @@ extension API.PackageController.GetRoute { swiftVersionBuildInfo: BuildInfo?, platformBuildInfo: BuildInfo?, weightedKeywords: [WeightedKeyword] = [], - swift6Readiness: Swift6Readiness?) { + swift6Readiness: Swift6Readiness?, + forkedFromInfo: ForkedFromInfo?) { // we consider certain attributes as essential and return nil (raising .notFound) let repository = result.repository guard @@ -177,7 +181,8 @@ extension API.PackageController.GetRoute { releaseReference: result.releaseVersion?.reference, preReleaseReference: result.preReleaseVersion?.reference, fundingLinks: result.repository.fundingLinks, - swift6Readiness: swift6Readiness + swift6Readiness: swift6Readiness, + forkedFromInfo: forkedFromInfo ) } @@ -348,7 +353,14 @@ extension API.PackageController.GetRoute.Model { } } } - + + enum ForkedFromInfo: Codable, Equatable { + case fromSPI(originalOwner: String, + originalOwnerName: String, + originalRepo: String, + originalPackageName: String) + case fromGitHub(url: String) + } } diff --git a/Sources/App/Controllers/API/API+PackageController+GetRoute.swift b/Sources/App/Controllers/API/API+PackageController+GetRoute.swift index 1a7d6750a..782967b6c 100644 --- a/Sources/App/Controllers/API/API+PackageController+GetRoute.swift +++ b/Sources/App/Controllers/API/API+PackageController+GetRoute.swift @@ -45,6 +45,7 @@ extension API.PackageController { async let buildInfo = API.PackageController.BuildInfo.query(on: database, owner: owner, repository: repository) + async let forkedFromInfo = forkedFromInfo(on: database, fork: packageResult.repository.forkedFrom) guard let model = try await Self.Model( @@ -55,7 +56,8 @@ extension API.PackageController { swiftVersionBuildInfo: buildInfo.swiftVersion, platformBuildInfo: buildInfo.platform, weightedKeywords: weightedKeywords, - swift6Readiness: buildInfo.swift6Readiness + swift6Readiness: buildInfo.swift6Readiness, + forkedFromInfo: forkedFromInfo ), let schema = API.PackageSchema(result: packageResult) else { @@ -84,4 +86,34 @@ extension API.PackageController.GetRoute { beta: links[1], latest: links[2]) } + + static func forkedFromInfo(on database: Database, fork: Fork?) async -> Model.ForkedFromInfo? { + guard let forkedFrom = fork else { return nil } + switch forkedFrom { + case .parentId(let id, let fallbackURL): + return await Model.ForkedFromInfo.query(on: database, packageId: id, fallbackURL: fallbackURL) + case let .parentURL(url): + return .fromGitHub(url: url) + } + } +} + + +extension API.PackageController.GetRoute.Model.ForkedFromInfo { + static func query(on database: Database, packageId: Package.Id, fallbackURL: String) async -> Self? { + let model = try? await Joined3 + .query(on: database, packageId: packageId, version: .defaultBranch) + .first() + + guard let repoName = model?.repository.name, + let ownerName = model?.repository.ownerName, + let owner = model?.repository.owner else { + return .fromGitHub(url: fallbackURL) + } + + return .fromSPI(originalOwner: owner, + originalOwnerName: ownerName, + originalRepo: repoName, + originalPackageName: model?.version.packageName ?? repoName) + } } diff --git a/Sources/App/Controllers/API/Types+WithExample.swift b/Sources/App/Controllers/API/Types+WithExample.swift index 1741e5dec..a15170f81 100644 --- a/Sources/App/Controllers/API/Types+WithExample.swift +++ b/Sources/App/Controllers/API/Types+WithExample.swift @@ -247,7 +247,8 @@ extension API.PackageController.GetRoute.Model: WithExample { defaultBranchReference: .branch("main"), releaseReference: .tag(1, 2, 3, "1.2.3"), preReleaseReference: nil, - swift6Readiness: nil) + swift6Readiness: nil, + forkedFromInfo: nil) } } diff --git a/Sources/App/Core/Query+Support/Joined3+Package.swift b/Sources/App/Core/Query+Support/Joined3+Package.swift index 1b9d11a36..9c67e3bac 100644 --- a/Sources/App/Core/Query+Support/Joined3+Package.swift +++ b/Sources/App/Core/Query+Support/Joined3+Package.swift @@ -40,4 +40,9 @@ extension Joined3 where M == Package, R1 == Repository, R2 == Version { .filter(Repository.self, \.$owner, .custom("ilike"), owner) .filter(Repository.self, \.$name, .custom("ilike"), repository) } + + static func query(on database: Database, packageId: Package.Id, version: Version.Kind) -> JoinedQueryBuilder { + query(on: database, version: version) + .filter(Package.self, \Package.$id == packageId) + } } diff --git a/Sources/App/Models/Repository.swift b/Sources/App/Models/Repository.swift index 22338a08d..1b252f83b 100644 --- a/Sources/App/Models/Repository.swift +++ b/Sources/App/Models/Repository.swift @@ -224,6 +224,7 @@ final class Repository: @unchecked Sendable, Model, Content { .filter(\.$package.$id == pkgId) .first() ?? Repository(packageId: pkgId) } + } diff --git a/Sources/App/Views/PackageController/GetRoute.Model+ext.swift b/Sources/App/Views/PackageController/GetRoute.Model+ext.swift index d4e77d1ef..b647d1696 100644 --- a/Sources/App/Views/PackageController/GetRoute.Model+ext.swift +++ b/Sources/App/Views/PackageController/GetRoute.Model+ext.swift @@ -181,6 +181,50 @@ extension API.PackageController.GetRoute.Model { return .empty } } + + func forkedListItem() -> Node { + if let forkedFromInfo { + let item: Node = { + switch forkedFromInfo { + case .fromGitHub(let url): + var text = url.replacingOccurrences(of: "https://github.com/", with: "") + text = text.removingSuffix(".git") + let repoURLNode: Node = .a( + .href(url), + .text(text) + ) + return .group( + .text("Forked from "), + repoURLNode, + .text(".") + ) + case .fromSPI(_, let ownerName, _, let originalPackageName): + let repoURLNode: Node = .a( + .href(forkedFromInfo.url), + .text("\(originalPackageName)") + ) + let ownerNode: Node = .a( + .href(forkedFromInfo.ownerURL ?? ""), + .text("\(ownerName)") + ) + return .group( + .text("Forked from "), + repoURLNode, + .text(" by "), + ownerNode, + .text(".") + ) + } + }() + + return .li( + .class("forked"), + item + ) + } else { + return .empty + } + } func binaryTargetsItem() -> Node { guard hasBinaryTargets else { return .empty } @@ -667,3 +711,24 @@ extension API.PackageController.GetRoute.Model.Swift6Readiness { return lines.joined(separator: "\n") } } + + +extension API.PackageController.GetRoute.Model.ForkedFromInfo { + var url: String { + switch self { + case .fromSPI(let originalOwner, _, let originalRepo, _): + return SiteURL.package(.value(originalOwner), .value(originalRepo), nil).relativeURL() + case .fromGitHub(let url): + return url + } + } + + var ownerURL: String? { + switch self { + case .fromSPI(let owner, _, _, _): + return SiteURL.author(.value(owner)).relativeURL() + case .fromGitHub: + return nil + } + } +} diff --git a/Sources/App/Views/PackageController/PackageShow+View.swift b/Sources/App/Views/PackageController/PackageShow+View.swift index 37f4a4d84..271e68543 100644 --- a/Sources/App/Views/PackageController/PackageShow+View.swift +++ b/Sources/App/Views/PackageController/PackageShow+View.swift @@ -167,6 +167,7 @@ extension PackageShow { .ul( .class("main-metadata"), model.archivedListItem(), + model.forkedListItem(), model.authorsListItem(), model.binaryTargetsItem(), model.historyListItem(), diff --git a/Tests/AppTests/API+PackageController+GetRoute+ModelTests.swift b/Tests/AppTests/API+PackageController+GetRoute+ModelTests.swift index bff7618a4..f5d242a31 100644 --- a/Tests/AppTests/API+PackageController+GetRoute+ModelTests.swift +++ b/Tests/AppTests/API+PackageController+GetRoute+ModelTests.swift @@ -42,7 +42,8 @@ class API_PackageController_GetRoute_ModelTests: SnapshotTestCase { swiftVersionBuildInfo: nil, platformBuildInfo: nil, weightedKeywords: [], - swift6Readiness: nil) + swift6Readiness: nil, + forkedFromInfo: nil) // validate XCTAssertNotNil(m) @@ -64,7 +65,8 @@ class API_PackageController_GetRoute_ModelTests: SnapshotTestCase { swiftVersionBuildInfo: nil, platformBuildInfo: nil, weightedKeywords: [], - swift6Readiness: nil)) + swift6Readiness: nil, + forkedFromInfo: nil)) // validate XCTAssertEqual(model.packageIdentity, "swift-bar") @@ -86,7 +88,8 @@ class API_PackageController_GetRoute_ModelTests: SnapshotTestCase { swiftVersionBuildInfo: nil, platformBuildInfo: nil, weightedKeywords: [], - swift6Readiness: nil)) + swift6Readiness: nil, + forkedFromInfo: nil)) // validate XCTAssertEqual(model.documentationTarget, .internal(docVersion: .reference("main"), archive: "archive1")) @@ -112,11 +115,40 @@ class API_PackageController_GetRoute_ModelTests: SnapshotTestCase { swiftVersionBuildInfo: nil, platformBuildInfo: nil, weightedKeywords: [], - swift6Readiness: nil)) + swift6Readiness: nil, + forkedFromInfo: nil)) // validate XCTAssertEqual(model.documentationTarget, .external(url: "https://example.com/package/documentation")) } + + func test_ForkedFromInfo_query() async throws { + let originalPkg = try await savePackage(on: app.db, id: .id0, "https://github.com/original/original") + try await Repository(package: originalPkg, + name: "original", + owner: "original", + ownerName: "OriginalOwner").save(on: app.db) + try await App.Version(package: originalPkg, latest: .defaultBranch, packageName: "OriginalPkg", reference: .branch("main")) + .save(on: app.db) + + // MUT + let forkedFrom = await API.PackageController.GetRoute.Model.ForkedFromInfo.query(on: app.db, packageId: .id0, fallbackURL: "https://github.com/original/original.git") + + // validate + XCTAssertEqual(forkedFrom, .fromSPI(originalOwner: "original", + originalOwnerName: "OriginalOwner", + originalRepo: "original", + originalPackageName: "OriginalPkg")) + } + + func test_ForkedFromInfo_query_fallback() async throws { + // when the package can't be found resort to fallback URL + // MUT + let forkedFrom = await API.PackageController.GetRoute.Model.ForkedFromInfo.query(on: app.db, packageId: .id0, fallbackURL: "https://github.com/original/original.git") + + // validate + XCTAssertEqual(forkedFrom, .fromGitHub(url: "https://github.com/original/original.git")) + } func test_gitHubOwnerUrl() throws { var model = API.PackageController.GetRoute.Model.mock @@ -144,6 +176,37 @@ class API_PackageController_GetRoute_ModelTests: SnapshotTestCase { let renderedHistory = model.historyListItem().render(indentedBy: .spaces(2)) assertSnapshot(of: renderedHistory, as: .lines) } + + func test_forked_from_github() throws { + var model = API.PackageController.GetRoute.Model.mock + model.forkedFromInfo = .fromGitHub(url: "https://github.com/owner/repository.git") + let renderedForkedFrom = model.forkedListItem().render(indentedBy: .spaces(2)) + assertSnapshot(of: renderedForkedFrom, as: .lines) + } + + func test_forked_from_spi_same_package_name() throws { + var model = API.PackageController.GetRoute.Model.mock + model.forkedFromInfo = .fromSPI( + originalOwner: "owner", + originalOwnerName: "OriginalOwner", + originalRepo: "repo", + originalPackageName: "Test" + ) + let renderedForkedFrom = model.forkedListItem().render(indentedBy: .spaces(2)) + assertSnapshot(of: renderedForkedFrom, as: .lines) + } + + func test_forked_from_spi_different_package_name() throws { + var model = API.PackageController.GetRoute.Model.mock + model.forkedFromInfo = .fromSPI( + originalOwner: "owner", + originalOwnerName: "OriginalOwner", + originalRepo: "repo", + originalPackageName: "Different" + ) + let renderedForkedFrom = model.forkedListItem().render(indentedBy: .spaces(2)) + assertSnapshot(of: renderedForkedFrom, as: .lines) + } func test_binary_targets() throws { var model = API.PackageController.GetRoute.Model.mock @@ -329,6 +392,41 @@ class API_PackageController_GetRoute_ModelTests: SnapshotTestCase { .fromSPIManifest("By Author One, Author Two, and more!") XCTAssertEqual(model.authorsListItem().render(), "
  • By Author One, Author Two, and more!
  • ") } + + func test_forkedFrom_github_formatting() throws { + var model = API.PackageController.GetRoute.Model.mock + model.forkedFromInfo = .fromGitHub(url: "https://github.com/owner/repository.git") + let renderedForkedFrom = model.forkedListItem().render() + XCTAssertEqual(renderedForkedFrom, "
  • Forked from owner/repository.
  • ") + } + + func test_forkedFrom_spi_same_package_name_formatting() throws { + var model = API.PackageController.GetRoute.Model.mock + model.forkedFromInfo = .fromSPI( + originalOwner: "owner", + originalOwnerName: "OriginalOwner", + originalRepo: "repo", + originalPackageName: "Test" + ) + let url = SiteURL.package(.value("owner"), .value("repo"), nil).relativeURL() + let ownerUrl = model.forkedFromInfo?.ownerURL ?? "" + let renderedForkedFrom = model.forkedListItem().render() + XCTAssertEqual(renderedForkedFrom, "
  • Forked from Test by OriginalOwner.
  • ") + } + + func test_forkedFrom_spi_different_package_name_formatting() throws { + var model = API.PackageController.GetRoute.Model.mock + model.forkedFromInfo = .fromSPI( + originalOwner: "owner", + originalOwnerName: "OriginalOwner", + originalRepo: "repo", + originalPackageName: "Different" + ) + let url = SiteURL.package(.value("owner"), .value("repo"), nil).relativeURL() + let ownerUrl = model.forkedFromInfo?.ownerURL ?? "" + let renderedForkedFrom = model.forkedListItem().render() + XCTAssertEqual(renderedForkedFrom, "
  • Forked from Different by OriginalOwner.
  • ") + } func test_BuildInfo_init() throws { // ensure nil propagation when all versions' values are nil diff --git a/Tests/AppTests/Mocks/API.PackageController.GetRoute.Model+mock.swift b/Tests/AppTests/Mocks/API.PackageController.GetRoute.Model+mock.swift index cafa1c83d..31da3f38e 100644 --- a/Tests/AppTests/Mocks/API.PackageController.GetRoute.Model+mock.swift +++ b/Tests/AppTests/Mocks/API.PackageController.GetRoute.Model+mock.swift @@ -125,7 +125,8 @@ extension API.PackageController.GetRoute.Model { defaultBranchReference: .branch("main"), releaseReference: .tag(5, 2, 0), preReleaseReference: .tag(5, 3, 0, "beta.1"), - swift6Readiness: nil + swift6Readiness: nil, + forkedFromInfo: nil ) } } diff --git a/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from.1.txt b/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from.1.txt new file mode 100644 index 000000000..620ce3bbe --- /dev/null +++ b/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from.1.txt @@ -0,0 +1,3 @@ +
  • Forked from + repository. +
  • \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from_github.1.txt b/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from_github.1.txt new file mode 100644 index 000000000..be633d8e1 --- /dev/null +++ b/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from_github.1.txt @@ -0,0 +1,3 @@ +
  • Forked from + owner/repository. +
  • \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from_spi_different_package_name.1.txt b/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from_spi_different_package_name.1.txt new file mode 100644 index 000000000..c97d0da5a --- /dev/null +++ b/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from_spi_different_package_name.1.txt @@ -0,0 +1,4 @@ +
  • Forked from + Different by + OriginalOwner. +
  • \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from_spi_same_package_name.1.txt b/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from_spi_same_package_name.1.txt new file mode 100644 index 000000000..eb55eff80 --- /dev/null +++ b/Tests/AppTests/__Snapshots__/API+PackageController+GetRoute+ModelTests/test_forked_from_spi_same_package_name.1.txt @@ -0,0 +1,4 @@ +
  • Forked from + Test by + OriginalOwner. +
  • \ No newline at end of file