From 5dda9510f48f6babb1a5c582e83c501b5eaa214e Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Wed, 4 Dec 2024 15:30:46 +0800 Subject: [PATCH 1/6] Fix gogit `GetRefCommitID` (#32705) --- modules/git/repo_commit_gogit.go | 11 +++++++++-- modules/git/repo_commit_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index 84580be9a5..993013eef7 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -14,9 +14,16 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" ) -// GetRefCommitID returns the last commit ID string of given reference (branch or tag). +// GetRefCommitID returns the last commit ID string of given reference. func (repo *Repository) GetRefCommitID(name string) (string, error) { - ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true) + if plumbing.IsHash(name) { + return name, nil + } + refName := plumbing.ReferenceName(name) + if err := refName.Validate(); err != nil { + return "", err + } + ref, err := repo.gogitRepo.Reference(refName, true) if err != nil { if err == plumbing.ErrReferenceNotFound { return "", ErrNotExist{ diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 19983b47b1..4c26fa2a48 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -101,3 +101,28 @@ func TestRepository_CommitsBetweenIDs(t *testing.T) { assert.Len(t, commits, c.ExpectedCommits, "case %d", i) } } + +func TestGetRefCommitID(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path) + assert.NoError(t, err) + defer bareRepo1.Close() + + // these test case are specific to the repo1_bare test repo + testCases := []struct { + Ref string + ExpectedCommitID string + }{ + {RefNameFromBranch("master").String(), "ce064814f4a0d337b333e646ece456cd39fab612"}, + {RefNameFromBranch("branch1").String(), "2839944139e0de9737a044f78b0e4b40d989a9e3"}, + {RefNameFromTag("test").String(), "3ad28a9149a2864384548f3d17ed7f38014c9e8a"}, + {"ce064814f4a0d337b333e646ece456cd39fab612", "ce064814f4a0d337b333e646ece456cd39fab612"}, + } + + for _, testCase := range testCases { + commitID, err := bareRepo1.GetRefCommitID(testCase.Ref) + if assert.NoError(t, err) { + assert.Equal(t, testCase.ExpectedCommitID, commitID) + } + } +} From e45ffc530f482a46de25d28f18b039f296750414 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 3 Dec 2024 23:59:28 -0800 Subject: [PATCH 2/6] Remove outdated code about fixture generation (#32708) --- contrib/fixtures/fixture_generation.go | 80 -------------------------- models/fixture_generation.go | 50 ---------------- models/fixture_test.go | 37 ------------ 3 files changed, 167 deletions(-) delete mode 100644 contrib/fixtures/fixture_generation.go delete mode 100644 models/fixture_generation.go delete mode 100644 models/fixture_test.go diff --git a/contrib/fixtures/fixture_generation.go b/contrib/fixtures/fixture_generation.go deleted file mode 100644 index 31797cc800..0000000000 --- a/contrib/fixtures/fixture_generation.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//nolint:forbidigo -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/models/unittest" -) - -// To generate derivative fixtures, execute the following from Gitea's repository base dir: -// go run -tags 'sqlite sqlite_unlock_notify' contrib/fixtures/fixture_generation.go [fixture...] - -var ( - generators = []struct { - gen func(ctx context.Context) (string, error) - name string - }{ - { - models.GetYamlFixturesAccess, "access", - }, - } - fixturesDir string -) - -func main() { - pathToGiteaRoot := "." - fixturesDir = filepath.Join(pathToGiteaRoot, "models", "fixtures") - if err := unittest.CreateTestEngine(unittest.FixturesOptions{ - Dir: fixturesDir, - }); err != nil { - fmt.Printf("CreateTestEngine: %+v", err) - os.Exit(1) - } - if err := unittest.PrepareTestDatabase(); err != nil { - fmt.Printf("PrepareTestDatabase: %+v\n", err) - os.Exit(1) - } - ctx := context.Background() - if len(os.Args) == 0 { - for _, r := range os.Args { - if err := generate(ctx, r); err != nil { - fmt.Printf("generate '%s': %+v\n", r, err) - os.Exit(1) - } - } - } else { - for _, g := range generators { - if err := generate(ctx, g.name); err != nil { - fmt.Printf("generate '%s': %+v\n", g.name, err) - os.Exit(1) - } - } - } -} - -func generate(ctx context.Context, name string) error { - for _, g := range generators { - if g.name == name { - data, err := g.gen(ctx) - if err != nil { - return err - } - path := filepath.Join(fixturesDir, name+".yml") - if err := os.WriteFile(path, []byte(data), 0o644); err != nil { - return fmt.Errorf("%s: %+v", path, err) - } - fmt.Printf("%s created.\n", path) - return nil - } - } - - return fmt.Errorf("generator not found") -} diff --git a/models/fixture_generation.go b/models/fixture_generation.go deleted file mode 100644 index 6234caefad..0000000000 --- a/models/fixture_generation.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/models/db" - access_model "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" -) - -// GetYamlFixturesAccess returns a string containing the contents -// for the access table, as recalculated using repo.RecalculateAccesses() -func GetYamlFixturesAccess(ctx context.Context) (string, error) { - repos := make([]*repo_model.Repository, 0, 50) - if err := db.GetEngine(ctx).Find(&repos); err != nil { - return "", err - } - - for _, repo := range repos { - repo.MustOwner(ctx) - if err := access_model.RecalculateAccesses(ctx, repo); err != nil { - return "", err - } - } - - var b strings.Builder - - accesses := make([]*access_model.Access, 0, 200) - if err := db.GetEngine(ctx).OrderBy("user_id, repo_id").Find(&accesses); err != nil { - return "", err - } - - for i, a := range accesses { - fmt.Fprintf(&b, "-\n") - fmt.Fprintf(&b, " id: %d\n", i+1) - fmt.Fprintf(&b, " user_id: %d\n", a.UserID) - fmt.Fprintf(&b, " repo_id: %d\n", a.RepoID) - fmt.Fprintf(&b, " mode: %d\n", a.Mode) - if i < len(accesses)-1 { - fmt.Fprintf(&b, "\n") - } - } - - return b.String(), nil -} diff --git a/models/fixture_test.go b/models/fixture_test.go deleted file mode 100644 index de5f412388..0000000000 --- a/models/fixture_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - "os" - "path/filepath" - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/util" - - "github.com/stretchr/testify/assert" -) - -func TestFixtureGeneration(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - test := func(ctx context.Context, gen func(ctx context.Context) (string, error), name string) { - expected, err := gen(ctx) - if !assert.NoError(t, err) { - return - } - p := filepath.Join(unittest.FixturesDir(), name+".yml") - bytes, err := os.ReadFile(p) - if !assert.NoError(t, err) { - return - } - data := string(util.NormalizeEOL(bytes)) - assert.EqualValues(t, expected, data, "Differences detected for %s", p) - } - - test(db.DefaultContext, GetYamlFixturesAccess, "access") -} From 838653d1dfa0fbc6313b0bba682075baba385a5e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 4 Dec 2024 17:26:54 +0800 Subject: [PATCH 3/6] Fix file editor & preview (#32706) Fix a regression caused by jQuery removal (`renderPreviewPanelContent`) And simplify the file editor, it doesn't need to be that complex. And remove jQuery code. --- templates/repo/editor/edit.tmpl | 28 ++------ templates/repo/editor/patch.tmpl | 25 ++------ web_src/js/features/codeeditor.ts | 6 +- web_src/js/features/comp/ConfirmModal.ts | 2 +- web_src/js/features/repo-editor.ts | 81 +++++++++++------------- web_src/js/vendor/jquery.are-you-sure.ts | 4 +- 6 files changed, 52 insertions(+), 94 deletions(-) diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index c556826827..204a426970 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -3,7 +3,10 @@ {{template "repo/header" .}}
{{template "base/alert" .}} -
+ {{.CsrfTokenHtml}} @@ -29,7 +32,7 @@
{{template "base/footer" .}} diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index ee515e25e2..93b2042fa9 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -134,19 +134,17 @@ function getFileBasedOptions(filename: string, lineWrapExts: string[]) { } function togglePreviewDisplay(previewable: boolean) { - const previewTab = document.querySelector('a[data-tab="preview"]'); + const previewTab = document.querySelector('a[data-tab="preview"]'); if (!previewTab) return; if (previewable) { - const newUrl = (previewTab.getAttribute('data-url') || '').replace(/(.*)\/.*/, `$1/markup`); - previewTab.setAttribute('data-url', newUrl); previewTab.style.display = ''; } else { previewTab.style.display = 'none'; // If the "preview" tab was active, user changes the filename to a non-previewable one, // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active if (previewTab.classList.contains('active')) { - const writeTab = document.querySelector('a[data-tab="write"]'); + const writeTab = document.querySelector('a[data-tab="write"]'); writeTab.click(); } } diff --git a/web_src/js/features/comp/ConfirmModal.ts b/web_src/js/features/comp/ConfirmModal.ts index bf645cdbdb..1ce490ec2e 100644 --- a/web_src/js/features/comp/ConfirmModal.ts +++ b/web_src/js/features/comp/ConfirmModal.ts @@ -5,7 +5,7 @@ import {fomanticQuery} from '../../modules/fomantic/base.ts'; const {i18n} = window.config; -export function confirmModal({header = '', content = '', confirmButtonColor = 'primary'} = {}) { +export function confirmModal({header = '', content = '', confirmButtonColor = 'primary'} = {}): Promise { return new Promise((resolve) => { const headerHtml = header ? `
${htmlEscape(header)}
` : ''; const modal = createElementFromHTML(` diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index adae55f25c..96b08250fb 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {htmlEscape} from 'escape-goat'; import {createCodeEditor} from './codeeditor.ts'; import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts'; @@ -6,39 +5,33 @@ import {initMarkupContent} from '../markup/content.ts'; import {attachRefIssueContextPopup} from './contextpopup.ts'; import {POST} from '../modules/fetch.ts'; import {initDropzone} from './dropzone.ts'; +import {confirmModal} from './comp/ConfirmModal.ts'; +import {applyAreYouSure} from '../vendor/jquery.are-you-sure.ts'; +import {fomanticQuery} from '../modules/fomantic/base.ts'; -function initEditPreviewTab($form) { - const $tabMenu = $form.find('.repo-editor-menu'); - $tabMenu.find('.item').tab(); - const $previewTab = $tabMenu.find('a[data-tab="preview"]'); - if ($previewTab.length) { - $previewTab.on('click', async function () { - const $this = $(this); - let context = `${$this.data('context')}/`; - const mode = $this.data('markup-mode') || 'comment'; - const $treePathEl = $form.find('input#tree_path'); - if ($treePathEl.length > 0) { - context += $treePathEl.val(); - } - context = context.substring(0, context.lastIndexOf('/')); +function initEditPreviewTab(elForm: HTMLFormElement) { + const elTabMenu = elForm.querySelector('.repo-editor-menu'); + fomanticQuery(elTabMenu.querySelectorAll('.item')).tab(); - const formData = new FormData(); - formData.append('mode', mode); - formData.append('context', context); - formData.append('text', $form.find('.tab[data-tab="write"] textarea').val()); - formData.append('file_path', $treePathEl.val()); - try { - const response = await POST($this.data('url'), {data: formData}); - const data = await response.text(); - const $previewPanel = $form.find('.tab[data-tab="preview"]'); - if ($previewPanel.length) { - renderPreviewPanelContent($previewPanel, data); - } - } catch (error) { - console.error('Error:', error); - } - }); - } + const elPreviewTab = elTabMenu.querySelector('a[data-tab="preview"]'); + const elPreviewPanel = elForm.querySelector('.tab[data-tab="preview"]'); + if (!elPreviewTab || !elPreviewPanel) return; + + elPreviewTab.addEventListener('click', async () => { + const elTreePath = elForm.querySelector('input#tree_path'); + const previewUrl = elPreviewTab.getAttribute('data-preview-url'); + const previewContextRef = elPreviewTab.getAttribute('data-preview-context-ref'); + let previewContext = `${previewContextRef}/${elTreePath.value}`; + previewContext = previewContext.substring(0, previewContext.lastIndexOf('/')); + const formData = new FormData(); + formData.append('mode', 'file'); + formData.append('context', previewContext); + formData.append('text', elForm.querySelector('.tab[data-tab="write"] textarea').value); + formData.append('file_path', elTreePath.value); + const response = await POST(previewUrl, {data: formData}); + const data = await response.text(); + renderPreviewPanelContent(elPreviewPanel, data); + }); } export function initRepoEditor() { @@ -151,8 +144,8 @@ export function initRepoEditor() { } }); - const $form = $('.repository.editor .edit.form'); - initEditPreviewTab($form); + const elForm = document.querySelector('.repository.editor .edit.form'); + initEditPreviewTab(elForm); (async () => { const editor = await createCodeEditor(editArea, filenameInput); @@ -160,16 +153,16 @@ export function initRepoEditor() { // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage // to enable or disable the commit button const commitButton = document.querySelector('#commit-button'); - const $editForm = $('.ui.edit.form'); const dirtyFileClass = 'dirty-file'; // Disabling the button at the start - if ($('input[name="page_has_posted"]').val() !== 'true') { + if (document.querySelector('input[name="page_has_posted"]').value !== 'true') { commitButton.disabled = true; } // Registering a custom listener for the file path and the file content - $editForm.areYouSure({ + // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added + applyAreYouSure(elForm, { silent: true, dirtyClass: dirtyFileClass, fieldSelector: ':input:not(.commit-form-wrapper :input)', @@ -187,15 +180,17 @@ export function initRepoEditor() { editor.setValue(value); } - commitButton?.addEventListener('click', (e) => { + commitButton?.addEventListener('click', async (e) => { // A modal which asks if an empty file should be committed if (!editArea.value) { - $('#edit-empty-content-modal').modal({ - onApprove() { - $('.edit.form').trigger('submit'); - }, - }).modal('show'); e.preventDefault(); + if (await confirmModal({ + header: elForm.getAttribute('data-text-empty-confirm-header'), + content: elForm.getAttribute('data-text-empty-confirm-content'), + })) { + elForm.classList.remove('dirty'); + elForm.submit(); + } } }); })(); diff --git a/web_src/js/vendor/jquery.are-you-sure.ts b/web_src/js/vendor/jquery.are-you-sure.ts index bd621a145e..9efe783c54 100644 --- a/web_src/js/vendor/jquery.are-you-sure.ts +++ b/web_src/js/vendor/jquery.are-you-sure.ts @@ -196,6 +196,6 @@ export function initAreYouSure($) { }; } -export function applyAreYouSure(selector: string) { - $(selector).areYouSure(); +export function applyAreYouSure(selectorOrEl: string|Element|$, opts = {}) { + $(selectorOrEl).areYouSure(opts); } From 4142397b0bd97c72da2af99292340b7bc68c3f49 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 4 Dec 2024 22:57:50 +0800 Subject: [PATCH 4/6] Fix mentionable users when writing issue comments (#32715) Fix #32702 --- routers/web/repo/issue.go | 8 ++++++-- routers/web/repo/issue_list.go | 4 +--- routers/web/repo/issue_page_meta.go | 2 +- routers/web/repo/pull.go | 4 +--- routers/web/shared/user/helper.go | 16 ++++++++++------ 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 415f34d1fb..833f59981b 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -637,8 +637,12 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, return attachHTML } -// get all teams that current user can mention -func handleTeamMentions(ctx *context.Context) { +// handleMentionableAssigneesAndTeams gets all teams that current user can mention, and fills the assignee users to the context data +func handleMentionableAssigneesAndTeams(ctx *context.Context, assignees []*user_model.User) { + // TODO: need to figure out how many places this is really used, and rename it to "MentionableAssignees" + // at the moment it is used on the issue list page, for the markdown editor mention + ctx.Data["Assignees"] = assignees + if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() { return } diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index ee2fc080f5..50bb668433 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -715,9 +715,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - - handleTeamMentions(ctx) + handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)) if ctx.Written() { return } diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index e04d76b287..7eda6e3c73 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -148,7 +148,7 @@ func (d *IssuePageMetaData) retrieveAssigneesDataForIssueWriter(ctx *context.Con d.AssigneesData.SelectedAssigneeIDs = strings.Join(ids, ",") } // FIXME: this is a tricky part which writes ctx.Data["Mentionable*"] - handleTeamMentions(ctx) + handleMentionableAssigneesAndTeams(ctx, d.AssigneesData.CandidateAssignees) } func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index e3b329d01d..0325585e53 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -840,9 +840,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi ctx.ServerError("GetRepoAssignees", err) return } - ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - - handleTeamMentions(ctx) + handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)) if ctx.Written() { return } diff --git a/routers/web/shared/user/helper.go b/routers/web/shared/user/helper.go index 6186b9b9ff..dfd65420c1 100644 --- a/routers/web/shared/user/helper.go +++ b/routers/web/shared/user/helper.go @@ -4,19 +4,23 @@ package user import ( - "sort" + "slices" "code.gitea.io/gitea/models/user" ) func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { if doer != nil { - sort.Slice(users, func(i, j int) bool { - if users[i].ID == users[j].ID { - return false - } - return users[i].ID == doer.ID // if users[i] is self, put it before others, so less=true + idx := slices.IndexFunc(users, func(u *user.User) bool { + return u.ID == doer.ID }) + if idx > 0 { + newUsers := make([]*user.User, len(users)) + newUsers[0] = users[idx] + copy(newUsers[1:], users[:idx]) + copy(newUsers[idx+1:], users[idx+1:]) + return newUsers + } } return users } From 5ab7aa700f4cafcb33d8ad77708d7419ad2480fa Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 4 Dec 2024 14:33:43 -0800 Subject: [PATCH 5/6] Use new mail package instead of an unmintained one (#32682) Resolve #18664 --- assets/go-licenses.json | 15 ++++++---- go.mod | 3 +- go.sum | 25 ++++++++++++++--- services/mailer/mail_test.go | 34 +++++++++++------------ services/mailer/mailer.go | 6 ++-- services/mailer/sender/main_test.go | 14 ++++++++++ services/mailer/sender/message.go | 38 ++++++++++++++------------ services/mailer/sender/message_test.go | 32 ++++++++++++++-------- services/mailer/sender/sender.go | 23 ++++++++++++---- services/mailer/sender/smtp.go | 12 ++++++-- services/mailer/sender/smtp_auth.go | 2 +- 11 files changed, 133 insertions(+), 71 deletions(-) create mode 100644 services/mailer/sender/main_test.go diff --git a/assets/go-licenses.json b/assets/go-licenses.json index fcfde08800..64c3b8b51c 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -1124,6 +1124,16 @@ "path": "github.com/valyala/fastjson/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2018 Aliaksandr Valialkin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, + { + "name": "github.com/wneessen/go-mail", + "path": "github.com/wneessen/go-mail/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2022-2023 The go-mail Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "github.com/wneessen/go-mail/smtp", + "path": "github.com/wneessen/go-mail/smtp/LICENSE", + "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + }, { "name": "github.com/x448/float16", "path": "github.com/x448/float16/LICENSE", @@ -1259,11 +1269,6 @@ "path": "google.golang.org/protobuf/LICENSE", "licenseText": "Copyright (c) 2018 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "gopkg.in/gomail.v2", - "path": "gopkg.in/gomail.v2/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Alexandre Cesaro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - }, { "name": "gopkg.in/ini.v1", "path": "gopkg.in/ini.v1/LICENSE", diff --git a/go.mod b/go.mod index bbd8186868..80b62ce83f 100644 --- a/go.mod +++ b/go.mod @@ -114,6 +114,7 @@ require ( github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.12 github.com/urfave/cli/v2 v2.27.5 + github.com/wneessen/go-mail v0.5.2 github.com/xanzy/go-gitlab v0.112.0 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 @@ -130,7 +131,6 @@ require ( golang.org/x/tools v0.26.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 - gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/xurls/v2 v2.5.0 @@ -319,7 +319,6 @@ require ( golang.org/x/mod v0.21.0 // indirect golang.org/x/time v0.7.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index df3b7d899c..d1b7890fb6 100644 --- a/go.sum +++ b/go.sum @@ -815,6 +815,8 @@ github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5 github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/wneessen/go-mail v0.5.2 h1:MZKwgHJoRboLJ+EHMLuHpZc95wo+u1xViL/4XSswDT8= +github.com/wneessen/go-mail v0.5.2/go.mod h1:kRroJvEq2hOSEPFRiKjN7Csrz0G1w+RpiGR3b6yo+Ck= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+DKw= @@ -887,8 +889,10 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= @@ -901,6 +905,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -920,8 +927,10 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= @@ -934,6 +943,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -966,10 +978,13 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -977,8 +992,10 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -989,7 +1006,9 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= @@ -1004,6 +1023,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1024,8 +1045,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1033,8 +1052,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index 42de7599eb..185b72f069 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -93,20 +93,20 @@ func TestComposeIssueCommentMessage(t *testing.T) { assert.NoError(t, err) assert.Len(t, msgs, 2) gomailMsg := msgs[0].ToMessage() - replyTo := gomailMsg.GetHeader("Reply-To")[0] - subject := gomailMsg.GetHeader("Subject")[0] + replyTo := gomailMsg.GetGenHeader("Reply-To")[0] + subject := gomailMsg.GetGenHeader("Subject")[0] - assert.Len(t, gomailMsg.GetHeader("To"), 1, "exactly one recipient is expected in the To field") + assert.Len(t, gomailMsg.GetAddrHeader("To"), 1, "exactly one recipient is expected in the To field") tokenRegex := regexp.MustCompile(`\Aincoming\+(.+)@localhost\z`) assert.Regexp(t, tokenRegex, replyTo) token := tokenRegex.FindAllStringSubmatch(replyTo, 1)[0][1] assert.Equal(t, "Re: ", subject[:4], "Comment reply subject should contain Re:") assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject) - assert.Equal(t, "", gomailMsg.GetHeader("In-Reply-To")[0], "In-Reply-To header doesn't match") - assert.ElementsMatch(t, []string{"", ""}, gomailMsg.GetHeader("References"), "References header doesn't match") - assert.Equal(t, "", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match") - assert.Equal(t, "", gomailMsg.GetHeader("List-Post")[0]) - assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto + assert.Equal(t, "", gomailMsg.GetGenHeader("In-Reply-To")[0], "In-Reply-To header doesn't match") + assert.ElementsMatch(t, []string{"", ""}, gomailMsg.GetGenHeader("References"), "References header doesn't match") + assert.Equal(t, "", gomailMsg.GetGenHeader("Message-ID")[0], "Message-ID header doesn't match") + assert.Equal(t, "", gomailMsg.GetGenHeader("List-Post")[0]) + assert.Len(t, gomailMsg.GetGenHeader("List-Unsubscribe"), 2) // url + mailto var buf bytes.Buffer gomailMsg.WriteTo(&buf) @@ -139,19 +139,19 @@ func TestComposeIssueMessage(t *testing.T) { assert.Len(t, msgs, 2) gomailMsg := msgs[0].ToMessage() - mailto := gomailMsg.GetHeader("To") - subject := gomailMsg.GetHeader("Subject") - messageID := gomailMsg.GetHeader("Message-ID") - inReplyTo := gomailMsg.GetHeader("In-Reply-To") - references := gomailMsg.GetHeader("References") + mailto := gomailMsg.GetAddrHeader("To") + subject := gomailMsg.GetGenHeader("Subject") + messageID := gomailMsg.GetGenHeader("Message-ID") + inReplyTo := gomailMsg.GetGenHeader("In-Reply-To") + references := gomailMsg.GetGenHeader("References") assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field") assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0]) assert.Equal(t, "", inReplyTo[0], "In-Reply-To header doesn't match") assert.Equal(t, "", references[0], "References header doesn't match") assert.Equal(t, "", messageID[0], "Message-ID header doesn't match") - assert.Empty(t, gomailMsg.GetHeader("List-Post")) // incoming mail feature disabled - assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 1) // url without mailto + assert.Empty(t, gomailMsg.GetGenHeader("List-Post")) // incoming mail feature disabled + assert.Len(t, gomailMsg.GetGenHeader("List-Unsubscribe"), 1) // url without mailto } func TestTemplateSelection(t *testing.T) { @@ -169,7 +169,7 @@ func TestTemplateSelection(t *testing.T) { template.Must(bodyTemplates.New("issue/close").Parse("issue/close/body")) expect := func(t *testing.T, msg *sender_service.Message, expSubject, expBody string) { - subject := msg.ToMessage().GetHeader("Subject") + subject := msg.ToMessage().GetGenHeader("Subject") msgbuf := new(bytes.Buffer) _, _ = msg.ToMessage().WriteTo(msgbuf) wholemsg := msgbuf.String() @@ -225,7 +225,7 @@ func TestTemplateServices(t *testing.T) { Content: "test body", Comment: comment, }, recipients, fromMention, "TestTemplateServices") - subject := msg.ToMessage().GetHeader("Subject") + subject := msg.ToMessage().GetGenHeader("Subject") msgbuf := new(bytes.Buffer) _, _ = msg.ToMessage().WriteTo(msgbuf) wholemsg := msgbuf.String() diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go index bf4b5a43cb..bcd4facca9 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -48,11 +48,11 @@ func NewContext(ctx context.Context) { mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*sender_service.Message) []*sender_service.Message { for _, msg := range items { gomailMsg := msg.ToMessage() - log.Trace("New e-mail sending request %s: %s", gomailMsg.GetHeader("To"), msg.Info) + log.Trace("New e-mail sending request %s: %s", gomailMsg.GetGenHeader("To"), msg.Info) if err := sender_service.Send(sender, msg); err != nil { - log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetHeader("To"), msg.Info, err) + log.Error("Failed to send emails %s: %s - %v", gomailMsg.GetGenHeader("To"), msg.Info, err) } else { - log.Trace("E-mails sent %s: %s", gomailMsg.GetHeader("To"), msg.Info) + log.Trace("E-mails sent %s: %s", gomailMsg.GetGenHeader("To"), msg.Info) } } return nil diff --git a/services/mailer/sender/main_test.go b/services/mailer/sender/main_test.go new file mode 100644 index 0000000000..c67057964f --- /dev/null +++ b/services/mailer/sender/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package sender + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/services/mailer/sender/message.go b/services/mailer/sender/message.go index a3255692f0..db20675572 100644 --- a/services/mailer/sender/message.go +++ b/services/mailer/sender/message.go @@ -6,6 +6,7 @@ package sender import ( "fmt" "hash/fnv" + "net/mail" "strings" "time" @@ -14,7 +15,7 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/jaytaylor/html2text" - "gopkg.in/gomail.v2" + gomail "github.com/wneessen/go-mail" ) // Message mail body and log info @@ -31,45 +32,46 @@ type Message struct { } // ToMessage converts a Message to gomail.Message -func (m *Message) ToMessage() *gomail.Message { - msg := gomail.NewMessage() - msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName) - msg.SetHeader("To", m.To) +func (m *Message) ToMessage() *gomail.Msg { + msg := gomail.NewMsg() + addr := mail.Address{Name: m.FromDisplayName, Address: m.FromAddress} + _ = msg.SetAddrHeader("From", addr.String()) + _ = msg.SetAddrHeader("To", m.To) if m.ReplyTo != "" { - msg.SetHeader("Reply-To", m.ReplyTo) + msg.SetGenHeader("Reply-To", m.ReplyTo) } for header := range m.Headers { - msg.SetHeader(header, m.Headers[header]...) + msg.SetGenHeader(gomail.Header(header), m.Headers[header]...) } if setting.MailService.SubjectPrefix != "" { - msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) + msg.SetGenHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject) } else { - msg.SetHeader("Subject", m.Subject) + msg.SetGenHeader("Subject", m.Subject) } - msg.SetDateHeader("Date", m.Date) - msg.SetHeader("X-Auto-Response-Suppress", "All") + msg.SetDateWithValue(m.Date) + msg.SetGenHeader("X-Auto-Response-Suppress", "All") plainBody, err := html2text.FromString(m.Body) if err != nil || setting.MailService.SendAsPlainText { if strings.Contains(base.TruncateString(m.Body, 100), "") { log.Warn("Mail contains HTML but configured to send as plain text.") } - msg.SetBody("text/plain", plainBody) + msg.SetBodyString("text/plain", plainBody) } else { - msg.SetBody("text/plain", plainBody) - msg.AddAlternative("text/html", m.Body) + msg.SetBodyString("text/plain", plainBody) + msg.AddAlternativeString("text/html", m.Body) } - if len(msg.GetHeader("Message-ID")) == 0 { - msg.SetHeader("Message-ID", m.generateAutoMessageID()) + if len(msg.GetGenHeader("Message-ID")) == 0 { + msg.SetGenHeader("Message-ID", m.generateAutoMessageID()) } for k, v := range setting.MailService.OverrideHeader { - if len(msg.GetHeader(k)) != 0 { + if len(msg.GetGenHeader(gomail.Header(k))) != 0 { log.Debug("Mailer override header '%s' as per config", k) } - msg.SetHeader(k, v...) + msg.SetGenHeader(gomail.Header(k), v...) } return msg diff --git a/services/mailer/sender/message_test.go b/services/mailer/sender/message_test.go index d47052685e..63d0bc349a 100644 --- a/services/mailer/sender/message_test.go +++ b/services/mailer/sender/message_test.go @@ -25,25 +25,27 @@ func TestGenerateMessageID(t *testing.T) { m := NewMessageFrom("", "display-name", "from-address", "subject", "body") m.Date = date gm := m.ToMessage() - assert.Equal(t, "", gm.GetHeader("Message-ID")[0]) + assert.Equal(t, "", gm.GetGenHeader("Message-ID")[0]) m = NewMessageFrom("a@b.com", "display-name", "from-address", "subject", "body") m.Date = date gm = m.ToMessage() - assert.Equal(t, "", gm.GetHeader("Message-ID")[0]) + assert.Equal(t, "", gm.GetGenHeader("Message-ID")[0]) m = NewMessageFrom("a@b.com", "display-name", "from-address", "subject", "body") m.SetHeader("Message-ID", "") gm = m.ToMessage() - assert.Equal(t, "", gm.GetHeader("Message-ID")[0]) + assert.Equal(t, "", gm.GetGenHeader("Message-ID")[0]) } func TestToMessage(t *testing.T) { - oldConf := *setting.MailService + oldConf := setting.MailService defer func() { - setting.MailService = &oldConf + setting.MailService = oldConf }() - setting.MailService.From = "test@gitea.com" + setting.MailService = &setting.Mailer{ + From: "test@gitea.com", + } m1 := Message{ Info: "info", @@ -54,18 +56,24 @@ func TestToMessage(t *testing.T) { Body: "Some Issue got closed by Y-Man", } + assertHeaders := func(t *testing.T, expected, header map[string]string) { + for k, v := range expected { + assert.Equal(t, v, header[k], "Header %s should be %s but got %s", k, v, header[k]) + } + } + buf := &strings.Builder{} _, err := m1.ToMessage().WriteTo(buf) assert.NoError(t, err) header, _ := extractMailHeaderAndContent(t, buf.String()) - assert.EqualValues(t, map[string]string{ + assertHeaders(t, map[string]string{ "Content-Type": "multipart/alternative;", "Date": "Mon, 01 Jan 0001 00:00:00 +0000", "From": "\"Test Gitea\" ", "Message-ID": "", - "Mime-Version": "1.0", + "MIME-Version": "1.0", "Subject": "Issue X Closed", - "To": "a@b.com", + "To": "", "X-Auto-Response-Suppress": "All", }, header) @@ -78,14 +86,14 @@ func TestToMessage(t *testing.T) { _, err = m1.ToMessage().WriteTo(buf) assert.NoError(t, err) header, _ = extractMailHeaderAndContent(t, buf.String()) - assert.EqualValues(t, map[string]string{ + assertHeaders(t, map[string]string{ "Content-Type": "multipart/alternative;", "Date": "Mon, 01 Jan 0001 00:00:00 +0000", "From": "\"Test Gitea\" ", "Message-ID": "", - "Mime-Version": "1.0", + "MIME-Version": "1.0", "Subject": "Issue X Closed", - "To": "a@b.com", + "To": "", "X-Auto-Response-Suppress": "All", "Auto-Submitted": "auto-generated", }, header) diff --git a/services/mailer/sender/sender.go b/services/mailer/sender/sender.go index bf317aa846..e470c2f2b3 100644 --- a/services/mailer/sender/sender.go +++ b/services/mailer/sender/sender.go @@ -4,13 +4,15 @@ package sender import ( + "io" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - - "gopkg.in/gomail.v2" ) -type Sender gomail.Sender +type Sender interface { + Send(from string, to []string, msg io.WriterTo) error +} var Send = send @@ -19,9 +21,18 @@ func send(sender Sender, msgs ...*Message) error { log.Error("Mailer: Send is being invoked but mail service hasn't been initialized") return nil } - goMsgs := []*gomail.Message{} for _, msg := range msgs { - goMsgs = append(goMsgs, msg.ToMessage()) + m := msg.ToMessage() + froms := m.GetFrom() + to, err := m.GetRecipients() + if err != nil { + return err + } + + // TODO: implement sending from multiple addresses + if err := sender.Send(froms[0].Address, to, m); err != nil { + return err + } } - return gomail.Send(sender, goMsgs...) + return nil } diff --git a/services/mailer/sender/smtp.go b/services/mailer/sender/smtp.go index ab49b7e5f8..c53c3da997 100644 --- a/services/mailer/sender/smtp.go +++ b/services/mailer/sender/smtp.go @@ -8,12 +8,13 @@ import ( "fmt" "io" "net" - "net/smtp" "os" "strings" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + + "github.com/wneessen/go-mail/smtp" ) // SMTPSender Sender SMTP mail sender @@ -106,7 +107,7 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error { if strings.Contains(options, "CRAM-MD5") { auth = smtp.CRAMMD5Auth(opts.User, opts.Passwd) } else if strings.Contains(options, "PLAIN") { - auth = smtp.PlainAuth("", opts.User, opts.Passwd, host) + auth = smtp.PlainAuth("", opts.User, opts.Passwd, host, false) } else if strings.Contains(options, "LOGIN") { // Patch for AUTH LOGIN auth = LoginAuth(opts.User, opts.Passwd) @@ -146,5 +147,10 @@ func (s *SMTPSender) Send(from string, to []string, msg io.WriterTo) error { return fmt.Errorf("SMTP close failed: %w", err) } - return client.Quit() + err = client.Quit() + if err != nil { + log.Error("Quit client failed: %v", err) + } + + return nil } diff --git a/services/mailer/sender/smtp_auth.go b/services/mailer/sender/smtp_auth.go index df65498a5a..260b12437b 100644 --- a/services/mailer/sender/smtp_auth.go +++ b/services/mailer/sender/smtp_auth.go @@ -5,9 +5,9 @@ package sender import ( "fmt" - "net/smtp" "github.com/Azure/go-ntlmssp" + "github.com/wneessen/go-mail/smtp" ) type loginAuth struct { From 0c3c041c88afc66a5048c1d8cf1b29c8bbbb798f Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 5 Dec 2024 00:09:07 +0100 Subject: [PATCH 6/6] Add Arch package registry (#32692) Close #25037 Close #31037 This PR adds a Arch package registry usable with pacman. ![grafik](https://github.com/user-attachments/assets/81cdb0c2-02f9-4733-bee2-e48af6b45224) Rewrite of #25396 and #31037. You can follow [this tutorial](https://wiki.archlinux.org/title/Creating_packages) to build a package for testing. Docs PR: https://gitea.com/gitea/docs/pulls/111 Co-authored-by: [d1nch8g@ion.lc](mailto:d1nch8g@ion.lc) Co-authored-by: @ExplodingDragon --------- Co-authored-by: dancheg97 Co-authored-by: dragon Co-authored-by: wxiaoguang --- models/packages/arch/search.go | 38 ++ models/packages/descriptor.go | 3 + models/packages/package.go | 6 + models/packages/package_file.go | 5 + modules/packages/arch/metadata.go | 249 ++++++++++++ modules/packages/arch/metadata_test.go | 157 ++++++++ modules/setting/packages.go | 2 + options/locale/locale_en-US.ini | 5 + public/assets/img/svg/gitea-arch.svg | 1 + routers/api/packages/api.go | 44 +++ routers/api/packages/arch/arch.go | 306 +++++++++++++++ routers/web/user/package.go | 31 +- services/forms/package_form.go | 2 +- services/packages/alpine/repository.go | 2 +- services/packages/arch/repository.go | 401 ++++++++++++++++++++ services/packages/cleanup/cleanup.go | 18 +- services/packages/packages.go | 2 + templates/package/content/arch.tmpl | 41 ++ templates/package/content/container.tmpl | 4 +- templates/package/metadata/alpine.tmpl | 6 +- templates/package/metadata/arch.tmpl | 4 + templates/package/metadata/cargo.tmpl | 10 +- templates/package/metadata/chef.tmpl | 6 +- templates/package/metadata/composer.tmpl | 6 +- templates/package/metadata/conan.tmpl | 8 +- templates/package/metadata/conda.tmpl | 8 +- templates/package/metadata/container.tmpl | 14 +- templates/package/metadata/cran.tmpl | 6 +- templates/package/metadata/debian.tmpl | 4 +- templates/package/metadata/helm.tmpl | 4 +- templates/package/metadata/maven.tmpl | 8 +- templates/package/metadata/npm.tmpl | 8 +- templates/package/metadata/nuget.tmpl | 6 +- templates/package/metadata/pub.tmpl | 6 +- templates/package/metadata/pypi.tmpl | 6 +- templates/package/metadata/rpm.tmpl | 4 +- templates/package/metadata/rubygems.tmpl | 6 +- templates/package/metadata/swift.tmpl | 4 +- templates/package/metadata/vagrant.tmpl | 6 +- templates/package/view.tmpl | 26 +- tests/integration/api_packages_arch_test.go | 302 +++++++++++++++ web_src/css/base.css | 2 +- web_src/svg/gitea-arch.svg | 1 + 43 files changed, 1687 insertions(+), 91 deletions(-) create mode 100644 models/packages/arch/search.go create mode 100644 modules/packages/arch/metadata.go create mode 100644 modules/packages/arch/metadata_test.go create mode 100644 public/assets/img/svg/gitea-arch.svg create mode 100644 routers/api/packages/arch/arch.go create mode 100644 services/packages/arch/repository.go create mode 100644 templates/package/content/arch.tmpl create mode 100644 templates/package/metadata/arch.tmpl create mode 100644 tests/integration/api_packages_arch_test.go create mode 100644 web_src/svg/gitea-arch.svg diff --git a/models/packages/arch/search.go b/models/packages/arch/search.go new file mode 100644 index 0000000000..f35c037b23 --- /dev/null +++ b/models/packages/arch/search.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "context" + + packages_model "code.gitea.io/gitea/models/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" +) + +// GetRepositories gets all available repositories +func GetRepositories(ctx context.Context, ownerID int64) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeArch, + ownerID, + packages_model.PropertyTypeFile, + arch_module.PropertyRepository, + nil, + ) +} + +// GetArchitectures gets all available architectures for the given repository +func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeArch, + ownerID, + packages_model.PropertyTypeFile, + arch_module.PropertyArchitecture, + &packages_model.DistinctPropertyDependency{ + Name: arch_module.PropertyRepository, + Value: repository, + }, + ) +} diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index b8ef698d38..803b73c968 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/alpine" + "code.gitea.io/gitea/modules/packages/arch" "code.gitea.io/gitea/modules/packages/cargo" "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/packages/composer" @@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc switch p.Type { case TypeAlpine: metadata = &alpine.VersionMetadata{} + case TypeArch: + metadata = &arch.VersionMetadata{} case TypeCargo: metadata = &cargo.Metadata{} case TypeChef: diff --git a/models/packages/package.go b/models/packages/package.go index 65a2574150..417d62d199 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -31,6 +31,7 @@ type Type string // List of supported packages const ( TypeAlpine Type = "alpine" + TypeArch Type = "arch" TypeCargo Type = "cargo" TypeChef Type = "chef" TypeComposer Type = "composer" @@ -55,6 +56,7 @@ const ( var TypeList = []Type{ TypeAlpine, + TypeArch, TypeCargo, TypeChef, TypeComposer, @@ -82,6 +84,8 @@ func (pt Type) Name() string { switch pt { case TypeAlpine: return "Alpine" + case TypeArch: + return "Arch" case TypeCargo: return "Cargo" case TypeChef: @@ -131,6 +135,8 @@ func (pt Type) SVGName() string { switch pt { case TypeAlpine: return "gitea-alpine" + case TypeArch: + return "gitea-arch" case TypeCargo: return "gitea-cargo" case TypeChef: diff --git a/models/packages/package_file.go b/models/packages/package_file.go index 1bb6b57a34..270cb32fdf 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -221,6 +221,11 @@ func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*Packag return pfs, count, err } +// HasFiles tests if there are files of packages matching the search options +func HasFiles(ctx context.Context, opts *PackageFileSearchOptions) (bool, error) { + return db.Exist[PackageFile](ctx, opts.toConds()) +} + // CalculateFileSize sums up all blob sizes matching the search options. // It does NOT respect the deduplication of blobs. func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) { diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go new file mode 100644 index 0000000000..e1e79c60e0 --- /dev/null +++ b/modules/packages/arch/metadata.go @@ -0,0 +1,249 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "io" + "regexp" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" + + "github.com/klauspost/compress/zstd" + "github.com/ulikunitz/xz" +) + +const ( + PropertyRepository = "arch.repository" + PropertyArchitecture = "arch.architecture" + PropertySignature = "arch.signature" + PropertyMetadata = "arch.metadata" + + SettingKeyPrivate = "arch.key.private" + SettingKeyPublic = "arch.key.public" + + RepositoryPackage = "_arch" + RepositoryVersion = "_repository" + + AnyArch = "any" +) + +var ( + ErrMissingPKGINFOFile = util.NewInvalidArgumentErrorf(".PKGINFO file is missing") + ErrUnsupportedFormat = util.NewInvalidArgumentErrorf("unsupported package container format") + ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") + ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") + + // https://man.archlinux.org/man/PKGBUILD.5 + namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`) + versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) +) + +type Package struct { + Name string + Version string + VersionMetadata VersionMetadata + FileMetadata FileMetadata + FileCompressionExtension string +} + +type VersionMetadata struct { + Description string `json:"description,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Licenses []string `json:"licenses,omitempty"` +} + +type FileMetadata struct { + Architecture string `json:"architecture"` + Base string `json:"base,omitempty"` + InstalledSize int64 `json:"installed_size,omitempty"` + BuildDate int64 `json:"build_date,omitempty"` + Packager string `json:"packager,omitempty"` + Groups []string `json:"groups,omitempty"` + Provides []string `json:"provides,omitempty"` + Depends []string `json:"depends,omitempty"` + OptDepends []string `json:"opt_depends,omitempty"` + MakeDepends []string `json:"make_depends,omitempty"` + CheckDepends []string `json:"check_depends,omitempty"` + XData []string `json:"xdata,omitempty"` + Backup []string `json:"backup,omitempty"` + Files []string `json:"files,omitempty"` +} + +// ParsePackage parses an Arch package file +func ParsePackage(r io.Reader) (*Package, error) { + header := make([]byte, 10) + n, err := util.ReadAtMost(r, header) + if err != nil { + return nil, err + } + + r = io.MultiReader(bytes.NewReader(header[:n]), r) + + var inner io.Reader + var compressionType string + if bytes.HasPrefix(header, []byte{0x28, 0xB5, 0x2F, 0xFD}) { // zst + zr, err := zstd.NewReader(r) + if err != nil { + return nil, err + } + defer zr.Close() + + inner = zr + compressionType = "zst" + } else if bytes.HasPrefix(header, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A}) { // xz + xzr, err := xz.NewReader(r) + if err != nil { + return nil, err + } + + inner = xzr + compressionType = "xz" + } else if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { // gz + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + inner = gzr + compressionType = "gz" + } else { + return nil, ErrUnsupportedFormat + } + + var p *Package + files := make([]string, 0, 10) + + tr := tar.NewReader(inner) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Typeflag != tar.TypeReg { + continue + } + + filename := hd.FileInfo().Name() + if filename == ".PKGINFO" { + p, err = ParsePackageInfo(tr) + if err != nil { + return nil, err + } + } else if !strings.HasPrefix(filename, ".") { + files = append(files, hd.Name) + } + } + + if p == nil { + return nil, ErrMissingPKGINFOFile + } + + p.FileMetadata.Files = files + p.FileCompressionExtension = compressionType + + return p, nil +} + +// ParsePackageInfo parses a .PKGINFO file to retrieve the metadata +// https://man.archlinux.org/man/PKGBUILD.5 +// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_package.c#L161 +func ParsePackageInfo(r io.Reader) (*Package, error) { + p := &Package{} + + s := bufio.NewScanner(r) + for s.Scan() { + line := s.Text() + + if strings.HasPrefix(line, "#") { + continue + } + + i := strings.IndexRune(line, '=') + if i == -1 { + continue + } + + key := strings.TrimSpace(line[:i]) + value := strings.TrimSpace(line[i+1:]) + + switch key { + case "pkgname": + p.Name = value + case "pkgbase": + p.FileMetadata.Base = value + case "pkgver": + p.Version = value + case "pkgdesc": + p.VersionMetadata.Description = value + case "url": + p.VersionMetadata.ProjectURL = value + case "packager": + p.FileMetadata.Packager = value + case "arch": + p.FileMetadata.Architecture = value + case "license": + p.VersionMetadata.Licenses = append(p.VersionMetadata.Licenses, value) + case "provides": + p.FileMetadata.Provides = append(p.FileMetadata.Provides, value) + case "depend": + p.FileMetadata.Depends = append(p.FileMetadata.Depends, value) + case "optdepend": + p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value) + case "makedepend": + p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value) + case "checkdepend": + p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value) + case "backup": + p.FileMetadata.Backup = append(p.FileMetadata.Backup, value) + case "group": + p.FileMetadata.Groups = append(p.FileMetadata.Groups, value) + case "builddate": + date, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + p.FileMetadata.BuildDate = date + case "size": + size, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + p.FileMetadata.InstalledSize = size + case "xdata": + p.FileMetadata.XData = append(p.FileMetadata.XData, value) + } + } + if err := s.Err(); err != nil { + return nil, err + } + + if !namePattern.MatchString(p.Name) { + return nil, ErrInvalidName + } + if !versionPattern.MatchString(p.Version) { + return nil, ErrInvalidVersion + } + if p.FileMetadata.Architecture == "" { + return nil, ErrInvalidArchitecture + } + + if !validation.IsValidURL(p.VersionMetadata.ProjectURL) { + p.VersionMetadata.ProjectURL = "" + } + + return p, nil +} diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go new file mode 100644 index 0000000000..f611ef5e84 --- /dev/null +++ b/modules/packages/arch/metadata_test.go @@ -0,0 +1,157 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "testing" + + "github.com/klauspost/compress/zstd" + "github.com/stretchr/testify/assert" + "github.com/ulikunitz/xz" +) + +const ( + packageName = "gitea" + packageVersion = "1.0.1" + packageDescription = "Package Description" + packageProjectURL = "https://gitea.com" + packagePackager = "KN4CK3R " +) + +func createPKGINFOContent(name, version string) []byte { + return []byte(`pkgname = ` + name + ` +pkgbase = ` + name + ` +pkgver = ` + version + ` +pkgdesc = ` + packageDescription + ` +url = ` + packageProjectURL + ` +# comment +group=group +builddate = 1678834800 +size = 123456 +arch = x86_64 +license = MIT +packager = ` + packagePackager + ` +depend = common +xdata = value +depend = gitea +provides = common +provides = gitea +optdepend = hex +checkdepend = common +makedepend = cmake +backup = usr/bin/paket1`) +} + +func TestParsePackage(t *testing.T) { + createPackage := func(compression string, files map[string][]byte) io.Reader { + var buf bytes.Buffer + var cw io.WriteCloser + switch compression { + case "zst": + cw, _ = zstd.NewWriter(&buf) + case "xz": + cw, _ = xz.NewWriter(&buf) + case "gz": + cw = gzip.NewWriter(&buf) + } + tw := tar.NewWriter(cw) + + for name, content := range files { + hdr := &tar.Header{ + Name: name, + Mode: 0o600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write(content) + } + + tw.Close() + cw.Close() + + return &buf + } + + for _, c := range []string{"gz", "xz", "zst"} { + t.Run(c, func(t *testing.T) { + t.Run("MissingPKGINFOFile", func(t *testing.T) { + data := createPackage(c, map[string][]byte{"dummy.txt": {}}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrMissingPKGINFOFile) + }) + + t.Run("InvalidPKGINFOFile", func(t *testing.T) { + data := createPackage(c, map[string][]byte{".PKGINFO": {}}) + + pp, err := ParsePackage(data) + assert.Nil(t, pp) + assert.ErrorIs(t, err, ErrInvalidName) + }) + + t.Run("Valid", func(t *testing.T) { + data := createPackage(c, map[string][]byte{ + ".PKGINFO": createPKGINFOContent(packageName, packageVersion), + "/test/dummy.txt": {}, + }) + + p, err := ParsePackage(data) + assert.NoError(t, err) + assert.NotNil(t, p) + + assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files) + }) + }) + } +} + +func TestParsePackageInfo(t *testing.T) { + t.Run("InvalidName", func(t *testing.T) { + data := createPKGINFOContent("", packageVersion) + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidName) + }) + + t.Run("InvalidVersion", func(t *testing.T) { + data := createPKGINFOContent(packageName, "") + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + }) + + t.Run("Valid", func(t *testing.T) { + data := createPKGINFOContent(packageName, packageVersion) + + p, err := ParsePackageInfo(bytes.NewReader(data)) + assert.NoError(t, err) + assert.NotNil(t, p) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageName, p.FileMetadata.Base) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, packageDescription, p.VersionMetadata.Description) + assert.Equal(t, packagePackager, p.FileMetadata.Packager) + assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL) + assert.ElementsMatch(t, []string{"MIT"}, p.VersionMetadata.Licenses) + assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate) + assert.EqualValues(t, 123456, p.FileMetadata.InstalledSize) + assert.Equal(t, "x86_64", p.FileMetadata.Architecture) + assert.ElementsMatch(t, []string{"value"}, p.FileMetadata.XData) + assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides) + assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends) + assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends) + assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends) + assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends) + assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup) + }) +} diff --git a/modules/setting/packages.go b/modules/setting/packages.go index bc093e7ea6..3f618cfd64 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -22,6 +22,7 @@ var ( LimitTotalOwnerCount int64 LimitTotalOwnerSize int64 LimitSizeAlpine int64 + LimitSizeArch int64 LimitSizeCargo int64 LimitSizeChef int64 LimitSizeComposer int64 @@ -79,6 +80,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) { Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE") + Packages.LimitSizeArch = mustBytes(sec, "LIMIT_SIZE_ARCH") Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO") Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF") Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 57d2d89c5a..395063faf8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3524,6 +3524,11 @@ alpine.repository = Repository Info alpine.repository.branches = Branches alpine.repository.repositories = Repositories alpine.repository.architectures = Architectures +arch.registry = Add server with related repository and architecture to /etc/pacman.conf: +arch.install = Sync package with pacman: +arch.repository = Repository Info +arch.repository.repositories = Repositories +arch.repository.architectures = Architectures cargo.registry = Setup this registry in the Cargo configuration file (for example ~/.cargo/config.toml): cargo.install = To install the package using Cargo, run the following command: chef.registry = Setup this registry in your ~/.chef/config.rb file: diff --git a/public/assets/img/svg/gitea-arch.svg b/public/assets/img/svg/gitea-arch.svg new file mode 100644 index 0000000000..943a92c579 --- /dev/null +++ b/public/assets/img/svg/gitea-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index c3da5a7513..4e194f65fa 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/packages/alpine" + "code.gitea.io/gitea/routers/api/packages/arch" "code.gitea.io/gitea/routers/api/packages/cargo" "code.gitea.io/gitea/routers/api/packages/chef" "code.gitea.io/gitea/routers/api/packages/composer" @@ -135,6 +136,49 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/arch", func() { + r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) + + r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { + path := strings.Trim(ctx.PathParam("*"), "/") + + if ctx.Req.Method == "PUT" { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + ctx.SetPathParam("repository", path) + arch.UploadPackageFile(ctx) + return + } + + pathFields := strings.Split(path, "/") + pathFieldsLen := len(pathFields) + + if (ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET") && pathFieldsLen >= 2 { + ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-2], "/")) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("filename", pathFields[pathFieldsLen-1]) + arch.GetPackageOrRepositoryFile(ctx) + return + } + + if ctx.Req.Method == "DELETE" && pathFieldsLen >= 3 { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-3], "/")) + ctx.SetPathParam("name", pathFields[pathFieldsLen-3]) + ctx.SetPathParam("version", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-1]) + arch.DeletePackageVersion(ctx) + return + } + + ctx.Status(http.StatusNotFound) + }) + }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { r.Group("/api/v1/crates", func() { r.Get("", cargo.SearchPackages) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go new file mode 100644 index 0000000000..573e93cfb0 --- /dev/null +++ b/routers/api/packages/arch/arch.go @@ -0,0 +1,306 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/json" + packages_module "code.gitea.io/gitea/modules/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/api/packages/helper" + "code.gitea.io/gitea/services/context" + packages_service "code.gitea.io/gitea/services/packages" + arch_service "code.gitea.io/gitea/services/packages/arch" +) + +func apiError(ctx *context.Context, status int, obj any) { + helper.LogAndProcessError(ctx, status, obj, func(message string) { + ctx.PlainText(status, message) + }) +} + +func GetRepositoryKey(ctx *context.Context) { + _, pub, err := arch_service.GetOrCreateKeyPair(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ContentType: "application/pgp-keys", + }) +} + +func UploadPackageFile(ctx *context.Context) { + repository := strings.TrimSpace(ctx.PathParam("repository")) + + upload, needToClose, err := ctx.UploadStream() + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if needToClose { + defer upload.Close() + } + + buf, err := packages_module.CreateHashedBufferFromReader(upload) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer buf.Close() + + pck, err := arch_module.ParsePackage(buf) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + apiError(ctx, http.StatusBadRequest, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + fileMetadataRaw, err := json.Marshal(pck.FileMetadata) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + signature, err := arch_service.SignData(ctx, ctx.Package.Owner.ID, buf) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + if _, err := buf.Seek(0, io.SeekStart); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + // Search for duplicates with different file compression + has, err := packages_model.HasFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + PackageType: packages_model.TypeArch, + Query: fmt.Sprintf("%s-%s-%s.pkg.tar.%%", pck.Name, pck.Version, pck.FileMetadata.Architecture), + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: pck.FileMetadata.Architecture, + }, + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if has { + apiError(ctx, http.StatusConflict, packages_model.ErrDuplicatePackageFile) + return + } + + _, _, err = packages_service.CreatePackageOrAddFileToExisting( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeArch, + Name: pck.Name, + Version: pck.Version, + }, + Creator: ctx.Doer, + Metadata: pck.VersionMetadata, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: fmt.Sprintf("%s-%s-%s.pkg.tar.%s", pck.Name, pck.Version, pck.FileMetadata.Architecture, pck.FileCompressionExtension), + CompositeKey: fmt.Sprintf("%s|%s", repository, pck.FileMetadata.Architecture), + }, + Creator: ctx.Doer, + Data: buf, + IsLead: true, + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: pck.FileMetadata.Architecture, + arch_module.PropertyMetadata: string(fileMetadataRaw), + arch_module.PropertySignature: base64.StdEncoding.EncodeToString(signature), + }, + }, + ) + if err != nil { + switch err { + case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile: + apiError(ctx, http.StatusConflict, err) + case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: + apiError(ctx, http.StatusForbidden, err) + default: + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if err := arch_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, repository, pck.FileMetadata.Architecture); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusCreated) +} + +func GetPackageOrRepositoryFile(ctx *context.Context) { + repository := ctx.PathParam("repository") + architecture := ctx.PathParam("architecture") + filename := ctx.PathParam("filename") + filenameOrig := filename + + isSignature := strings.HasSuffix(filename, ".sig") + if isSignature { + filename = filename[:len(filename)-len(".sig")] + } + + opts := &packages_model.PackageFileSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + PackageType: packages_model.TypeArch, + Query: filename, + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + } + + if strings.HasSuffix(filename, ".db.tar.gz") || strings.HasSuffix(filename, ".files.tar.gz") || strings.HasSuffix(filename, ".files") || strings.HasSuffix(filename, ".db") { + // The requested filename is based on the user-defined repository name. + // Normalize everything to "packages.db". + opts.Query = arch_service.IndexArchiveFilename + + pv, err := arch_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + opts.VersionID = pv.ID + } + + pfs, _, err := packages_model.SearchFiles(ctx, opts) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pfs) == 0 { + // Try again with architecture 'any' + if architecture == arch_module.AnyArch { + apiError(ctx, http.StatusNotFound, nil) + return + } + + opts.CompositeKey = fmt.Sprintf("%s|%s", repository, arch_module.AnyArch) + if pfs, _, err = packages_model.SearchFiles(ctx, opts); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + if len(pfs) != 1 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + if isSignature { + pfps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pfs[0].ID, arch_module.PropertySignature) + if err != nil || len(pfps) == 0 { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + data, err := base64.StdEncoding.DecodeString(pfps[0].Value) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.ServeContent(bytes.NewReader(data), &context.ServeHeaderOptions{ + Filename: filenameOrig, + }) + return + } + + s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + helper.ServePackageFile(ctx, s, u, pf) +} + +func DeletePackageVersion(ctx *context.Context) { + repository := ctx.PathParam("repository") + architecture := ctx.PathParam("architecture") + name := ctx.PathParam("name") + version := ctx.PathParam("version") + + release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + defer release() + + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeArch, name, version) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + VersionID: pv.ID, + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + if len(pfs) != 1 { + apiError(ctx, http.StatusNotFound, nil) + return + } + + if err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx, ctx.Doer, pfs[0]); err != nil { + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if err := arch_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, repository, architecture); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 4e39eabc0f..c6f85ac734 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" alpine_module "code.gitea.io/gitea/modules/packages/alpine" + arch_module "code.gitea.io/gitea/modules/packages/arch" debian_module "code.gitea.io/gitea/modules/packages/debian" rpm_module "code.gitea.io/gitea/modules/packages/rpm" "code.gitea.io/gitea/modules/setting" @@ -178,13 +179,13 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["IsPackagesPage"] = true ctx.Data["PackageDescriptor"] = pd + registryHostURL, err := url.Parse(httplib.GuessCurrentHostURL(ctx)) + if err != nil { + registryHostURL, _ = url.Parse(setting.AppURL) + } + ctx.Data["PackageRegistryHost"] = registryHostURL.Host + switch pd.Package.Type { - case packages_model.TypeContainer: - registryAppURL, err := url.Parse(httplib.GuessCurrentAppURL(ctx)) - if err != nil { - registryAppURL, _ = url.Parse(setting.AppURL) - } - ctx.Data["RegistryHost"] = registryAppURL.Host case packages_model.TypeAlpine: branches := make(container.Set[string]) repositories := make(container.Set[string]) @@ -204,6 +205,23 @@ func ViewPackageVersion(ctx *context.Context) { } ctx.Data["Branches"] = util.Sorted(branches.Values()) + ctx.Data["Repositories"] = util.Sorted(repositories.Values()) + ctx.Data["Architectures"] = util.Sorted(architectures.Values()) + case packages_model.TypeArch: + repositories := make(container.Set[string]) + architectures := make(container.Set[string]) + + for _, f := range pd.Files { + for _, pp := range f.Properties { + switch pp.Name { + case arch_module.PropertyRepository: + repositories.Add(pp.Value) + case arch_module.PropertyArchitecture: + architectures.Add(pp.Value) + } + } + } + ctx.Data["Repositories"] = util.Sorted(repositories.Values()) ctx.Data["Architectures"] = util.Sorted(architectures.Values()) case packages_model.TypeDebian: @@ -249,7 +267,6 @@ func ViewPackageVersion(ctx *context.Context) { var ( total int64 pvs []*packages_model.PackageVersion - err error ) switch pd.Package.Type { case packages_model.TypeContainer: diff --git a/services/forms/package_form.go b/services/forms/package_form.go index cc940d42d3..9b6f907164 100644 --- a/services/forms/package_form.go +++ b/services/forms/package_form.go @@ -15,7 +15,7 @@ import ( type PackageCleanupRuleForm struct { ID int64 Enabled bool - Type string `binding:"Required;In(alpine,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` + Type string `binding:"Required;In(alpine,arch,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"` KeepCount int `binding:"In(0,1,5,10,25,50,100)"` KeepPattern string `binding:"RegexPattern"` RemoveDays int `binding:"In(0,7,14,30,60,90,180)"` diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go index 664ab34559..27e6391980 100644 --- a/services/packages/alpine/repository.go +++ b/services/packages/alpine/repository.go @@ -72,7 +72,7 @@ func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, err return priv, pub, nil } -// BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures +// BuildAllRepositoryFiles (re)builds all repository files for every available branches, repositories and architectures func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) if err != nil { diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go new file mode 100644 index 0000000000..ab1b85ae95 --- /dev/null +++ b/services/packages/arch/repository.go @@ -0,0 +1,401 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "os" + "strings" + + packages_model "code.gitea.io/gitea/models/packages" + arch_model "code.gitea.io/gitea/models/packages/arch" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/globallock" + "code.gitea.io/gitea/modules/json" + packages_module "code.gitea.io/gitea/modules/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" + "code.gitea.io/gitea/modules/util" + packages_service "code.gitea.io/gitea/services/packages" + + "github.com/keybase/go-crypto/openpgp" + "github.com/keybase/go-crypto/openpgp/armor" + "github.com/keybase/go-crypto/openpgp/packet" +) + +const ( + IndexArchiveFilename = "packages.db" +) + +func AquireRegistryLock(ctx context.Context, ownerID int64) (globallock.ReleaseFunc, error) { + return globallock.Lock(ctx, fmt.Sprintf("packages_arch_%d", ownerID)) +} + +// GetOrCreateRepositoryVersion gets or creates the internal repository package +// The Arch registry needs multiple index files which are stored in this package. +func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) { + return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeArch, arch_module.RepositoryPackage, arch_module.RepositoryVersion) +} + +// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository files +func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) { + priv, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPrivate) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return "", "", err + } + + pub, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPublic) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return "", "", err + } + + if priv == "" || pub == "" { + priv, pub, err = generateKeypair() + if err != nil { + return "", "", err + } + + if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPrivate, priv); err != nil { + return "", "", err + } + + if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPublic, pub); err != nil { + return "", "", err + } + } + + return priv, pub, nil +} + +func generateKeypair() (string, string, error) { + e, err := openpgp.NewEntity("", "Arch Registry", "", nil) + if err != nil { + return "", "", err + } + + var priv strings.Builder + var pub strings.Builder + + w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil) + if err != nil { + return "", "", err + } + if err := e.SerializePrivate(w, nil); err != nil { + return "", "", err + } + w.Close() + + w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil) + if err != nil { + return "", "", err + } + if err := e.Serialize(w); err != nil { + return "", "", err + } + w.Close() + + return priv.String(), pub.String(), nil +} + +func SignData(ctx context.Context, ownerID int64, r io.Reader) ([]byte, error) { + priv, _, err := GetOrCreateKeyPair(ctx, ownerID) + if err != nil { + return nil, err + } + + block, err := armor.Decode(strings.NewReader(priv)) + if err != nil { + return nil, err + } + + e, err := openpgp.ReadEntity(packet.NewReader(block.Body)) + if err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + if err := openpgp.DetachSign(buf, e, r, nil); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// BuildAllRepositoryFiles (re)builds all repository files for every available repositories and architectures +func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { + pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) + if err != nil { + return err + } + + // 1. Delete all existing repository files + pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) + if err != nil { + return err + } + + for _, pf := range pfs { + if err := packages_service.DeletePackageFile(ctx, pf); err != nil { + return err + } + } + + // 2. (Re)Build repository files for existing packages + repositories, err := arch_model.GetRepositories(ctx, ownerID) + if err != nil { + return err + } + for _, repository := range repositories { + architectures, err := arch_model.GetArchitectures(ctx, ownerID, repository) + if err != nil { + return err + } + for _, architecture := range architectures { + if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil { + return fmt.Errorf("failed to build repository files [%s/%s]: %w", repository, architecture, err) + } + } + } + + return nil +} + +// BuildSpecificRepositoryFiles builds index files for the repository +func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, repository, architecture string) error { + pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) + if err != nil { + return err + } + + architectures := container.SetOf(architecture) + if architecture == arch_module.AnyArch { + // Update all other architectures too when updating the any index + additionalArchitectures, err := arch_model.GetArchitectures(ctx, ownerID, repository) + if err != nil { + return err + } + architectures.AddMultiple(additionalArchitectures...) + } + + for architecture := range architectures { + if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil { + return err + } + } + return nil +} + +func searchPackageFiles(ctx context.Context, ownerID int64, repository, architecture string) ([]*packages_model.PackageFile, error) { + pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: ownerID, + PackageType: packages_model.TypeArch, + Query: "%.pkg.tar.%", + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: architecture, + }, + }) + if err != nil { + return nil, err + } + return pfs, nil +} + +func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, repository, architecture string) error { + pfs, err := searchPackageFiles(ctx, ownerID, repository, architecture) + if err != nil { + return err + } + if architecture != arch_module.AnyArch { + // Add all any packages too + anyarchFiles, err := searchPackageFiles(ctx, ownerID, repository, arch_module.AnyArch) + if err != nil { + return err + } + pfs = append(pfs, anyarchFiles...) + } + + // Delete the package indices if there are no packages + if len(pfs) == 0 { + pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s", repository, architecture)) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } else if pf == nil { + return nil + } + + return packages_service.DeletePackageFile(ctx, pf) + } + + indexContent, _ := packages_module.NewHashedBuffer() + defer indexContent.Close() + + gw := gzip.NewWriter(indexContent) + tw := tar.NewWriter(gw) + + cache := make(map[int64]*packages_model.Package) + + for _, pf := range pfs { + opts := &entryOptions{ + File: pf, + } + + opts.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID) + if err != nil { + return err + } + if err := json.Unmarshal([]byte(opts.Version.MetadataJSON), &opts.VersionMetadata); err != nil { + return err + } + opts.Package = cache[opts.Version.PackageID] + if opts.Package == nil { + opts.Package, err = packages_model.GetPackageByID(ctx, opts.Version.PackageID) + if err != nil { + return err + } + cache[opts.Package.ID] = opts.Package + } + opts.Blob, err = packages_model.GetBlobByID(ctx, pf.BlobID) + if err != nil { + return err + } + + sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertySignature) + if err != nil { + return err + } + if len(sig) == 0 { + return util.ErrNotExist + } + opts.Signature = sig[0].Value + + meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyMetadata) + if err != nil { + return err + } + if len(meta) == 0 { + return util.ErrNotExist + } + if err := json.Unmarshal([]byte(meta[0].Value), &opts.FileMetadata); err != nil { + return err + } + + if err := writeFiles(tw, opts); err != nil { + return err + } + if err := writeDescription(tw, opts); err != nil { + return err + } + } + + tw.Close() + gw.Close() + + signature, err := SignData(ctx, ownerID, indexContent) + if err != nil { + return err + } + + if _, err := indexContent.Seek(0, io.SeekStart); err != nil { + return err + } + + _, err = packages_service.AddFileToPackageVersionInternal( + ctx, + repoVersion, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: IndexArchiveFilename, + CompositeKey: fmt.Sprintf("%s|%s", repository, architecture), + }, + Creator: user_model.NewGhostUser(), + Data: indexContent, + IsLead: false, + OverwriteExisting: true, + Properties: map[string]string{ + arch_module.PropertyRepository: repository, + arch_module.PropertyArchitecture: architecture, + arch_module.PropertySignature: base64.StdEncoding.EncodeToString(signature), + }, + }, + ) + return err +} + +type entryOptions struct { + Package *packages_model.Package + Version *packages_model.PackageVersion + VersionMetadata *arch_module.VersionMetadata + File *packages_model.PackageFile + FileMetadata *arch_module.FileMetadata + Blob *packages_model.PackageBlob + Signature string +} + +type keyValue struct { + Key string + Value string +} + +func writeFiles(tw *tar.Writer, opts *entryOptions) error { + return writeFields(tw, fmt.Sprintf("%s-%s/files", opts.Package.Name, opts.Version.Version), []keyValue{ + {"FILES", strings.Join(opts.FileMetadata.Files, "\n")}, + }) +} + +// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_sync.c#L562 +func writeDescription(tw *tar.Writer, opts *entryOptions) error { + return writeFields(tw, fmt.Sprintf("%s-%s/desc", opts.Package.Name, opts.Version.Version), []keyValue{ + {"FILENAME", opts.File.Name}, + {"MD5SUM", opts.Blob.HashMD5}, + {"SHA256SUM", opts.Blob.HashSHA256}, + {"PGPSIG", opts.Signature}, + {"CSIZE", fmt.Sprintf("%d", opts.Blob.Size)}, + {"ISIZE", fmt.Sprintf("%d", opts.FileMetadata.InstalledSize)}, + {"NAME", opts.Package.Name}, + {"BASE", opts.FileMetadata.Base}, + {"ARCH", opts.FileMetadata.Architecture}, + {"VERSION", opts.Version.Version}, + {"DESC", opts.VersionMetadata.Description}, + {"URL", opts.VersionMetadata.ProjectURL}, + {"LICENSE", strings.Join(opts.VersionMetadata.Licenses, "\n")}, + {"GROUPS", strings.Join(opts.FileMetadata.Groups, "\n")}, + {"BUILDDATE", fmt.Sprintf("%d", opts.FileMetadata.BuildDate)}, + {"PACKAGER", opts.FileMetadata.Packager}, + {"PROVIDES", strings.Join(opts.FileMetadata.Provides, "\n")}, + {"DEPENDS", strings.Join(opts.FileMetadata.Depends, "\n")}, + {"OPTDEPENDS", strings.Join(opts.FileMetadata.OptDepends, "\n")}, + {"MAKEDEPENDS", strings.Join(opts.FileMetadata.MakeDepends, "\n")}, + {"CHECKDEPENDS", strings.Join(opts.FileMetadata.CheckDepends, "\n")}, + {"XDATA", strings.Join(opts.FileMetadata.XData, "\n")}, + }) +} + +func writeFields(tw *tar.Writer, filename string, fields []keyValue) error { + buf := &bytes.Buffer{} + for _, kv := range fields { + if kv.Value == "" { + continue + } + fmt.Fprintf(buf, "%%%s%%\n%s\n\n", kv.Key, kv.Value) + } + + if err := tw.WriteHeader(&tar.Header{ + Name: filename, + Size: int64(buf.Len()), + Mode: int64(os.ModePerm), + }); err != nil { + return err + } + + _, err := io.Copy(tw, buf) + return err +} diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go index d7c9355da5..b7ba2b6ac4 100644 --- a/services/packages/cleanup/cleanup.go +++ b/services/packages/cleanup/cleanup.go @@ -16,6 +16,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" packages_service "code.gitea.io/gitea/services/packages" alpine_service "code.gitea.io/gitea/services/packages/alpine" + arch_service "code.gitea.io/gitea/services/packages/arch" cargo_service "code.gitea.io/gitea/services/packages/cargo" container_service "code.gitea.io/gitea/services/packages/container" debian_service "code.gitea.io/gitea/services/packages/debian" @@ -120,18 +121,29 @@ func ExecuteCleanupRules(outerCtx context.Context) error { } if anyVersionDeleted { - if pcr.Type == packages_model.TypeDebian { + switch pcr.Type { + case packages_model.TypeDebian: if err := debian_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { return fmt.Errorf("CleanupRule [%d]: debian.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } - } else if pcr.Type == packages_model.TypeAlpine { + case packages_model.TypeAlpine: if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } - } else if pcr.Type == packages_model.TypeRpm { + case packages_model.TypeRpm: if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } + case packages_model.TypeArch: + release, err := arch_service.AquireRegistryLock(ctx, pcr.OwnerID) + if err != nil { + return err + } + defer release() + + if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { + return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err) + } } } return nil diff --git a/services/packages/packages.go b/services/packages/packages.go index 95579be34b..55351afce2 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -355,6 +355,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p switch packageType { case packages_model.TypeAlpine: typeSpecificSize = setting.Packages.LimitSizeAlpine + case packages_model.TypeArch: + typeSpecificSize = setting.Packages.LimitSizeArch case packages_model.TypeCargo: typeSpecificSize = setting.Packages.LimitSizeCargo case packages_model.TypeChef: diff --git a/templates/package/content/arch.tmpl b/templates/package/content/arch.tmpl new file mode 100644 index 0000000000..1c568cbb78 --- /dev/null +++ b/templates/package/content/arch.tmpl @@ -0,0 +1,41 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} +

{{ctx.Locale.Tr "packages.installation"}}

+
+
+
+ +
[{{.PackageDescriptor.Owner.LowerName}}.{{.PackageRegistryHost}}]
+SigLevel = Optional TrustAll
+Server = 
+
+
+ +
pacman -Sy {{.PackageDescriptor.Package.LowerName}}
+
+
+ +
+
+
+ +

{{ctx.Locale.Tr "packages.arch.repository"}}

+
+ + + + + + + + + + + +
{{ctx.Locale.Tr "packages.arch.repository.repositories"}}
{{StringUtils.Join .Repositories ", "}}
{{ctx.Locale.Tr "packages.arch.repository.architectures"}}
{{StringUtils.Join .Architectures ", "}}
+
+ + {{if .PackageDescriptor.Metadata.Description}} +

{{ctx.Locale.Tr "packages.about"}}

+
{{.PackageDescriptor.Metadata.Description}}
+ {{end}} +{{end}} diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index 138fedecb3..aaed25bfbd 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -5,13 +5,13 @@
{{if eq .PackageDescriptor.Metadata.Type "helm"}} -
helm pull oci://{{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}} --version {{.PackageDescriptor.Version.LowerVersion}}
+
helm pull oci://{{.PackageRegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}} --version {{.PackageDescriptor.Version.LowerVersion}}
{{else}} {{$separator := ":"}} {{if not .PackageDescriptor.Metadata.IsTagged}} {{$separator = "@"}} {{end}} -
docker pull {{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}
+
docker pull {{.PackageRegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}
{{end}}
diff --git a/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl index 3e7f10f66a..c9174948b1 100644 --- a/templates/package/metadata/alpine.tmpl +++ b/templates/package/metadata/alpine.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "alpine"}} - {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl new file mode 100644 index 0000000000..2aea036ec2 --- /dev/null +++ b/templates/package/metadata/arch.tmpl @@ -0,0 +1,4 @@ +{{if eq .PackageDescriptor.Package.Type "arch"}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} +{{end}} diff --git a/templates/package/metadata/cargo.tmpl b/templates/package/metadata/cargo.tmpl index 5ad3c20a93..f7dd887a24 100644 --- a/templates/package/metadata/cargo.tmpl +++ b/templates/package/metadata/cargo.tmpl @@ -1,7 +1,7 @@ {{if eq .PackageDescriptor.Package.Type "cargo"}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/chef.tmpl b/templates/package/metadata/chef.tmpl index 23a9ce3ec0..6bf606ca48 100644 --- a/templates/package/metadata/chef.tmpl +++ b/templates/package/metadata/chef.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "chef"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/composer.tmpl b/templates/package/metadata/composer.tmpl index 0f6ff9d6f2..e69e91745f 100644 --- a/templates/package/metadata/composer.tmpl +++ b/templates/package/metadata/composer.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "composer"}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}
{{end}} - {{if .PackageDescriptor.Metadata.Homepage}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.Homepage}}{{end}} + {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.}}
{{end}} {{end}} diff --git a/templates/package/metadata/conan.tmpl b/templates/package/metadata/conan.tmpl index 4e05ec2587..8b15375553 100644 --- a/templates/package/metadata/conan.tmpl +++ b/templates/package/metadata/conan.tmpl @@ -1,6 +1,6 @@ {{if eq .PackageDescriptor.Package.Type "conan"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/conda.tmpl b/templates/package/metadata/conda.tmpl index 3628686e13..4add9453fa 100644 --- a/templates/package/metadata/conda.tmpl +++ b/templates/package/metadata/conda.tmpl @@ -1,6 +1,6 @@ {{if eq .PackageDescriptor.Package.Type "conda"}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} {{end}} diff --git a/templates/package/metadata/container.tmpl b/templates/package/metadata/container.tmpl index f5abb7ef6e..ecc17964d7 100644 --- a/templates/package/metadata/container.tmpl +++ b/templates/package/metadata/container.tmpl @@ -1,9 +1,9 @@ {{if eq .PackageDescriptor.Package.Type "container"}} -
{{svg "octicon-package" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Type.Name}}
- {{if .PackageDescriptor.Metadata.Platform}}
{{svg "octicon-cpu" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Platform}}
{{end}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Licenses}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} +
{{svg "octicon-package"}} {{.PackageDescriptor.Metadata.Type.Name}}
+ {{if .PackageDescriptor.Metadata.Platform}}
{{svg "octicon-cpu"}} {{.PackageDescriptor.Metadata.Platform}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.Licenses}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} {{end}} diff --git a/templates/package/metadata/cran.tmpl b/templates/package/metadata/cran.tmpl index 1d5a11e196..3ada7ac743 100644 --- a/templates/package/metadata/cran.tmpl +++ b/templates/package/metadata/cran.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "cran"}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}
{{end}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "mr-3"}} {{.}}
{{end}} - {{range .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.ProjectURL}}{{end}} {{end}} diff --git a/templates/package/metadata/debian.tmpl b/templates/package/metadata/debian.tmpl index 3cd845c9fe..d35e8b00da 100644 --- a/templates/package/metadata/debian.tmpl +++ b/templates/package/metadata/debian.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "debian"}} - {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Maintainer}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} {{end}} diff --git a/templates/package/metadata/helm.tmpl b/templates/package/metadata/helm.tmpl index 50ea484999..b3b3f348cf 100644 --- a/templates/package/metadata/helm.tmpl +++ b/templates/package/metadata/helm.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "helm"}} - {{range .PackageDescriptor.Metadata.Maintainers}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}
{{end}} - {{if .PackageDescriptor.Metadata.Home}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{range .PackageDescriptor.Metadata.Maintainers}}
{{svg "octicon-person"}} {{.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.Home}}{{end}} {{end}} diff --git a/templates/package/metadata/maven.tmpl b/templates/package/metadata/maven.tmpl index 36412723d2..33662be7ca 100644 --- a/templates/package/metadata/maven.tmpl +++ b/templates/package/metadata/maven.tmpl @@ -1,8 +1,8 @@ {{if and (eq .PackageDescriptor.Package.Type "maven") (not .PackageDescriptor.Metadata)}} -
{{svg "octicon-note" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.no_metadata"}}
+
{{svg "octicon-note"}} {{ctx.Locale.Tr "packages.no_metadata"}}
{{end}} {{if and (eq .PackageDescriptor.Package.Type "maven") .PackageDescriptor.Metadata}} - {{if .PackageDescriptor.Metadata.Name}}
{{svg "octicon-note" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Name}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.Name}}
{{svg "octicon-note"}} {{.PackageDescriptor.Metadata.Name}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} {{end}} diff --git a/templates/package/metadata/npm.tmpl b/templates/package/metadata/npm.tmpl index df37504e37..ff245f2b03 100644 --- a/templates/package/metadata/npm.tmpl +++ b/templates/package/metadata/npm.tmpl @@ -1,8 +1,8 @@ {{if eq .PackageDescriptor.Package.Type "npm"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{range .PackageDescriptor.VersionProperties}} - {{if eq .Name "npm.tag"}}
{{svg "octicon-versions" 16 "tw-mr-2"}} {{.Value}}
{{end}} + {{if eq .Name "npm.tag"}}
{{svg "octicon-versions"}} {{.Value}}
{{end}} {{end}} {{end}} diff --git a/templates/package/metadata/nuget.tmpl b/templates/package/metadata/nuget.tmpl index 5534577bd2..2d18528f85 100644 --- a/templates/package/metadata/nuget.tmpl +++ b/templates/package/metadata/nuget.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "nuget"}} - {{if .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Authors}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Authors}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/pub.tmpl b/templates/package/metadata/pub.tmpl index 16f7cec370..e54207c4c6 100644 --- a/templates/package/metadata/pub.tmpl +++ b/templates/package/metadata/pub.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "pub"}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.documentation_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}{{end}} {{end}} diff --git a/templates/package/metadata/pypi.tmpl b/templates/package/metadata/pypi.tmpl index 3d9b213907..9dfac07cbf 100644 --- a/templates/package/metadata/pypi.tmpl +++ b/templates/package/metadata/pypi.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "pypi"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/rpm.tmpl b/templates/package/metadata/rpm.tmpl index eda8a489f3..65093933a9 100644 --- a/templates/package/metadata/rpm.tmpl +++ b/templates/package/metadata/rpm.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "rpm"}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.License}}
{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}
{{end}} {{end}} diff --git a/templates/package/metadata/rubygems.tmpl b/templates/package/metadata/rubygems.tmpl index 9b11287691..04fc3695ab 100644 --- a/templates/package/metadata/rubygems.tmpl +++ b/templates/package/metadata/rubygems.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "rubygems"}} - {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.Authors}}
{{svg "octicon-person"}} {{.}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}} {{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} {{end}} diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl index fdffb6dede..fe28759de3 100644 --- a/templates/package/metadata/swift.tmpl +++ b/templates/package/metadata/swift.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "swift"}} - {{if .PackageDescriptor.Metadata.Author.String}}
{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Author.String}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/metadata/vagrant.tmpl b/templates/package/metadata/vagrant.tmpl index 4628a2dcbb..795ab33da9 100644 --- a/templates/package/metadata/vagrant.tmpl +++ b/templates/package/metadata/vagrant.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "vagrant"}} - {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.repository_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.Author}}
{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}{{end}} {{end}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 0f06d7afbd..9e92207466 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -1,12 +1,10 @@ {{template "base/head" .}} -
+
{{template "shared/user/org_profile_avatar" .}}
{{template "user/overview/header" .}}
-
-

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})

