Skip to content

Commit 18fc07d

Browse files
authored
Merge branch 'main' into fix-trivial
2 parents 15c2d5d + 3d544a3 commit 18fc07d

File tree

14 files changed

+232
-183
lines changed

14 files changed

+232
-183
lines changed

models/repo/repo.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ func (repo *Repository) IsBroken() bool {
279279
}
280280

281281
// MarkAsBrokenEmpty marks the repo as broken and empty
282+
// FIXME: the status "broken" and "is_empty" were abused,
283+
// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken"
282284
func (repo *Repository) MarkAsBrokenEmpty() {
283285
repo.Status = RepositoryBroken
284286
repo.IsEmpty = true

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,7 @@ create_new_repo_command = Creating a new repository on the command line
12351235
push_exist_repo = Pushing an existing repository from the command line
12361236
empty_message = This repository does not contain any content.
12371237
broken_message = The Git data underlying this repository cannot be read. Contact the administrator of this instance or delete this repository.
1238+
no_branch = This repository doesn’t have any branches.
12381239
12391240
code = Code
12401241
code.desc = Access source code, files, commits and branches.

routers/api/packages/pypi/pypi.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"regexp"
1111
"sort"
1212
"strings"
13+
"unicode"
1314

1415
packages_model "code.gitea.io/gitea/models/packages"
1516
packages_module "code.gitea.io/gitea/modules/packages"
@@ -139,9 +140,30 @@ func UploadPackageFile(ctx *context.Context) {
139140
return
140141
}
141142

142-
projectURL := ctx.Req.FormValue("home_page")
143-
if !validation.IsValidURL(projectURL) {
144-
projectURL = ""
143+
// Ensure ctx.Req.Form exists.
144+
_ = ctx.Req.ParseForm()
145+
146+
var homepageURL string
147+
projectURLs := ctx.Req.Form["project_urls"]
148+
for _, purl := range projectURLs {
149+
label, url, found := strings.Cut(purl, ",")
150+
if !found {
151+
continue
152+
}
153+
if normalizeLabel(label) != "homepage" {
154+
continue
155+
}
156+
homepageURL = strings.TrimSpace(url)
157+
break
158+
}
159+
160+
if len(homepageURL) == 0 {
161+
// TODO: Home-page is a deprecated metadata field. Remove this branch once it's no longer apart of the spec.
162+
homepageURL = ctx.Req.FormValue("home_page")
163+
}
164+
165+
if !validation.IsValidURL(homepageURL) {
166+
homepageURL = ""
145167
}
146168

147169
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
@@ -160,7 +182,7 @@ func UploadPackageFile(ctx *context.Context) {
160182
Description: ctx.Req.FormValue("description"),
161183
LongDescription: ctx.Req.FormValue("long_description"),
162184
Summary: ctx.Req.FormValue("summary"),
163-
ProjectURL: projectURL,
185+
ProjectURL: homepageURL,
164186
License: ctx.Req.FormValue("license"),
165187
RequiresPython: ctx.Req.FormValue("requires_python"),
166188
},
@@ -189,6 +211,23 @@ func UploadPackageFile(ctx *context.Context) {
189211
ctx.Status(http.StatusCreated)
190212
}
191213

214+
// Normalizes a Project-URL label.
215+
// See https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization.
216+
func normalizeLabel(label string) string {
217+
var builder strings.Builder
218+
219+
// "A label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result
220+
// to lowercase."
221+
for _, r := range label {
222+
if unicode.IsPunct(r) || unicode.IsSpace(r) {
223+
continue
224+
}
225+
builder.WriteRune(unicode.ToLower(r))
226+
}
227+
228+
return builder.String()
229+
}
230+
192231
func isValidNameAndVersion(packageName, packageVersion string) bool {
193232
return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion)
194233
}

routers/api/packages/pypi/pypi_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,13 @@ func TestIsValidNameAndVersion(t *testing.T) {
3636
assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa"))
3737
assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta"))
3838
}
39+
40+
func TestNormalizeLabel(t *testing.T) {
41+
// Cases fetched from https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization.
42+
assert.Equal(t, "homepage", normalizeLabel("Homepage"))
43+
assert.Equal(t, "homepage", normalizeLabel("Home-page"))
44+
assert.Equal(t, "homepage", normalizeLabel("Home page"))
45+
assert.Equal(t, "changelog", normalizeLabel("Change_Log"))
46+
assert.Equal(t, "whatsnew", normalizeLabel("What's New?"))
47+
assert.Equal(t, "github", normalizeLabel("github"))
48+
}

routers/web/repo/view_home.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -223,35 +223,49 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) {
223223
}
224224
}
225225

