diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index e5d5ca669f..26c512b6f6 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -175,14 +175,15 @@ RUN_USER = ; git
;; The port number the builtin SSH server should listen on, defaults to SSH_PORT
;SSH_LISTEN_PORT =
;;
-;; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'.
+;; Root path of SSH user directory for the system's standalone SSH server if Gitea is not using its builtin SSH server.
+;; Default is the '.ssh' directory in the run user's home directory.
;SSH_ROOT_PATH =
;;
-;; Gitea will create a authorized_keys file by default when it is not using the internal ssh server
+;; Gitea will create an authorized_keys file by default when it is not using the builtin SSH server
;; If you intend to use the AuthorizedKeysCommand functionality then you should turn this off.
;SSH_CREATE_AUTHORIZED_KEYS_FILE = true
;;
-;; Gitea will create a authorized_principals file by default when it is not using the internal ssh server
+;; Gitea will create an authorized_principals file by default when it is not using the builtin SSH server
;; If you intend to use the AuthorizedPrincipalsCommand functionality then you should turn this off.
;SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE = true
;;
@@ -1178,16 +1179,16 @@ LEVEL = Info
;[repository.release]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
+;; Comma-separated list of allowed release attachment file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
;ALLOWED_TYPES =
;;
;; Number of releases that are displayed on release page
;DEFAULT_PAGING_NUM = 10
;;
-;; Max size of each file in megabytes. Defaults to 2GB
+;; Max size of each release attachment file in megabytes. Defaults to 2GB
;FILE_MAX_SIZE = 2048
;;
-;; Max number of files per upload. Defaults to 5
+;; Max number of release attachment files per upload. Defaults to 5
;MAX_FILES = 5
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1994,16 +1995,18 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
-;; Whether issue and pull request attachments are enabled. Defaults to `true`
+;; Whether issue, pull-request and release attachments are enabled. Defaults to `true`
+;; ALLOWED_TYPES/MAX_SIZE/MAX_FILES in this section only affect issue and pull-request attachments, not release attachments.
+;; Release attachment has its own config options in [repository.release] section.
;ENABLED = true
;;
-;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
+;; Comma-separated list of allowed issue/pull-request attachment file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip
;;
-;; Max size of each file. Defaults to 100MB
+;; Max size of each issue/pull-request attachment file. Defaults to 100MB
;MAX_SIZE = 100
;;
-;; Max number of files per upload. Defaults to 5
+;; Max number of issue/pull-request attachment files per upload. Defaults to 5
;MAX_FILES = 5
;;
;; Storage type for attachments, `local` for local disk or `minio` for s3 compatible
diff --git a/docker/root/etc/s6/openssh/finish b/docker/root/etc/s6/openssh/finish
index 06bd986563..8e89b35c51 100755
--- a/docker/root/etc/s6/openssh/finish
+++ b/docker/root/etc/s6/openssh/finish
@@ -1,2 +1,8 @@
#!/bin/bash
+# $1 = exit code of the run script, $2 = signal
+if [ "$1" -ne 0 ]; then
+ # avoid immediately restarting the sshd service, which may cause CPU 100% if the error (permission, configuration) is not fixed
+ echo "openssh failed with exit code $1 - waiting a short delay before attempting a restart"
+ sleep 3
+fi
exit 0
diff --git a/modules/packages/rpm/metadata.go b/modules/packages/rpm/metadata.go
index f4f78c2cab..d8ac7ea75f 100644
--- a/modules/packages/rpm/metadata.go
+++ b/modules/packages/rpm/metadata.go
@@ -46,10 +46,11 @@ type Package struct {
}
type VersionMetadata struct {
- License string `json:"license,omitempty"`
- ProjectURL string `json:"project_url,omitempty"`
- Summary string `json:"summary,omitempty"`
- Description string `json:"description,omitempty"`
+ License string `json:"license,omitempty"`
+ ProjectURL string `json:"project_url,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Description string `json:"description,omitempty"`
+ Updates []*Update `json:"updates,omitempty"`
}
type FileMetadata struct {
@@ -296,3 +297,43 @@ func getChangelogs(h *rpmutils.RpmHeader) []*Changelog {
}
return changelogs
}
+
+type DateAttr struct {
+ Date string `xml:"date,attr" json:"date"`
+}
+
+type Update struct {
+ From string `xml:"from,attr" json:"from"`
+ Status string `xml:"status,attr" json:"status"`
+ Type string `xml:"type,attr" json:"type"`
+ Version string `xml:"version,attr" json:"version"`
+ ID string `xml:"id" json:"id"`
+ Title string `xml:"title" json:"title"`
+ Severity string `xml:"severity" json:"severity"`
+ Description string `xml:"description" json:"description"`
+ Issued *DateAttr `xml:"issued" json:"issued"`
+ Updated *DateAttr `xml:"updated" json:"updated"`
+ References []*Reference `xml:"references>reference" json:"references"`
+ PkgList []*Collection `xml:"pkglist>collection" json:"pkg_list"`
+}
+
+type Reference struct {
+ Href string `xml:"href,attr" json:"href"`
+ ID string `xml:"id,attr" json:"id"`
+ Title string `xml:"title,attr" json:"title"`
+ Type string `xml:"type,attr" json:"type"`
+}
+
+type Collection struct {
+ Short string `xml:"short,attr" json:"short"`
+ Packages []*UpdatePackage `xml:"package" json:"packages"`
+}
+
+type UpdatePackage struct {
+ Arch string `xml:"arch,attr" json:"arch"`
+ Name string `xml:"name,attr" json:"name"`
+ Release string `xml:"release,attr" json:"release"`
+ Src string `xml:"src,attr" json:"src"`
+ Version string `xml:"version,attr" json:"version"`
+ Filename string `xml:"filename" json:"filename"`
+}
diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json
index aca0f31449..85a6922072 100644
--- a/options/locale/locale_ga-IE.json
+++ b/options/locale/locale_ga-IE.json
@@ -173,6 +173,8 @@
"search.org_kind": "Cuardaigh eagraíochtaí…",
"search.team_kind": "Cuardaigh foirne…",
"search.code_kind": "Cuardaigh cód…",
+ "search.code_empty": "Tosaigh cuardach cóid.",
+ "search.code_empty_description": "Cuir isteach eochairfhocal chun cuardach a dhéanamh ar fud an chóid.",
"search.code_search_unavailable": "Níl cuardach cód ar fáil faoi láthair. Déan teagmháil le riarthóir an láithreáin.",
"search.code_search_by_git_grep": "Soláthraíonn “git grep” torthaí cuardaigh cód reatha. D'fhéadfadh torthaí níos fearr a bheith ann má chuireann riarthóir an láithreáin ar chumas Innéacsaithe",
"search.package_kind": "Cuardaigh pacáistí…",
@@ -1216,7 +1218,7 @@
"repo.ambiguous_runes_description": "Tá carachtair Unicode sa chomhad seo a d'fhéadfadh a bheith mearbhall le carachtair eile. Má cheapann tú go bhfuil sé seo d'aon ghnó, is féidir leat neamhaird a dhéanamh go sábháilte don rabhadh seo Úsáid an cnaipe Escape chun iad a nochtadh. ",
"repo.invisible_runes_line": "Tá carachtair unicode dofheicthe ag an líne seo ",
"repo.ambiguous_runes_line": "Tá carachtair unicode débhríoch ag an líne seo ",
- "repo.ambiguous_character": "Is féidir %[1]c [U+%04[1]X] a mheascadh le %[2]c [U+%04[2]X]",
+ "repo.ambiguous_character": "Is féidir mearbhall a dhéanamh idir %[1]s agus %[2]s",
"repo.escape_control_characters": "Éalú",
"repo.unescape_control_characters": "Dí-Éalú",
"repo.file_copy_permalink": "Cóipeáil Buan-nasc",
@@ -1357,10 +1359,13 @@
"repo.projects.desc": "Saincheisteanna a bhainistiú agus tionscadail a tharraingt isteach.",
"repo.projects.description": "Cur síos (roghnach)",
"repo.projects.description_placeholder": "Cur síos",
+ "repo.projects.empty": "Gan aon tionscadail go fóill.",
+ "repo.projects.empty_description": "Cruthaigh tionscadal chun saincheisteanna agus iarratais tarraingthe a chomhordú.",
"repo.projects.create": "Cruthaigh Tionscadal",
"repo.projects.title": "Teideal",
"repo.projects.new": "Tionscadal Nua",
"repo.projects.new_subheader": "Déan do chuid oibre a chomhordú, a rianú agus a nuashonrú in aon áit amháin, ionas go bhfanann na tionscadail trédhearcach agus de réir sceidil.",
+ "repo.projects.no_results": "Níl aon tionscadail a oireann do do chuardach.",
"repo.projects.create_success": "Tá an tionscadal \"%s\" cruthaithe.",
"repo.projects.deletion": "Scrios tionscadal",
"repo.projects.deletion_desc": "Má scriostar tionscadal, bainfear de gach saincheist a bhaineann leis é. Lean ort?",
@@ -2722,6 +2727,8 @@
"org.members": "Comhaltaí",
"org.teams": "Foirne",
"org.code": "Cód",
+ "org.repos.empty": "Gan aon stórtha fós.",
+ "org.repos.empty_description": "Cruthaigh stórlann chun cód a roinnt leis an eagraíocht.",
"org.lower_members": "comhaltaí",
"org.lower_repositories": "stórais",
"org.create_new_team": "Foireann Nua",
@@ -2858,6 +2865,8 @@
"org.worktime.date_range_end": "Dáta deiridh",
"org.worktime.query": "Ceist",
"org.worktime.time": "Am",
+ "org.worktime.empty": "Gan aon sonraí ama oibre go fóill.",
+ "org.worktime.empty_description": "Coigeartaigh an raon dáta chun an t-am rianaithe a fheiceáil.",
"org.worktime.by_repositories": "De réir stórtha",
"org.worktime.by_milestones": "De réir clocha míle",
"org.worktime.by_members": "Ag baill",
@@ -3506,6 +3515,7 @@
"packages.dependencies": "Spleithiúlachtaí",
"packages.keywords": "Eochairfhocail",
"packages.details": "Sonraí",
+ "packages.name": "Ainm an Phacáiste",
"packages.details.author": "Údar",
"packages.details.project_site": "Suíomh an Tionscadail",
"packages.details.repository_site": "Suíomh Stóras",
@@ -3601,6 +3611,18 @@
"packages.swift.registry": "Socraigh an clárlann seo ón líne ordaithe:",
"packages.swift.install": "Cuir an pacáiste i do chomhad Package.swift:",
"packages.swift.install2": "agus reáchtáil an t-ordú seo a leanas:",
+ "packages.terraform.install": "Socraigh do stát chun an cúltaca HTTP a úsáid",
+ "packages.terraform.install2": "agus reáchtáil an t-ordú seo a leanas:",
+ "packages.terraform.lock_status": "Stádas Glas",
+ "packages.terraform.locked_by": "Glasáilte ag %s",
+ "packages.terraform.unlocked": "Díghlasáilte",
+ "packages.terraform.lock": "Glas",
+ "packages.terraform.unlock": "Díghlasáil",
+ "packages.terraform.lock.success": "Glasáladh staid Terraform go rathúil.",
+ "packages.terraform.unlock.success": "Díghlasáladh staid Terraform go rathúil.",
+ "packages.terraform.lock.error.already_locked": "Tá staid Terraform faoi ghlas cheana féin.",
+ "packages.terraform.delete.locked": "Tá staid an Terraform faoi ghlas agus ní féidir é a scriosadh.",
+ "packages.terraform.delete.latest": "Ní féidir an leagan is déanaí de staid Terraform a scriosadh.",
"packages.vagrant.install": "Chun bosca Vagrant a chur leis, reáchtáil an t-ordú seo a leanas:",
"packages.settings.link": "Nasc an pacáiste seo le stóras",
"packages.settings.link.description": "Má nascann tú pacáiste le stórlann, beidh an pacáiste le feiceáil i liosta pacáistí an stórlainne. Ní féidir ach stórlanna faoin úinéir céanna a nascadh. Má fhágtar an réimse folamh, bainfear an nasc.",
@@ -3614,8 +3636,13 @@
"packages.settings.delete": "Scrios pacáiste",
"packages.settings.delete.description": "Tá pacáiste a scriosadh buan agus ní féidir é a chur ar ais.",
"packages.settings.delete.notice": "Tá tú ar tí %s (%s) a scriosadh. Tá an oibríocht seo dochúlaithe, an bhfuil tú cinnte?",
+ "packages.settings.delete.notice.package": "Tá tú ar tí %s agus a leaganacha uile a scriosadh. Ní féidir an oibríocht seo a aisiompú, an bhfuil tú cinnte?",
"packages.settings.delete.success": "Tá an pacáiste scriosta.",
+ "packages.settings.delete.version.success": "Scriosadh an leagan pacáiste.",
"packages.settings.delete.error": "Theip ar an pacáiste a scriosadh.",
+ "packages.settings.delete.version": "Scrios an leagan",
+ "packages.settings.delete.confirm": "Cuir isteach ainm an phacáiste le deimhniú",
+ "packages.settings.delete.invalid_package_name": "Tá ainm an phacáiste a chuir tú isteach mícheart.",
"packages.owner.settings.cargo.title": "Innéacs Clárlann Lasta",
"packages.owner.settings.cargo.initialize": "Innéacs a chur i dtosach",
"packages.owner.settings.cargo.initialize.description": "Tá gá le stóras innéacs speisialta Git chun an clárlann Cargo a úsáid. Tríd an rogha seo, cruthófar an stóras (nó athchruthófar é) agus cumrófar é go huathoibríoch.",
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index 876d6aaa62..9283eb8a5e 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -473,6 +473,7 @@ func CommonRoutes() *web.Router {
g.MatchPath("HEAD", "//repodata/", rpm.CheckRepositoryFileExistence)
g.MatchPath("GET", "//repodata/", rpm.GetRepositoryFile)
g.MatchPath("PUT", "//upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile)
+ g.MatchPath("POST", "//package///errata", reqPackageAccess(perm.AccessModeWrite), rpm.UploadErrata)
// this URL pattern is only used internally in the RPM index, it is generated by us, the filename part is not really used (can be anything)
g.MatchPath("HEAD,GET", "//package////", rpm.DownloadPackageFile)
g.MatchPath("HEAD,GET", "//package///", rpm.DownloadPackageFile)
diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go
index 4447a0c3cf..51cedd2a9f 100644
--- a/routers/api/packages/rpm/rpm.go
+++ b/routers/api/packages/rpm/rpm.go
@@ -10,6 +10,7 @@ import (
"io"
"net/http"
"strings"
+ "time"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
@@ -316,3 +317,146 @@ func DeletePackageFile(webctx *context.Context) {
webctx.Status(http.StatusNoContent)
}
+
+// UploadErrata handles uploading errata information for a package version
+func UploadErrata(ctx *context.Context) {
+ name := ctx.PathParam("name")
+ version := ctx.PathParam("version")
+ group := ctx.PathParam("group")
+
+ var updates []*rpm_module.Update
+ if err := json.NewDecoder(ctx.Req.Body).Decode(&updates); err != nil {
+ apiError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ pv, err := packages_model.GetVersionByNameAndVersion(ctx,
+ ctx.Package.Owner.ID,
+ packages_model.TypeRpm,
+ name,
+ version,
+ )
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ var vm *rpm_module.VersionMetadata
+ if pv.MetadataJSON != "" {
+ if err := json.Unmarshal([]byte(pv.MetadataJSON), &vm); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ } else {
+ vm = &rpm_module.VersionMetadata{}
+ }
+
+ now := time.Now().Format("2006-01-02 15:04:05")
+ for _, u := range updates {
+ if u == nil {
+ continue
+ }
+
+ // Sanitize to remove nil elements from JSON payload
+ var cleanPkgList []*rpm_module.Collection
+ for _, coll := range u.PkgList {
+ if coll == nil {
+ continue
+ }
+ var cleanPackages []*rpm_module.UpdatePackage
+ for _, pkg := range coll.Packages {
+ if pkg == nil {
+ continue
+ }
+ cleanPackages = append(cleanPackages, pkg)
+ }
+ coll.Packages = cleanPackages
+ cleanPkgList = append(cleanPkgList, coll)
+ }
+ u.PkgList = cleanPkgList
+
+ found := false
+ for i, existing := range vm.Updates {
+ if existing.ID == u.ID {
+ // Merge PkgList with deduplication
+ for _, newColl := range u.PkgList {
+ if newColl == nil {
+ continue
+ }
+ collFound := false
+ for j, existingColl := range existing.PkgList {
+ if existingColl.Short == newColl.Short {
+ // Merge packages
+ for _, newPkg := range newColl.Packages {
+ if newPkg == nil {
+ continue
+ }
+ pkgFound := false
+ for _, existingPkg := range existingColl.Packages {
+ if existingPkg.Name == newPkg.Name &&
+ existingPkg.Version == newPkg.Version &&
+ existingPkg.Release == newPkg.Release &&
+ existingPkg.Arch == newPkg.Arch {
+ pkgFound = true
+ break
+ }
+ }
+ if !pkgFound {
+ vm.Updates[i].PkgList[j].Packages = append(vm.Updates[i].PkgList[j].Packages, newPkg)
+ }
+ }
+ collFound = true
+ break
+ }
+ }
+ if !collFound {
+ vm.Updates[i].PkgList = append(vm.Updates[i].PkgList, newColl)
+ }
+ }
+ vm.Updates[i].From = u.From
+ vm.Updates[i].Status = u.Status
+ vm.Updates[i].Type = u.Type
+ vm.Updates[i].Version = u.Version
+ vm.Updates[i].Title = u.Title
+ vm.Updates[i].Severity = u.Severity
+ vm.Updates[i].Description = u.Description
+ vm.Updates[i].References = u.References
+ vm.Updates[i].Updated = &rpm_module.DateAttr{Date: now}
+ found = true
+ break
+ }
+ }
+ if !found {
+ if u.Issued == nil {
+ u.Issued = &rpm_module.DateAttr{Date: now}
+ }
+ if u.Updated == nil {
+ u.Updated = &rpm_module.DateAttr{Date: now}
+ }
+ vm.Updates = append(vm.Updates, u)
+ }
+ }
+
+ vmBytes, err := json.Marshal(vm)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ pv.MetadataJSON = string(vmBytes)
+ if err := packages_model.UpdateVersion(ctx, pv); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if err := rpm_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.Status(http.StatusOK)
+}
diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go
index bfe9c92f1c..b64f713401 100644
--- a/routers/api/v1/repo/issue_attachment.go
+++ b/routers/api/v1/repo/issue_attachment.go
@@ -186,7 +186,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
}
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
- attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{
+ attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
index 3227f5ddee..30b79a1d54 100644
--- a/routers/api/v1/repo/issue_comment_attachment.go
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -193,7 +193,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
}
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
- attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{
+ attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go
index 19075961f3..c0bf89a9d0 100644
--- a/routers/api/v1/repo/release_attachment.go
+++ b/routers/api/v1/repo/release_attachment.go
@@ -242,7 +242,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
}
// Create a new attachment and save the file
- attach, err := attachment_service.UploadAttachmentReleaseSizeLimit(ctx, uploaderFile, setting.Repository.Release.AllowedTypes, &repo_model.Attachment{
+ attach, err := attachment_service.UploadAttachmentForRelease(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index 8b35f52ed6..bb2002521c 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -23,16 +23,16 @@ import (
// UploadIssueAttachment response for Issue/PR attachments
func UploadIssueAttachment(ctx *context.Context) {
- uploadAttachment(ctx, ctx.Repo.Repository.ID, setting.Attachment.AllowedTypes)
+ uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForIssue)
}
// UploadReleaseAttachment response for uploading release attachments
func UploadReleaseAttachment(ctx *context.Context) {
- uploadAttachment(ctx, ctx.Repo.Repository.ID, setting.Repository.Release.AllowedTypes)
+ uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForRelease)
}
// UploadAttachment response for uploading attachments
-func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
+func uploadAttachment(ctx *context.Context, repoID int64, uploadFunc attachment.UploadAttachmentFunc) {
if !setting.Attachment.Enabled {
ctx.HTTPError(http.StatusNotFound, "attachment is not enabled")
return
@@ -46,7 +46,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
defer file.Close()
uploaderFile := attachment.NewLimitedUploaderKnownSize(file, header.Size)
- attach, err := attachment.UploadAttachmentReleaseSizeLimit(ctx, uploaderFile, allowedTypes, &repo_model.Attachment{
+ attach, err := uploadFunc(ctx, uploaderFile, &repo_model.Attachment{
Name: header.Filename,
UploaderID: ctx.Doer.ID,
RepoID: repoID,
@@ -56,7 +56,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
ctx.HTTPError(http.StatusBadRequest, err.Error())
return
}
- ctx.ServerError("UploadAttachmentReleaseSizeLimit", err)
+ ctx.ServerError("uploadAttachment(uploadFunc)", err)
return
}
@@ -119,7 +119,7 @@ func DeleteAttachment(ctx *context.Context) {
})
}
-// GetAttachment serve attachments with the given UUID
+// ServeAttachment serve attachments with the given UUID
func ServeAttachment(ctx *context.Context, uuid string) {
attach, err := repo_model.GetAttachmentByUUID(ctx, uuid)
if err != nil {
diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go
index ac30b678c3..860dcd7442 100644
--- a/routers/web/repo/issue_comment.go
+++ b/routers/web/repo/issue_comment.go
@@ -57,22 +57,26 @@ func NewComment(ctx *context.Context) {
return
}
+ redirect := fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, issueType, issue.Index)
attachments := util.Iif(setting.Attachment.Enabled, form.Files, nil)
- // Can allow empty comments if there are attachments or a status change (close, reopen, approve, reject)
- // So, only stop if there is no content, no attachments, and no status change.
- if form.Content == "" && len(attachments) == 0 && form.Status == "" {
- ctx.JSONError(ctx.Tr("repo.issues.comment_no_content"))
- return
- }
-
- comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
- if err != nil {
- if errors.Is(err, user_model.ErrBlockedUser) {
- ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
- } else {
- ctx.ServerError("CreateIssueComment", err)
+ // allow empty content if there are attachments
+ if form.Content != "" || len(attachments) > 0 {
+ comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
+ if err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else {
+ ctx.ServerError("CreateIssueComment", err)
+ }
+ return
}
+ // redirect to the comment's hashtag
+ redirect += "#" + comment.HashTag()
+ } else if form.Status == "" {
+ // if no status change (close, reopen), it is a plain comment, and content is required
+ // "approve/reject" are handled differently in SubmitReview
+ ctx.JSONError(ctx.Tr("repo.issues.comment_no_content"))
return
}
@@ -86,6 +90,7 @@ func NewComment(ctx *context.Context) {
!(issue.IsPull && issue.PullRequest.HasMerged) {
// Duplication and conflict check should apply to reopen pull request.
var branchOtherUnmergedPR *issues_model.PullRequest
+ var err error
if form.Status == "reopen" && issue.IsPull {
pull := issue.PullRequest
branchOtherUnmergedPR, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
@@ -179,11 +184,6 @@ func NewComment(ctx *context.Context) {
}
} // end if: handle close or reopen
- // Redirect to the comment, add hashtag if it exists
- redirect := fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, issueType, issue.Index)
- if comment != nil {
- redirect += "#" + comment.HashTag()
- }
ctx.JSONRedirect(redirect)
}
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index d69253dd59..a824cdb246 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -54,12 +54,17 @@ func NewLimitedUploaderMaxBytesReader(r io.ReadCloser, w http.ResponseWriter) *U
return &UploaderFile{rd: r, size: -1, respWriter: w}
}
-func UploadAttachmentGeneralSizeLimit(ctx context.Context, file *UploaderFile, allowedTypes string, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
- return uploadAttachment(ctx, file, allowedTypes, setting.Attachment.MaxSize<<20, attach)
+type UploadAttachmentFunc func(ctx context.Context, file *UploaderFile, attach *repo_model.Attachment) (*repo_model.Attachment, error)
+
+func UploadAttachmentForIssue(ctx context.Context, file *UploaderFile, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
+ return uploadAttachment(ctx, file, setting.Attachment.AllowedTypes, setting.Attachment.MaxSize<<20, attach)
}
-func UploadAttachmentReleaseSizeLimit(ctx context.Context, file *UploaderFile, allowedTypes string, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
- return uploadAttachment(ctx, file, allowedTypes, setting.Repository.Release.FileMaxSize<<20, attach)
+func UploadAttachmentForRelease(ctx context.Context, file *UploaderFile, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
+ // FIXME: although the release attachment has different settings from the issue attachment,
+ // it still uses the same attachment table, the same storage and the same upload logic
+ // So if the "issue attachment [attachment]" is not enabled, it will also affect the release attachment, which is not expected.
+ return uploadAttachment(ctx, file, setting.Repository.Release.AllowedTypes, setting.Repository.Release.FileMaxSize<<20, attach)
}
func uploadAttachment(ctx context.Context, file *UploaderFile, allowedTypes string, maxFileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go
index dfb7b1244c..3c350e3d5d 100644
--- a/services/mailer/incoming/incoming_handler.go
+++ b/services/mailer/incoming/incoming_handler.go
@@ -88,7 +88,7 @@ func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *u
for _, attachment := range content.Attachments {
attachmentBuf := bytes.NewReader(attachment.Content)
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(attachmentBuf, attachmentBuf.Size())
- a, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{
+ a, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
Name: attachment.Name,
UploaderID: doer.ID,
RepoID: issue.Repo.ID,
diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go
index fbbf8d7dad..ddf306b698 100644
--- a/services/packages/rpm/repository.go
+++ b/services/packages/rpm/repository.go
@@ -13,6 +13,7 @@ import (
"errors"
"fmt"
"io"
+ "slices"
"strings"
"time"
@@ -241,15 +242,22 @@ func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, group stri
return err
}
+ data := []*repoData{primary, filelists, other}
+
+ updates := collectUpdateInfoUpdates(pfs, cache)
+ if len(updates) > 0 {
+ updateInfo, err := buildUpdateInfo(ctx, pv, updates, group)
+ if err != nil {
+ return err
+ }
+ data = append(data, updateInfo)
+ }
+
return buildRepomd(
ctx,
pv,
ownerID,
- []*repoData{
- primary,
- filelists,
- other,
- },
+ data,
group,
)
}
@@ -563,6 +571,93 @@ func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*p
}, group)
}
+func collectUpdateInfoUpdates(pfs []*packages_model.PackageFile, c packageCache) (updates []*rpm_module.Update) {
+ seenVersions := make(map[int64]bool)
+ for _, pf := range pfs {
+ pd := c[pf]
+ if pd.Version != nil && !seenVersions[pd.Version.ID] && pd.VersionMetadata.Updates != nil {
+ updates = append(updates, pd.VersionMetadata.Updates...)
+ seenVersions[pd.Version.ID] = true
+ }
+ }
+ return updates
+}
+
+// buildUpdateInfo builds the updateinfo.xml file
+func buildUpdateInfo(ctx context.Context, pv *packages_model.PackageVersion, updates []*rpm_module.Update, group string) (*repoData, error) {
+ // Group updates by ID to merge package lists
+ type updateKey struct {
+ ID string
+ }
+ updateMap := make(map[updateKey]*rpm_module.Update)
+
+ for _, u := range updates {
+ key := updateKey{ID: u.ID}
+ if existing, ok := updateMap[key]; ok {
+ for _, newColl := range u.PkgList {
+ collFound := false
+ for j, existingColl := range existing.PkgList {
+ if existingColl.Short == newColl.Short {
+ for _, newPkg := range newColl.Packages {
+ pkgFound := false
+ for _, existingPkg := range existingColl.Packages {
+ if existingPkg.Name == newPkg.Name &&
+ existingPkg.Version == newPkg.Version &&
+ existingPkg.Release == newPkg.Release &&
+ existingPkg.Arch == newPkg.Arch {
+ pkgFound = true
+ break
+ }
+ }
+ if !pkgFound {
+ existing.PkgList[j].Packages = append(existing.PkgList[j].Packages, newPkg)
+ }
+ }
+ collFound = true
+ break
+ }
+ }
+ if !collFound {
+ collCopy := *newColl
+ collCopy.Packages = append([]*rpm_module.UpdatePackage(nil), newColl.Packages...)
+ existing.PkgList = append(existing.PkgList, &collCopy)
+ }
+ }
+ } else {
+ // Create a shallow copy so we don't mutate the original cached pointer
+ uCopy := *u
+ // Deep copy PkgList and Collections to avoid mutating cache
+ // Note: References is shallow-copied, but safe as long as it remains immutable
+ uCopy.PkgList = make([]*rpm_module.Collection, len(u.PkgList))
+ for i, coll := range u.PkgList {
+ collCopy := *coll
+ collCopy.Packages = append([]*rpm_module.UpdatePackage(nil), coll.Packages...)
+ uCopy.PkgList[i] = &collCopy
+ }
+ updateMap[key] = &uCopy
+ }
+ }
+
+ var mergedUpdates []*rpm_module.Update
+ for _, u := range updateMap {
+ mergedUpdates = append(mergedUpdates, u)
+ }
+ slices.SortFunc(mergedUpdates, func(a, b *rpm_module.Update) int {
+ return strings.Compare(a.ID, b.ID)
+ })
+
+ type updateInfo struct {
+ XMLName xml.Name `xml:"updates"`
+ Xmlns string `xml:"xmlns,attr"`
+ Updates []*rpm_module.Update `xml:"update"`
+ }
+
+ return addDataAsFileToRepo(ctx, pv, "updateinfo", &updateInfo{
+ Xmlns: "http://linux.duke.edu/metadata/updateinfo",
+ Updates: mergedUpdates,
+ }, group)
+}
+
// writtenCounter counts all written bytes
type writtenCounter struct {
written int64
diff --git a/tests/integration/api_packages_rpm_test.go b/tests/integration/api_packages_rpm_test.go
index 61bd8ffc63..ecde21cc70 100644
--- a/tests/integration/api_packages_rpm_test.go
+++ b/tests/integration/api_packages_rpm_test.go
@@ -12,12 +12,14 @@ import (
"io"
"net/http"
"net/http/httptest"
+ "slices"
"strings"
"testing"
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/json"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"
@@ -74,6 +76,15 @@ Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5
content, err := io.ReadAll(zr)
assert.NoError(t, err)
+ decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
+ t.Helper()
+
+ zr, err := gzip.NewReader(resp.Body)
+ assert.NoError(t, err)
+
+ assert.NoError(t, xml.NewDecoder(zr).Decode(v))
+ }
+
rootURL := fmt.Sprintf("/api/packages/%s/rpm", user.Name)
for _, group := range []string{"", "el9", "el9/stable"} {
@@ -247,15 +258,6 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----")
})
- decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
- t.Helper()
-
- zr, err := gzip.NewReader(resp.Body)
- assert.NoError(t, err)
-
- assert.NoError(t, xml.NewDecoder(zr).Decode(v))
- }
-
t.Run("primary.xml.gz", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@@ -420,6 +422,328 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
})
})
+ t.Run("Errata", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ type updateInfo struct {
+ XMLName xml.Name `xml:"updates"`
+ Xmlns string `xml:"xmlns,attr"`
+ Updates []*rpm_module.Update `xml:"update"`
+ }
+
+ errataURL := fmt.Sprintf("%s/package/%s/%s/errata", groupURL, packageName, packageVersion)
+
+ advisory := rpm_module.Update{
+ From: "security@example.com",
+ Status: "stable",
+ Type: "security",
+ Version: "1.0",
+ ID: "CVE-2023-1234",
+ Title: "Test Security Update",
+ Severity: "Important",
+ Description: "This is a test security update.",
+ References: []*rpm_module.Reference{
+ {
+ Href: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-1234",
+ ID: "CVE-2023-1234",
+ Title: "CVE-2023-1234",
+ Type: "cve",
+ },
+ },
+ PkgList: []*rpm_module.Collection{
+ {
+ Short: "el9",
+ Packages: []*rpm_module.UpdatePackage{
+ {
+ Arch: packageArchitecture,
+ Name: packageName,
+ Release: "1",
+ Src: "gitea-test-1.0.2-1.src.rpm",
+ Version: "1.0.2",
+ Filename: fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture),
+ },
+ },
+ },
+ },
+ }
+
+ t.Run("Success", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ updates := []*rpm_module.Update{&advisory}
+ body, err := json.Marshal(updates)
+ assert.NoError(t, err)
+
+ req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ url := groupURL + "/repodata"
+
+ // Check repomd.xml contains updateinfo
+ req = NewRequest(t, "GET", url+"/repomd.xml")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type testRepoData struct {
+ Type string `xml:"type,attr"`
+ }
+ var repomd struct {
+ Data []*testRepoData `xml:"data"`
+ }
+ err = xml.NewDecoder(resp.Body).Decode(&repomd)
+ require.NoError(t, err)
+
+ found := slices.IndexFunc(repomd.Data, func(s *testRepoData) bool {
+ return s.Type == "updateinfo"
+ }) >= 0
+ assert.True(t, found, "updateinfo not found in repomd.xml")
+
+ // Now check updateinfo.xml.gz
+ req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ var result updateInfo
+ decodeGzipXML(t, resp, &result)
+
+ assert.Equal(t, "http://linux.duke.edu/metadata/updateinfo", result.Xmlns)
+ assert.Len(t, result.Updates, 1)
+ assert.Equal(t, "CVE-2023-1234", result.Updates[0].ID)
+ assert.NotEmpty(t, result.Updates[0].Issued.Date)
+ assert.NotEmpty(t, result.Updates[0].Updated.Date)
+ })
+
+ t.Run("InvalidJSON", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "POST", errataURL, strings.NewReader("invalid json")).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+
+ t.Run("NullElementsInJSON", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Send a payload with null inside arrays
+ payload := `[
+ {
+ "id": "CVE-2023-5678",
+ "from": "security@example.com",
+ "status": "stable",
+ "type": "security",
+ "version": "1.0",
+ "title": "Test Null Elements",
+ "severity": "Important",
+ "description": "Test null elements",
+ "pkg_list": [
+ null,
+ {
+ "short": "el9",
+ "packages": [
+ null,
+ {
+ "arch": "x86_64",
+ "name": "gitea",
+ "release": "1",
+ "src": "gitea-1.0.0-1.src.rpm",
+ "version": "1.0.0",
+ "filename": "gitea-1.0.0-1.x86_64.rpm"
+ }
+ ]
+ }
+ ]
+ }
+ ]`
+
+ req := NewRequestWithBody(t, "POST", errataURL, strings.NewReader(payload)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ // Verify it was stored correctly (skipping nulls)
+ url := groupURL + "/repodata"
+ req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result updateInfo
+ decodeGzipXML(t, resp, &result)
+
+ // We need to find the new advisory CVE-2023-5678
+ var newAdvisory *rpm_module.Update
+ for _, u := range result.Updates {
+ if u.ID == "CVE-2023-5678" {
+ newAdvisory = u
+ break
+ }
+ }
+ assert.NotNil(t, newAdvisory)
+ assert.Len(t, newAdvisory.PkgList, 1)
+ assert.Equal(t, "el9", newAdvisory.PkgList[0].Short)
+ assert.Len(t, newAdvisory.PkgList[0].Packages, 1)
+ assert.Equal(t, "gitea", newAdvisory.PkgList[0].Packages[0].Name)
+ })
+
+ t.Run("PackageNotFound", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ badURL := fmt.Sprintf("%s/package/%s/non-existent-version/errata", groupURL, packageName)
+ updates := []*rpm_module.Update{&advisory}
+ body, err := json.Marshal(updates)
+ assert.NoError(t, err)
+
+ req := NewRequestWithBody(t, "POST", badURL, bytes.NewReader(body)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("MergeAdvisories", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Upload a second advisory with the same ID but a different package
+ advisory2 := advisory
+ advisory2.PkgList = []*rpm_module.Collection{
+ {
+ Short: "el9",
+ Packages: []*rpm_module.UpdatePackage{
+ {
+ Arch: packageArchitecture,
+ Name: "another-package",
+ Release: "1",
+ Src: "another-package-1.0.0-1.src.rpm",
+ Version: "1.0.0",
+ Filename: "another-package-1.0.0-1.x86_64.rpm",
+ },
+ },
+ },
+ }
+
+ updates := []*rpm_module.Update{&advisory2}
+ body, err := json.Marshal(updates)
+ assert.NoError(t, err)
+
+ req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ // Check updateinfo.xml.gz again
+ url := groupURL + "/repodata"
+ req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result updateInfo
+ decodeGzipXML(t, resp, &result)
+
+ var targetUpdate *rpm_module.Update
+ for _, u := range result.Updates {
+ if u.ID == "CVE-2023-1234" {
+ targetUpdate = u
+ break
+ }
+ }
+ assert.NotNil(t, targetUpdate)
+ // Verify that package lists are merged into the same collection
+ assert.Len(t, targetUpdate.PkgList, 1)
+ assert.Len(t, targetUpdate.PkgList[0].Packages, 2)
+ assert.Equal(t, "another-package", result.Updates[0].PkgList[0].Packages[1].Name)
+ })
+
+ t.Run("NewCollection", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Upload a third advisory with the same ID but a different collection
+ advisory3 := advisory
+ advisory3.PkgList = []*rpm_module.Collection{
+ {
+ Short: "el8",
+ Packages: []*rpm_module.UpdatePackage{
+ {
+ Arch: packageArchitecture,
+ Name: packageName,
+ Release: "1",
+ Src: "gitea-test-1.0.2-1.src.rpm",
+ Version: "1.0.2",
+ Filename: fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture),
+ },
+ },
+ },
+ }
+
+ updates := []*rpm_module.Update{&advisory3}
+ body, err := json.Marshal(updates)
+ assert.NoError(t, err)
+
+ req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ // Check updateinfo.xml.gz again
+ url := groupURL + "/repodata"
+ req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result updateInfo
+ decodeGzipXML(t, resp, &result)
+
+ var targetUpdate *rpm_module.Update
+ for _, u := range result.Updates {
+ if u.ID == "CVE-2023-1234" {
+ targetUpdate = u
+ break
+ }
+ }
+ assert.NotNil(t, targetUpdate)
+ // Verify that we now have 2 collections
+ assert.Len(t, targetUpdate.PkgList, 2)
+ // We need to be careful with order, map iteration is random
+ // Let's check both exist
+ shorts := []string{targetUpdate.PkgList[0].Short, targetUpdate.PkgList[1].Short}
+ assert.Contains(t, shorts, "el9")
+ assert.Contains(t, shorts, "el8")
+ })
+
+ t.Run("Idempotency", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ updates := []*rpm_module.Update{&advisory}
+ body, err := json.Marshal(updates)
+ assert.NoError(t, err)
+
+ // Post twice
+ req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ req = NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ // Check updateinfo.xml.gz
+ url := groupURL + "/repodata"
+ req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result updateInfo
+ decodeGzipXML(t, resp, &result)
+
+ var targetUpdate *rpm_module.Update
+ for _, u := range result.Updates {
+ if u.ID == "CVE-2023-1234" {
+ targetUpdate = u
+ break
+ }
+ }
+ assert.NotNil(t, targetUpdate)
+ assert.Len(t, targetUpdate.PkgList, 2)
+
+ var el9Coll *rpm_module.Collection
+ for _, coll := range targetUpdate.PkgList {
+ if coll.Short == "el9" {
+ el9Coll = coll
+ break
+ }
+ }
+ assert.NotNil(t, el9Coll)
+ assert.Len(t, el9Coll.Packages, 2)
+ })
+ })
+
t.Run("Delete", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()