-
+

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})

{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}} {{if .HasRepositoryAccess}} @@ -19,6 +17,7 @@
{{template "package/content/alpine" .}} + {{template "package/content/arch" .}} {{template "package/content/cargo" .}} {{template "package/content/chef" .}} {{template "package/content/composer" .}} @@ -42,14 +41,15 @@
{{ctx.Locale.Tr "packages.details"}} -
-
{{svg .PackageDescriptor.Package.Type.SVGName 16 "tw-mr-2"}} {{.PackageDescriptor.Package.Type.Name}}
+
+
{{svg .PackageDescriptor.Package.Type.SVGName}} {{.PackageDescriptor.Package.Type.Name}}
{{if .HasRepositoryAccess}} -
{{svg "octicon-repo" 16 "tw-mr-2"}} {{.PackageDescriptor.Repository.FullName}}
+ {{end}} -
{{svg "octicon-calendar" 16 "tw-mr-2"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
-
{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}
+
{{svg "octicon-calendar"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
+
{{svg "octicon-download"}} {{.PackageDescriptor.Version.DownloadCount}}
{{template "package/metadata/alpine" .}} + {{template "package/metadata/arch" .}} {{template "package/metadata/cargo" .}} {{template "package/metadata/chef" .}} {{template "package/metadata/composer" .}} @@ -70,7 +70,7 @@ {{template "package/metadata/swift" .}} {{template "package/metadata/vagrant" .}} {{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}} -
{{svg "octicon-database" 16 "tw-mr-2"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}
+
{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}
{{end}}
{{if not (eq .PackageDescriptor.Package.Type "container")}} @@ -98,12 +98,12 @@
{{if or .CanWritePackages .HasRepositoryAccess}}
-
+
{{if .HasRepositoryAccess}} -
{{svg "octicon-issue-opened" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues"}}
+
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}}
{{end}} {{if .CanWritePackages}} -
{{svg "octicon-tools" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.settings"}}
+
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
{{end}}
{{end}} diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go new file mode 100644 index 0000000000..9c7a9dd19d --- /dev/null +++ b/tests/integration/api_packages_arch_test.go @@ -0,0 +1,302 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + arch_module "code.gitea.io/gitea/modules/packages/arch" + arch_service "code.gitea.io/gitea/services/packages/arch" + "code.gitea.io/gitea/tests" + + "github.com/klauspost/compress/zstd" + "github.com/stretchr/testify/assert" + "github.com/ulikunitz/xz" +) + +func TestPackageArch(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + packageName := "gitea-test" + packageVersion := "1.4.1-r3" + + createPackage := func(compression, name, version, architecture string) []byte { + var buf bytes.Buffer + var cw io.WriteCloser + switch compression { + case "zst": + cw, _ = zstd.NewWriter(&buf) + case "xz": + cw, _ = xz.NewWriter(&buf) + case "gz": + cw = gzip.NewWriter(&buf) + } + tw := tar.NewWriter(cw) + + info := []byte(`pkgname = ` + name + ` +pkgbase = ` + name + ` +pkgver = ` + version + ` +pkgdesc = Description +# comment +builddate = 1678834800 +size = 8 +arch = ` + architecture + ` +license = MIT`) + + hdr := &tar.Header{ + Name: ".PKGINFO", + Mode: 0o600, + Size: int64(len(info)), + } + tw.WriteHeader(hdr) + tw.Write(info) + + for _, file := range []string{"etc/dummy", "opt/file/bin"} { + hdr := &tar.Header{ + Name: file, + Mode: 0o600, + Size: 4, + } + tw.WriteHeader(hdr) + tw.Write([]byte("test")) + } + + tw.Close() + cw.Close() + + return buf.Bytes() + } + + compressions := []string{"gz", "xz", "zst"} + repositories := []string{"main", "testing", "with/slash", ""} + + rootURL := fmt.Sprintf("/api/packages/%s/arch", user.Name) + + t.Run("RepositoryKey", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", rootURL+"/repository.key") + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type")) + assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----") + }) + + contentAarch64Gz := createPackage("gz", packageName, packageVersion, "aarch64") + for _, compression := range compressions { + contentAarch64 := createPackage(compression, packageName, packageVersion, "aarch64") + contentAny := createPackage(compression, packageName+"_"+arch_module.AnyArch, packageVersion, arch_module.AnyArch) + + for _, repository := range repositories { + t.Run(fmt.Sprintf("[%s,%s]", repository, compression), func(t *testing.T) { + t.Run("Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + uploadURL := fmt.Sprintf("%s/%s", rootURL, repository) + + req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeArch) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) + assert.NoError(t, err) + assert.Nil(t, pd.SemVer) + assert.IsType(t, &arch_module.VersionMetadata{}, pd.Metadata) + assert.Equal(t, packageName, pd.Package.Name) + assert.Equal(t, packageVersion, pd.Version.Version) + + pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) + assert.NoError(t, err) + assert.NotEmpty(t, pfs) + assert.Condition(t, func() bool { + seen := false + expectedFilename := fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression) + expectedCompositeKey := fmt.Sprintf("%s|aarch64", repository) + for _, pf := range pfs { + if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey { + if seen { + return false + } + seen = true + + assert.True(t, pf.IsLead) + + pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID) + assert.NoError(t, err) + + for _, pfp := range pfps { + switch pfp.Name { + case arch_module.PropertyRepository: + assert.Equal(t, repository, pfp.Value) + case arch_module.PropertyArchitecture: + assert.Equal(t, "aarch64", pfp.Value) + } + } + } + } + return seen + }) + + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusConflict) + + // Add same package with different compression leads to conflict + req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64Gz)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusConflict) + }) + + readIndexContent := func(r io.Reader) (map[string]string, error) { + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + + content := make(map[string]string) + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + buf, err := io.ReadAll(tr) + if err != nil { + return nil, err + } + + content[hd.Name] = string(buf) + } + + return content, nil + } + + t.Run("Index", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + resp := MakeRequest(t, req, http.StatusOK) + + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) + + desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%FILENAME%\n"+fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression)+"\n\n") + assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") + assert.Contains(t, desc, "%VERSION%\n"+packageVersion+"\n\n") + assert.Contains(t, desc, "%ARCH%\naarch64\n") + assert.NotContains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") + assert.Contains(t, desc, "%LICENSE%\nMIT\n") + + files, has := content[fmt.Sprintf("%s-%s/files", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, files, "%FILES%\netc/dummy\nopt/file/bin\n\n") + + for _, indexFile := range []string{ + arch_service.IndexArchiveFilename, + arch_service.IndexArchiveFilename + ".tar.gz", + "index.db", + "index.db.tar.gz", + "index.files", + "index.files.tar.gz", + } { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, indexFile)) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s.sig", rootURL, repository, indexFile)) + MakeRequest(t, req, http.StatusOK) + } + }) + + t.Run("Download", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s", rootURL, repository, packageName, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s.sig", rootURL, repository, packageName, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) + }) + + t.Run("Any", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s", rootURL, repository), bytes.NewReader(contentAny)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + resp := MakeRequest(t, req, http.StatusOK) + + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) + + desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n") + assert.Contains(t, desc, "%ARCH%\naarch64\n") + + desc, has = content[fmt.Sprintf("%s-%s/desc", packageName+"_"+arch_module.AnyArch, packageVersion)] + assert.True(t, has) + assert.Contains(t, desc, "%NAME%\n"+packageName+"_any\n\n") + assert.Contains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n") + + // "any" architecture package should be available with every architecture requested + for _, arch := range []string{arch_module.AnyArch, "aarch64", "myarch"} { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s-%s-any.pkg.tar.%s", rootURL, repository, arch, packageName+"_"+arch_module.AnyArch, packageVersion, compression)) + MakeRequest(t, req, http.StatusOK) + } + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/any", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + }) + + t.Run("Delete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + + // Deleting the last file of an architecture should remove that index + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename)) + MakeRequest(t, req, http.StatusNotFound) + }) + }) + } + } +} diff --git a/web_src/css/base.css b/web_src/css/base.css index babbf4c89d..8f5ef51c4a 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1386,7 +1386,7 @@ table th[data-sortt-desc] .svg { .flex-text-block { display: flex; align-items: center; - gap: .25rem; + gap: .5rem; min-width: 0; } diff --git a/web_src/svg/gitea-arch.svg b/web_src/svg/gitea-arch.svg new file mode 100644 index 0000000000..ba8254d804 --- /dev/null +++ b/web_src/svg/gitea-arch.svg @@ -0,0 +1 @@ + \ No newline at end of file