226+
func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) {
227+
ctx.Repo.Repository.IsEmpty = empty
228+
if ctx.Repo.Repository.Status == repo_model.RepositoryReady || ctx.Repo.Repository.Status == repo_model.RepositoryBroken {
229+
ctx.Repo.Repository.Status = status // only handle ready and broken status, leave other status as-is
230+
}
231+
if err := repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil {
232+
ctx.ServerError("updateContextRepoEmptyAndStatus: UpdateRepositoryCols", err)
233+
return
234+
}
235+
}
236+
226237
func handleRepoEmptyOrBroken(ctx *context.Context) {
227238
showEmpty := true
228-
var err error
229239
if ctx.Repo.GitRepo != nil {
230-
showEmpty, err = ctx.Repo.GitRepo.IsEmpty()
240+
reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
231241
if err != nil {
242+
showEmpty = true // the repo is broken
243+
updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryBroken)
232244
log.Error("GitRepo.IsEmpty: %v", err)
233-
ctx.Repo.Repository.Status = repo_model.RepositoryBroken
234-
showEmpty = true
235245
ctx.Flash.Error(ctx.Tr("error.occurred"), true)
246+
} else if reallyEmpty {
247+
showEmpty = true // the repo is really empty
248+
updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
249+
} else if ctx.Repo.Commit == nil {
250+
showEmpty = true // it is not really empty, but there is no branch
251+
// at the moment, other repo units like "actions" are not able to handle such case,
252+
// so we just mark the repo as empty to prevent from displaying these units.
253+
updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
254+
} else {
255+
// the repo is actually not empty and has branches, need to update the database later
256+
showEmpty = false
236257
}
237258
}
238259
if showEmpty {
239260
ctx.HTML(http.StatusOK, tplRepoEMPTY)
240261
return
241262
}
242263

243-
// the repo is not really empty, so we should update the modal in database
244-
// such problem may be caused by:
245-
// 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually
246-
// and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos.
247-
// it's possible for a repository to be non-empty by that flag but still 500
248-
// because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed.
249-
ctx.Repo.Repository.IsEmpty = false
250-
if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil {
251-
ctx.ServerError("UpdateRepositoryCols", err)
252-
return
253-
}
254-
if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
264+
// The repo is not really empty, so we should update the model in database, such problem may be caused by:
265+
// 1) an error occurs during pushing/receiving.
266+
// 2) the user replaces an empty git repo manually.
267+
updateContextRepoEmptyAndStatus(ctx, false, repo_model.RepositoryReady)
268+
if err := repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
255269
ctx.ServerError("UpdateRepoSize", err)
256270
return
257271
}

services/context/repo.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -897,10 +897,8 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
897897
refName = brs[0].Name
898898
} else if len(brs) == 0 {
899899
log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path)
900-
ctx.Repo.Repository.MarkAsBrokenEmpty()
901900
} else {
902901
log.Error("GetBranches error: %v", err)
903-
ctx.Repo.Repository.MarkAsBrokenEmpty()
904902
}
905903
}
906904
ctx.Repo.RefName = refName
@@ -911,7 +909,6 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
911909
} else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") {
912910
// if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users
913911
log.Error("GetBranchCommit: %v", err)
914-
ctx.Repo.Repository.MarkAsBrokenEmpty()
915912
} else {
916913
ctx.ServerError("GetBranchCommit", err)
917914
return

templates/repo/empty.tmpl

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@
1414
{{end}}
1515
</div>
1616
{{end}}
17+
1718
{{if .Repository.IsBroken}}
18-
<div class="ui segment center">
19-
{{ctx.Locale.Tr "repo.broken_message"}}
20-
</div>
19+
<div class="ui segment center">{{ctx.Locale.Tr "repo.broken_message"}}</div>
20+
{{else if .Repository.IsEmpty}}
21+
<div class="ui segment center">{{ctx.Locale.Tr "repo.no_branch"}}</div>
2122
{{else if .CanWriteCode}}
22-
<h4 class="ui top attached header">
23-
{{ctx.Locale.Tr "repo.quick_guide"}}
24-
</h4>
23+
<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.quick_guide"}}</h4>
2524
<div class="ui attached guide table segment empty-repo-guide">
2625
<div class="item">
2726
<h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository"}}</small></h3>
@@ -66,12 +65,10 @@ git push -u origin {{.Repository.DefaultBranch}}</code></pre>
6665
</div>
6766
</div>
6867
{{end}}
69-
{{else}}
70-
<div class="ui segment center">
71-
{{ctx.Locale.Tr "repo.empty_message"}}
72-
</div>
73-
{{end}}
74-
</div>
68+
</div>
69+
{{else}}
70+
<div class="ui segment center">{{ctx.Locale.Tr "repo.empty_message"}}</div>
71+
{{end}}
7572
</div>
7673
</div>
7774
</div>

templates/repo/header.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@
162162
</a>
163163
{{end}}
164164

165-
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}}
165+
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo)}}
166166
<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions">
167167
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}
168168
{{if .Repository.NumOpenActionRuns}}

tests/integration/api_packages_pypi_test.go

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ func TestPackagePyPI(t *testing.T) {
3232
packageVersion := "1!1.0.1+r1234"
3333
packageAuthor := "KN4CK3R"
3434
packageDescription := "Test Description"
35+
projectURL := "https://example.com"
3536

3637
content := "test"
3738
hashSHA256 := "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
3839

3940
root := fmt.Sprintf("/api/packages/%s/pypi", user.Name)
4041

41-
uploadFile := func(t *testing.T, filename, content string, expectedStatus int) {
42-
body := &bytes.Buffer{}
43-
writer := multipart.NewWriter(body)
42+
createBasicMultipartFile := func(filename, packageName, content string) (body *bytes.Buffer, writer *multipart.Writer, closer func() error) {
43+
body = &bytes.Buffer{}
44+
writer = multipart.NewWriter(body)
4445
part, _ := writer.CreateFormFile("content", filename)
4546
_, _ = io.Copy(part, strings.NewReader(content))
4647

@@ -52,14 +53,27 @@ func TestPackagePyPI(t *testing.T) {
5253
writer.WriteField("sha256_digest", hashSHA256)
5354
writer.WriteField("requires_python", "3.6")
5455

55-
_ = writer.Close()
56+
return body, writer, writer.Close
57+
}
5658

59+
uploadHelper := func(t *testing.T, body *bytes.Buffer, contentType string, expectedStatus int) {
5760
req := NewRequestWithBody(t, "POST", root, body).
58-
SetHeader("Content-Type", writer.FormDataContentType()).
61+
SetHeader("Content-Type", contentType).
5962
AddBasicAuth(user.Name)
6063
MakeRequest(t, req, expectedStatus)
6164
}
6265

66+
uploadFile := func(t *testing.T, filename, content string, expectedStatus int) {
67+
body, writer, closeFunc := createBasicMultipartFile(filename, packageName, content)
68+
69+
writer.WriteField("project_urls", "DOCUMENTATION , https://readthedocs.org")
70+
writer.WriteField("project_urls", fmt.Sprintf("Home-page, %s", projectURL))
71+
72+
_ = closeFunc()
73+
74+
uploadHelper(t, body, writer.FormDataContentType(), expectedStatus)
75+
}
76+
6377
t.Run("Upload", func(t *testing.T) {
6478
defer tests.PrintCurrentTest(t)()
6579

@@ -74,6 +88,7 @@ func TestPackagePyPI(t *testing.T) {
7488
assert.NoError(t, err)
7589
assert.Nil(t, pd.SemVer)
7690
assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
91+
assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL)
7792
assert.Equal(t, packageName, pd.Package.Name)
7893
assert.Equal(t, packageVersion, pd.Version.Version)
7994

@@ -133,6 +148,48 @@ func TestPackagePyPI(t *testing.T) {
133148
uploadFile(t, "test.tar.gz", content, http.StatusConflict)
134149
})
135150

151+
t.Run("UploadUsingDeprecatedHomepageMetadata", func(t *testing.T) {
152+
defer tests.PrintCurrentTest(t)()
153+
154+
pkgName := "homepage-package"
155+
body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content)
156+
157+
writer.WriteField("home_page", projectURL)
158+
159+
_ = closeFunc()
160+
161+
uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated)
162+
163+
pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName)
164+
assert.NoError(t, err)
165+
assert.Len(t, pvs, 1)
166+
167+
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
168+
assert.NoError(t, err)
169+
assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
170+
assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL)
171+
})
172+
173+
t.Run("UploadWithoutAnyHomepageURLMetadata", func(t *testing.T) {
174+
defer tests.PrintCurrentTest(t)()
175+
176+
pkgName := "no-project-url-or-homepage-package"
177+
body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content)
178+
179+
_ = closeFunc()
180+
181+
uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated)
182+
183+
pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName)
184+
assert.NoError(t, err)
185+
assert.Len(t, pvs, 1)
186+
187+
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
188+
assert.NoError(t, err)
189+
assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
190+
assert.Empty(t, pd.Metadata.(*pypi.Metadata).ProjectURL)
191+
})
192+
136193
t.Run("Download", func(t *testing.T) {
137194
defer tests.PrintCurrentTest(t)()
138195

@@ -147,7 +204,7 @@ func TestPackagePyPI(t *testing.T) {
147204
downloadFile("test.whl")
148205
downloadFile("test.tar.gz")
149206

150-
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI)
207+
pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, packageName)
151208
assert.NoError(t, err)
152209
assert.Len(t, pvs, 1)
153210
assert.Equal(t, int64(2), pvs[0].DownloadCount)

web_src/css/base.css

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,13 @@ a.label,
336336
border-color: var(--color-secondary);
337337
}
338338

339+
.ui.dropdown .menu > .header {
340+
text-transform: none; /* reset fomantic's "uppercase" */
341+
}
342+
339343
.ui.dropdown .menu > .header:not(.ui) {
340344
color: var(--color-text);
345+
font-size: 0.95em; /* reset fomantic's small font-size */
341346
}
342347

343348
.ui.dropdown .menu > .item {
@@ -691,10 +696,6 @@ input:-webkit-autofill:active,
691696
box-shadow: 0 6px 18px var(--color-shadow) !important;
692697
}
693698

694-
.ui.dropdown .menu > .header {
695-
font-size: 0.8em;
696-
}
697-
698699
.ui .text.left {
699700
text-align: left !important;
700701
}

0 commit comments

Comments
 (0)