mirror of
https://github.com/go-gitea/gitea
synced 2026-02-10 13:28:10 +00:00
Compare commits
23 Commits
v1.17.0-rc2
...
v1.17.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8769df117d | ||
|
|
09f2e1e1a2 | ||
|
|
eeb490c7ab | ||
|
|
97a8c96c5b | ||
|
|
d1e53bfd7f | ||
|
|
fc7b5afd9b | ||
|
|
210b096da7 | ||
|
|
d6bc1558c6 | ||
|
|
6986e56791 | ||
|
|
ae86a0bc9f | ||
|
|
4b53a5c3a1 | ||
|
|
da10ce8b07 | ||
|
|
4ed32e79b6 | ||
|
|
fa46d66835 | ||
|
|
648ec3cfed | ||
|
|
a9a440e600 | ||
|
|
39b2ede930 | ||
|
|
0a32bd56eb | ||
|
|
e0f35ea00f | ||
|
|
bed13bfa9e | ||
|
|
435038b2c6 | ||
|
|
2fe0dab2d5 | ||
|
|
e930d66a9c |
+38
-42
@@ -4,44 +4,7 @@ This changelog goes through all the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
|
||||
## [1.17.0-rc2](https://github.com/go-gitea/gitea/releases/tag/v1.17.0-rc2) - 2022-07-13
|
||||
|
||||
* SECURITY
|
||||
* Use git.HOME_PATH for Git HOME directory (#20114) (#20293)
|
||||
* Add write check for creating Commit Statuses (#20332) (#20333)
|
||||
* ENHANCEMENTS
|
||||
* Make notification bell more prominent on mobile (#20108, #20236, #20251) (#20269)
|
||||
* Adjust max-widths for the repository file table (#20243) (#20247)
|
||||
* Display full name (#20171) (#20246)
|
||||
* BUGFIXES
|
||||
* Allow RSA 2047 bit keys (#20272) (#20396)
|
||||
* Add missing return for when topic isn't found (#20351) (#20395)
|
||||
* Fix commit status icon when in subdirectory (#20285) (#20385)
|
||||
* Initialize cron last (#20373) (#20384)
|
||||
* Set target on create release with existing tag (#20381) (#20382)
|
||||
* Update xorm.io/xorm to fix a interpreting db column sizes issue on 32bit systems (#20371) (#20372)
|
||||
* Make sure `repo_dir` is an empty directory or doesn't exist before 'dump-repo' (#20205) (#20370)
|
||||
* Prevent context deadline error propagation in GetCommitsInfo (#20346) (#20361)
|
||||
* Correctly handle draft releases without a tag (#20314) (#20335)
|
||||
* Prevent "empty" scrollbars on Firefox (#20294) (#20308)
|
||||
* Refactor SSH init code, fix directory creation for TrustedUserCAKeys file (#20299) (#20306)
|
||||
* Bump goldmark to v1.4.13 (#20300) (#20301)
|
||||
* Do not create empty ".ssh" directory when loading config (#20289) (#20298)
|
||||
* Fix NPE when using non-numeric (#20277) (#20278)
|
||||
* Store read access in access for team repositories (#20275) (#20276)
|
||||
* EscapeFilter the group dn membership (#20200) (#20254)
|
||||
* Only show Followers that current user can access (#20220) (#20252)
|
||||
* Update Bluemonday to v1.0.19 (#20199) (#20209)
|
||||
* Refix indices on actions table (#20158) (#20198)
|
||||
* Check if project has the same repository id with issue when assign project to issue (#20133) (#20188)
|
||||
* Fix remove file on initial comment (#20127) (#20128)
|
||||
* Catch the error before the response is processed by goth (#20000) (#20102)
|
||||
* Dashboard feed respect setting.UI.FeedPagingNum again (#20094) (#20099)
|
||||
* Alter hook_task TEXT fields to LONGTEXT (#20038) (#20041)
|
||||
* Respond with a 401 on git push when password isn't changed yet (#20026) (#20027)
|
||||
* Return 404 when tag is broken (#20017) (#20024)
|
||||
|
||||
## [1.17.0-rc1](https://github.com/go-gitea/gitea/releases/tag/v1.17.0-rc1) - 2022-06-18
|
||||
## [1.17.0](https://github.com/go-gitea/gitea/releases/tag/v1.17.0) - 2022-07-30
|
||||
|
||||
* BREAKING
|
||||
* Require go1.18 for Gitea 1.17 (#19918)
|
||||
@@ -66,6 +29,8 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
* Restrict email address validation (#17688)
|
||||
* Refactor Router Logger (#17308)
|
||||
* SECURITY
|
||||
* Use git.HOME_PATH for Git HOME directory (#20114) (#20293)
|
||||
* Add write check for creating Commit Statuses (#20332) (#20333)
|
||||
* Remove deprecated SSH ciphers from default (#18697)
|
||||
* FEDERATION
|
||||
* Return statistic information for nodeinfo (#19561)
|
||||
@@ -104,6 +69,9 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
* Return primary language and repository language stats API URL (#18396)
|
||||
* Implement http signatures support for the API (#17565)
|
||||
* ENHANCEMENTS
|
||||
* Make notification bell more prominent on mobile (#20108, #20236, #20251) (#20269)
|
||||
* Adjust max-widths for the repository file table (#20243) (#20247)
|
||||
* Display full name (#20171) (#20246)
|
||||
* Add dbconsistency checks for Stopwatches (#20010)
|
||||
* Add fetch.writeCommitGraph to gitconfig (#20006)
|
||||
* Add fgprof pprof profiler (#20005)
|
||||
@@ -148,7 +116,6 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
* PullService lock via pullID (#19520)
|
||||
* Make repository file list useable on mobile (#19515)
|
||||
* more context for models (#19511)
|
||||
* Allow package dump skipping (#19506)
|
||||
* Refactor readme file renderer (#19502)
|
||||
* By default force vertical tabs on mobile (#19486)
|
||||
* Github style following followers (#19482)
|
||||
@@ -165,7 +132,6 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
* Move access and repo permission to models/perm/access (#19350)
|
||||
* Disallow selecting the text of buttons (#19330)
|
||||
* Allow custom redirect for landing page (#19324)
|
||||
* Repository level enable package or disable (#19323)
|
||||
* Remove dependent on session auth for api/v1 routers (#19321)
|
||||
* Never use /api/v1 from Gitea UI Pages (#19318)
|
||||
* Remove legacy unmaintained packages, refactor to support change default locale (#19308)
|
||||
@@ -222,13 +188,44 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
* Allow custom default merge message with .gitea/default_merge_message/<merge_style>_TEMPLATE.md (#18177)
|
||||
* Prettify number of issues (#17760)
|
||||
* Add a "admin user generate-access-token" subcommand (#17722)
|
||||
* Move project files into models/project sub package (#17704)
|
||||
* Custom regexp external issues (#17624)
|
||||
* Add smtp password to install page (#17564)
|
||||
* Add config options to hide issue events (#17414)
|
||||
* Prevent double click new issue/pull/comment button (#16157)
|
||||
* Show issue assignee on project board (#15232)
|
||||
* BUGFIXES
|
||||
* WebAuthn CredentialID field needs to be increased in size (#20530) (#20555)
|
||||
* Ensure that all unmerged files are merged when conflict checking (#20528) (#20536)
|
||||
* Stop logging EOFs and exit(1)s in ssh handler (#20476) (#20529)
|
||||
* Add labels to two buttons that were missing them (#20419) (#20524)
|
||||
* Fix ROOT_URL detection for URLs without trailing slash (#20502) (#20503)
|
||||
* Dismiss prior pull reviews if done via web in review dismiss (#20197) (#20407)
|
||||
* Allow RSA 2047 bit keys (#20272) (#20396)
|
||||
* Add missing return for when topic isn't found (#20351) (#20395)
|
||||
* Fix commit status icon when in subdirectory (#20285) (#20385)
|
||||
* Initialize cron last (#20373) (#20384)
|
||||
* Set target on create release with existing tag (#20381) (#20382)
|
||||
* Update xorm.io/xorm to fix a interpreting db column sizes issue on 32bit systems (#20371) (#20372)
|
||||
* Make sure `repo_dir` is an empty directory or doesn't exist before 'dump-repo' (#20205) (#20370)
|
||||
* Prevent context deadline error propagation in GetCommitsInfo (#20346) (#20361)
|
||||
* Correctly handle draft releases without a tag (#20314) (#20335)
|
||||
* Prevent "empty" scrollbars on Firefox (#20294) (#20308)
|
||||
* Refactor SSH init code, fix directory creation for TrustedUserCAKeys file (#20299) (#20306)
|
||||
* Bump goldmark to v1.4.13 (#20300) (#20301)
|
||||
* Do not create empty ".ssh" directory when loading config (#20289) (#20298)
|
||||
* Fix NPE when using non-numeric (#20277) (#20278)
|
||||
* Store read access in access for team repositories (#20275) (#20276)
|
||||
* EscapeFilter the group dn membership (#20200) (#20254)
|
||||
* Only show Followers that current user can access (#20220) (#20252)
|
||||
* Update Bluemonday to v1.0.19 (#20199) (#20209)
|
||||
* Refix indices on actions table (#20158) (#20198)
|
||||
* Check if project has the same repository id with issue when assign project to issue (#20133) (#20188)
|
||||
* Fix remove file on initial comment (#20127) (#20128)
|
||||
* Catch the error before the response is processed by goth (#20000) (#20102)
|
||||
* Dashboard feed respect setting.UI.FeedPagingNum again (#20094) (#20099)
|
||||
* Alter hook_task TEXT fields to LONGTEXT (#20038) (#20041)
|
||||
* Respond with a 401 on git push when password isn't changed yet (#20026) (#20027)
|
||||
* Return 404 when tag is broken (#20017) (#20024)
|
||||
* Alter hook_task TEXT fields to LONGTEXT (#20038) (#20041)
|
||||
* Respond with a 401 on git push when password isn't changed yet (#20026) (#20027)
|
||||
* Return 404 when tag is broken (#20017) (#20024)
|
||||
@@ -303,7 +300,6 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
* MISC
|
||||
* Fix aria for logo (#19955)
|
||||
* In code search, get code unit accessible repos in one (main) query (#19764)
|
||||
* Enable packages by default again (#19746)
|
||||
* Add tooltip to pending PR comments (#19662)
|
||||
* Improve sync performance for pull-mirrors (#19125)
|
||||
* Improve dashboard's repo list performance (#18963)
|
||||
|
||||
@@ -37,7 +37,7 @@ PUT https://gitea.example.com/api/packages/{owner}/generic/{package_name}/{packa
|
||||
| ----------------- | ----------- |
|
||||
| `owner` | The owner of the package. |
|
||||
| `package_name` | The package name. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`). |
|
||||
| `package_version` | The package version as described in the [SemVer](https://semver.org/) spec. |
|
||||
| `package_version` | The package version, a non-empty string. |
|
||||
| `file_name` | The filename. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`). |
|
||||
|
||||
Example request using HTTP Basic authentication:
|
||||
|
||||
@@ -9,7 +9,7 @@ require (
|
||||
gitea.com/go-chi/cache v0.2.0
|
||||
gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5
|
||||
gitea.com/go-chi/session v0.0.0-20211218221615-e3605d8b28b8
|
||||
gitea.com/lunny/levelqueue v0.4.1
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20220729054728-f020868cc2f7
|
||||
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
|
||||
@@ -78,8 +78,8 @@ gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5 h1:J/1i8u40TbcLP/w2w
|
||||
gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5/go.mod h1:hQ9SYHKdOX968wJglb/NMQ+UqpOKwW4L+EYdvkWjHSo=
|
||||
gitea.com/go-chi/session v0.0.0-20211218221615-e3605d8b28b8 h1:tJQRXgZigkLeeW9LPlps9G9aMoE6LAmqigLA+wxmd1Q=
|
||||
gitea.com/go-chi/session v0.0.0-20211218221615-e3605d8b28b8/go.mod h1:fc/pjt5EqNKgqQXYzcas1Z5L5whkZHyOvTA7OzWVJck=
|
||||
gitea.com/lunny/levelqueue v0.4.1 h1:RZ+AFx5gBsZuyqCvofhAkPQ9uaVDPJnsULoJZIYaJNw=
|
||||
gitea.com/lunny/levelqueue v0.4.1/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20220729054728-f020868cc2f7 h1:Zc3RQWC2xOVglLciQH/ZIC5IqSk3Jn96LflGQLv18Rg=
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20220729054728-f020868cc2f7/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
|
||||
func TestPackageContainer(t *testing.T) {
|
||||
defer prepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
|
||||
has := func(l packages_model.PackagePropertyList, name string) bool {
|
||||
@@ -36,6 +37,15 @@ func TestPackageContainer(t *testing.T) {
|
||||
}
|
||||
return false
|
||||
}
|
||||
getAllByName := func(l packages_model.PackagePropertyList, name string) []string {
|
||||
values := make([]string, 0, len(l))
|
||||
for _, pp := range l {
|
||||
if pp.Name == name {
|
||||
values = append(values, pp.Value)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
images := []string{"test", "te/st"}
|
||||
tags := []string{"latest", "main"}
|
||||
@@ -66,7 +76,7 @@ func TestPackageContainer(t *testing.T) {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token"`}
|
||||
authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`}
|
||||
|
||||
t.Run("Anonymous", func(t *testing.T) {
|
||||
defer PrintCurrentTest(t)()
|
||||
@@ -236,7 +246,8 @@ func TestPackageContainer(t *testing.T) {
|
||||
assert.Nil(t, pd.SemVer)
|
||||
assert.Equal(t, image, pd.Package.Name)
|
||||
assert.Equal(t, tag, pd.Version.Version)
|
||||
assert.True(t, has(pd.Properties, container_module.PropertyManifestTagged))
|
||||
assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
|
||||
assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged))
|
||||
|
||||
assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
|
||||
metadata := pd.Metadata.(*container_module.Metadata)
|
||||
@@ -330,7 +341,8 @@ func TestPackageContainer(t *testing.T) {
|
||||
assert.Nil(t, pd.SemVer)
|
||||
assert.Equal(t, image, pd.Package.Name)
|
||||
assert.Equal(t, untaggedManifestDigest, pd.Version.Version)
|
||||
assert.False(t, has(pd.Properties, container_module.PropertyManifestTagged))
|
||||
assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
|
||||
assert.False(t, has(pd.VersionProperties, container_module.PropertyManifestTagged))
|
||||
|
||||
assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
|
||||
|
||||
@@ -362,18 +374,10 @@ func TestPackageContainer(t *testing.T) {
|
||||
assert.Nil(t, pd.SemVer)
|
||||
assert.Equal(t, image, pd.Package.Name)
|
||||
assert.Equal(t, multiTag, pd.Version.Version)
|
||||
assert.True(t, has(pd.Properties, container_module.PropertyManifestTagged))
|
||||
assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository))
|
||||
assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged))
|
||||
|
||||
getAllByName := func(l packages_model.PackagePropertyList, name string) []string {
|
||||
values := make([]string, 0, len(l))
|
||||
for _, pp := range l {
|
||||
if pp.Name == name {
|
||||
values = append(values, pp.Value)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.Properties, container_module.PropertyManifestReference))
|
||||
assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference))
|
||||
|
||||
assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
|
||||
metadata := pd.Metadata.(*container_module.Metadata)
|
||||
@@ -528,4 +532,56 @@ func TestPackageContainer(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("OwnerNameChange", func(t *testing.T) {
|
||||
defer PrintCurrentTest(t)()
|
||||
|
||||
checkCatalog := func(owner string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
defer PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%sv2/_catalog", setting.AppURL))
|
||||
addTokenAuthHeader(req, userToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
type RepositoryList struct {
|
||||
Repositories []string `json:"repositories"`
|
||||
}
|
||||
|
||||
repoList := &RepositoryList{}
|
||||
DecodeJSON(t, resp, &repoList)
|
||||
|
||||
assert.Len(t, repoList.Repositories, len(images))
|
||||
names := make([]string, 0, len(images))
|
||||
for _, image := range images {
|
||||
names = append(names, strings.ToLower(owner+"/"+image))
|
||||
}
|
||||
assert.ElementsMatch(t, names, repoList.Repositories)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run(fmt.Sprintf("Catalog[%s]", user.LowerName), checkCatalog(user.LowerName))
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
newOwnerName := "newUsername"
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/settings"),
|
||||
"name": newOwnerName,
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/settings"),
|
||||
"name": user.Name,
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ func TestPackageGeneric(t *testing.T) {
|
||||
|
||||
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, pd.SemVer)
|
||||
assert.Nil(t, pd.Metadata)
|
||||
assert.Equal(t, packageName, pd.Package.Name)
|
||||
assert.Equal(t, packageVersion, pd.Version.Version)
|
||||
|
||||
@@ -85,9 +85,9 @@ func TestPackageNpm(t *testing.T) {
|
||||
assert.IsType(t, &npm.Metadata{}, pd.Metadata)
|
||||
assert.Equal(t, packageName, pd.Package.Name)
|
||||
assert.Equal(t, packageVersion, pd.Version.Version)
|
||||
assert.Len(t, pd.Properties, 1)
|
||||
assert.Equal(t, npm.TagProperty, pd.Properties[0].Name)
|
||||
assert.Equal(t, packageTag, pd.Properties[0].Value)
|
||||
assert.Len(t, pd.VersionProperties, 1)
|
||||
assert.Equal(t, npm.TagProperty, pd.VersionProperties[0].Name)
|
||||
assert.Equal(t, packageTag, pd.VersionProperties[0].Value)
|
||||
|
||||
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
+8
-10
@@ -6,7 +6,6 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -20,14 +19,14 @@ import (
|
||||
// ErrWebAuthnCredentialNotExist represents a "ErrWebAuthnCRedentialNotExist" kind of error.
|
||||
type ErrWebAuthnCredentialNotExist struct {
|
||||
ID int64
|
||||
CredentialID string
|
||||
CredentialID []byte
|
||||
}
|
||||
|
||||
func (err ErrWebAuthnCredentialNotExist) Error() string {
|
||||
if err.CredentialID == "" {
|
||||
if len(err.CredentialID) == 0 {
|
||||
return fmt.Sprintf("WebAuthn credential does not exist [id: %d]", err.ID)
|
||||
}
|
||||
return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %s]", err.CredentialID)
|
||||
return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %x]", err.CredentialID)
|
||||
}
|
||||
|
||||
// IsErrWebAuthnCredentialNotExist checks if an error is a ErrWebAuthnCredentialNotExist.
|
||||
@@ -43,7 +42,7 @@ type WebAuthnCredential struct {
|
||||
Name string
|
||||
LowerName string `xorm:"unique(s)"`
|
||||
UserID int64 `xorm:"INDEX unique(s)"`
|
||||
CredentialID string `xorm:"INDEX VARCHAR(410)"`
|
||||
CredentialID []byte `xorm:"INDEX VARBINARY(1024)"`
|
||||
PublicKey []byte
|
||||
AttestationType string
|
||||
AAGUID []byte
|
||||
@@ -94,9 +93,8 @@ type WebAuthnCredentialList []*WebAuthnCredential
|
||||
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
|
||||
creds := make([]webauthn.Credential, 0, len(list))
|
||||
for _, cred := range list {
|
||||
credID, _ := base32.HexEncoding.DecodeString(cred.CredentialID)
|
||||
creds = append(creds, webauthn.Credential{
|
||||
ID: credID,
|
||||
ID: cred.CredentialID,
|
||||
PublicKey: cred.PublicKey,
|
||||
AttestationType: cred.AttestationType,
|
||||
Authenticator: webauthn.Authenticator{
|
||||
@@ -164,11 +162,11 @@ func HasWebAuthnRegistrationsByUID(uid int64) (bool, error) {
|
||||
}
|
||||
|
||||
// GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID
|
||||
func GetWebAuthnCredentialByCredID(userID int64, credID string) (*WebAuthnCredential, error) {
|
||||
func GetWebAuthnCredentialByCredID(userID int64, credID []byte) (*WebAuthnCredential, error) {
|
||||
return getWebAuthnCredentialByCredID(db.DefaultContext, userID, credID)
|
||||
}
|
||||
|
||||
func getWebAuthnCredentialByCredID(ctx context.Context, userID int64, credID string) (*WebAuthnCredential, error) {
|
||||
func getWebAuthnCredentialByCredID(ctx context.Context, userID int64, credID []byte) (*WebAuthnCredential, error) {
|
||||
cred := new(WebAuthnCredential)
|
||||
if found, err := db.GetEngine(ctx).Where("user_id = ? AND credential_id = ?", userID, credID).Get(cred); err != nil {
|
||||
return nil, err
|
||||
@@ -187,7 +185,7 @@ func createCredential(ctx context.Context, userID int64, name string, cred *weba
|
||||
c := &WebAuthnCredential{
|
||||
UserID: userID,
|
||||
Name: name,
|
||||
CredentialID: base32.HexEncoding.EncodeToString(cred.ID),
|
||||
CredentialID: cred.ID,
|
||||
PublicKey: cred.PublicKey,
|
||||
AttestationType: cred.AttestationType,
|
||||
AAGUID: cred.Authenticator.AAGUID,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
@@ -61,9 +60,7 @@ func TestCreateCredential(t *testing.T) {
|
||||
res, err := CreateCredential(1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "WebAuthn Created Credential", res.Name)
|
||||
bs, err := base32.HexEncoding.DecodeString(res.CredentialID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("Test"), bs)
|
||||
assert.Equal(t, []byte("Test"), res.CredentialID)
|
||||
|
||||
unittest.AssertExistsIf(t, true, &WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -361,7 +362,11 @@ func (opts GetMilestonesOption) toCond() builder.Cond {
|
||||
}
|
||||
|
||||
if len(opts.Name) != 0 {
|
||||
cond = cond.And(builder.Like{"name", opts.Name})
|
||||
if setting.Database.UseSQLite3 {
|
||||
cond = cond.And(builder.Like{"UPPER(name)", util.ToUpperASCII(opts.Name)})
|
||||
} else {
|
||||
cond = cond.And(builder.Like{"UPPER(name)", strings.ToUpper(opts.Name)})
|
||||
}
|
||||
}
|
||||
|
||||
return cond
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -474,6 +475,35 @@ func SubmitReview(doer *user_model.User, issue *Issue, reviewType ReviewType, co
|
||||
return review, comm, committer.Commit()
|
||||
}
|
||||
|
||||
// GetReviewOptions represent filter options for GetReviews
|
||||
type GetReviewOptions struct {
|
||||
IssueID int64
|
||||
ReviewerID int64
|
||||
Dismissed util.OptionalBool
|
||||
}
|
||||
|
||||
// GetReviews return reviews based on GetReviewOptions
|
||||
func GetReviews(ctx context.Context, opts *GetReviewOptions) ([]*Review, error) {
|
||||
if opts == nil {
|
||||
return nil, fmt.Errorf("opts are nil")
|
||||
}
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
if opts.IssueID != 0 {
|
||||
sess = sess.Where("issue_id=?", opts.IssueID)
|
||||
}
|
||||
if opts.ReviewerID != 0 {
|
||||
sess = sess.Where("reviewer_id=?", opts.ReviewerID)
|
||||
}
|
||||
if !opts.Dismissed.IsNone() {
|
||||
sess = sess.Where("dismissed=?", opts.Dismissed.IsTrue())
|
||||
}
|
||||
|
||||
reviews := make([]*Review, 0, 4)
|
||||
return reviews, sess.Find(&reviews)
|
||||
}
|
||||
|
||||
// GetReviewersByIssueID gets the latest review of each reviewer for a pull request
|
||||
func GetReviewersByIssueID(issueID int64) ([]*Review, error) {
|
||||
reviews := make([]*Review, 0, 10)
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
-
|
||||
id: 1
|
||||
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
|
||||
-
|
||||
id: 2
|
||||
credential_id: "051CLMMKB62S6M9M2A4H54K7MMCQALFJ36G4TGB2S9A47APLTILU6C6744CEBG4EKCGV357N21BSLH8JD33GQMFAR6DQ70S76P34J6FR="
|
||||
-
|
||||
id: 4
|
||||
credential_id: "APU4B1NDTEVTEM60V4T0FRL7SRJMO9KIE2AKFQ8JDGTQ7VHFI41FDEFTDLBVQEAE4ER49QV2GTGVFDNBO31BPOA3OQN6879OT6MTU3G="
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
-
|
||||
id: 1
|
||||
lower_name: "u2fkey-correctly-migrated"
|
||||
name: "u2fkey-correctly-migrated"
|
||||
user_id: 1
|
||||
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
|
||||
public_key: 0x040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf2
|
||||
attestation_type: 'fido-u2f'
|
||||
sign_count: 1
|
||||
clone_warning: false
|
||||
-
|
||||
id: 2
|
||||
lower_name: "non-u2f-key"
|
||||
name: "non-u2f-key"
|
||||
user_id: 1
|
||||
credential_id: "051CLMMKB62S6M9M2A4H54K7MMCQALFJ36G4TGB2S9A47APLTILU6C6744CEBG4EKCGV357N21BSLH8JD33GQMFAR6DQ70S76P34J6FR"
|
||||
public_key: 0x040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf2
|
||||
attestation_type: 'none'
|
||||
sign_count: 1
|
||||
clone_warning: false
|
||||
-
|
||||
id: 4
|
||||
lower_name: "packed-key"
|
||||
name: "packed-key"
|
||||
user_id: 1
|
||||
credential_id: "APU4B1NDTEVTEM60V4T0FRL7SRJMO9KIE2AKFQ8JDGTQ7VHFI41FDEFTDLBVQEAE4ER49QV2GTGVFDNBO31BPOA3OQN6879OT6MTU3G="
|
||||
public_key: 0x040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf2
|
||||
attestation_type: 'fido-u2f'
|
||||
sign_count: 1
|
||||
clone_warning: false
|
||||
|
||||
@@ -396,6 +396,16 @@ var migrations = []Migration{
|
||||
NewMigration("Alter hook_task table TEXT fields to LONGTEXT", alterHookTaskTextFieldsToLongText),
|
||||
// v218 -> v219
|
||||
NewMigration("Improve Action table indices v2", improveActionTableIndices),
|
||||
// v219 -> v220
|
||||
NewMigration("Add sync_on_commit column to push_mirror table", addSyncOnCommitColForPushMirror),
|
||||
// v220 -> v221
|
||||
NewMigration("Add container repository property", addContainerRepositoryProperty),
|
||||
// v221 -> v222
|
||||
NewMigration("Store WebAuthentication CredentialID as bytes and increase size to at least 1024", storeWebauthnCredentialIDAsBytes),
|
||||
// v222 -> v223
|
||||
NewMigration("Drop old CredentialID column", dropOldCredentialIDColumn),
|
||||
// v223 -> v224
|
||||
NewMigration("Rename CredentialIDBytes column to CredentialID", renameCredentialIDBytes),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addSyncOnCommitColForPushMirror(x *xorm.Engine) error {
|
||||
type PushMirror struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
Repo *repo.Repository `xorm:"-"`
|
||||
RemoteName string
|
||||
|
||||
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"`
|
||||
Interval time.Duration
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
|
||||
LastError string `xorm:"text"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(PushMirror))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
func addContainerRepositoryProperty(x *xorm.Engine) error {
|
||||
switch x.Dialect().URI().DBType {
|
||||
case schemas.SQLITE:
|
||||
_, err := x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, u.lower_name || '/' || p.lower_name FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?", packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
_, err := x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, CONCAT(u.lower_name, '/', p.lower_name) FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?", packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func storeWebauthnCredentialIDAsBytes(x *xorm.Engine) error {
|
||||
// Create webauthnCredential table
|
||||
type webauthnCredential struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Name string
|
||||
LowerName string `xorm:"unique(s)"`
|
||||
UserID int64 `xorm:"INDEX unique(s)"`
|
||||
CredentialID string `xorm:"INDEX VARCHAR(410)"`
|
||||
// Note the lack of INDEX here - these will be created once the column is renamed in v223.go
|
||||
CredentialIDBytes []byte `xorm:"VARBINARY(1024)"` // CredentialID is at most 1023 bytes as per spec released 20 July 2022
|
||||
PublicKey []byte
|
||||
AttestationType string
|
||||
AAGUID []byte
|
||||
SignCount uint32 `xorm:"BIGINT"`
|
||||
CloneWarning bool
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
if err := x.Sync2(&webauthnCredential{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var start int
|
||||
creds := make([]*webauthnCredential, 0, 50)
|
||||
for {
|
||||
err := x.Select("id, credential_id").OrderBy("id").Limit(50, start).Find(&creds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return fmt.Errorf("unable to allow start session. Error: %w", err)
|
||||
}
|
||||
for _, cred := range creds {
|
||||
cred.CredentialIDBytes, err = base32.HexEncoding.DecodeString(cred.CredentialID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse credential id %s for credential[%d]: %w", cred.CredentialID, cred.ID, err)
|
||||
}
|
||||
count, err := sess.ID(cred.ID).Cols("credential_id_bytes").Update(cred)
|
||||
if count != 1 || err != nil {
|
||||
return fmt.Errorf("unable to update credential id bytes for credential[%d]: %d,%w", cred.ID, count, err)
|
||||
}
|
||||
}
|
||||
return sess.Commit()
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(creds) < 50 {
|
||||
break
|
||||
}
|
||||
start += 50
|
||||
creds = creds[:0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_storeWebauthnCredentialIDAsBytes(t *testing.T) {
|
||||
// Create webauthnCredential table
|
||||
type WebauthnCredential struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Name string
|
||||
LowerName string `xorm:"unique(s)"`
|
||||
UserID int64 `xorm:"INDEX unique(s)"`
|
||||
CredentialID string `xorm:"INDEX VARCHAR(410)"`
|
||||
PublicKey []byte
|
||||
AttestationType string
|
||||
AAGUID []byte
|
||||
SignCount uint32 `xorm:"BIGINT"`
|
||||
CloneWarning bool
|
||||
}
|
||||
|
||||
type ExpectedWebauthnCredential struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CredentialID string // CredentialID is at most 1023 bytes as per spec released 20 July 2022
|
||||
}
|
||||
|
||||
type ConvertedWebauthnCredential struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
CredentialIDBytes []byte `xorm:"VARBINARY(1024)"` // CredentialID is at most 1023 bytes as per spec released 20 July 2022
|
||||
}
|
||||
|
||||
// Prepare and load the testing database
|
||||
x, deferable := prepareTestEnv(t, 0, new(WebauthnCredential), new(ExpectedWebauthnCredential))
|
||||
defer deferable()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := storeWebauthnCredentialIDAsBytes(x); err != nil {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := []ExpectedWebauthnCredential{}
|
||||
if err := x.Table("expected_webauthn_credential").Asc("id").Find(&expected); !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
got := []ConvertedWebauthnCredential{}
|
||||
if err := x.Table("webauthn_credential").Select("id, credential_id_bytes").Asc("id").Find(&got); !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
for i, e := range expected {
|
||||
credIDBytes, _ := base32.HexEncoding.DecodeString(e.CredentialID)
|
||||
assert.Equal(t, credIDBytes, got[i].CredentialIDBytes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func dropOldCredentialIDColumn(x *xorm.Engine) error {
|
||||
// This migration maybe rerun so that we should check if it has been run
|
||||
credentialIDExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webauthn_credential", "credential_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !credentialIDExist {
|
||||
// Column is already non-extant
|
||||
return nil
|
||||
}
|
||||
credentialIDBytesExists, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webauthn_credential", "credential_id_bytes")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !credentialIDBytesExists {
|
||||
// looks like 221 hasn't properly run
|
||||
return fmt.Errorf("webauthn_credential does not have a credential_id_bytes column... it is not safe to run this migration")
|
||||
}
|
||||
|
||||
// Create webauthnCredential table
|
||||
type webauthnCredential struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Name string
|
||||
LowerName string `xorm:"unique(s)"`
|
||||
UserID int64 `xorm:"INDEX unique(s)"`
|
||||
CredentialID string `xorm:"INDEX VARCHAR(410)"`
|
||||
// Note the lack of the INDEX on CredentialIDBytes - we will add this in v223.go
|
||||
CredentialIDBytes []byte `xorm:"VARBINARY(1024)"` // CredentialID is at most 1023 bytes as per spec released 20 July 2022
|
||||
PublicKey []byte
|
||||
AttestationType string
|
||||
AAGUID []byte
|
||||
SignCount uint32 `xorm:"BIGINT"`
|
||||
CloneWarning bool
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
if err := x.Sync2(&webauthnCredential{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Drop the old credential ID
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if err := dropTableColumns(sess, "webauthn_credential", "credential_id"); err != nil {
|
||||
return fmt.Errorf("unable to drop old credentialID column: %w", err)
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func renameCredentialIDBytes(x *xorm.Engine) error {
|
||||
// This migration maybe rerun so that we should check if it has been run
|
||||
credentialIDExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webauthn_credential", "credential_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if credentialIDExist {
|
||||
credentialIDBytesExists, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webauthn_credential", "credential_id_bytes")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !credentialIDBytesExists {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
// webauthnCredential table
|
||||
type webauthnCredential struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Name string
|
||||
LowerName string `xorm:"unique(s)"`
|
||||
UserID int64 `xorm:"INDEX unique(s)"`
|
||||
// Note the lack of INDEX here
|
||||
CredentialIDBytes []byte `xorm:"VARBINARY(1024)"` // CredentialID is at most 1023 bytes as per spec released 20 July 2022
|
||||
PublicKey []byte
|
||||
AttestationType string
|
||||
AAGUID []byte
|
||||
SignCount uint32 `xorm:"BIGINT"`
|
||||
CloneWarning bool
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sess.Sync2(new(webauthnCredential)); err != nil {
|
||||
return fmt.Errorf("error on Sync2: %v", err)
|
||||
}
|
||||
|
||||
if credentialIDExist {
|
||||
// if both errors and message exist, drop message at first
|
||||
if err := dropTableColumns(sess, "webauthn_credential", "credential_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case setting.Database.UseMySQL:
|
||||
if _, err := sess.Exec("ALTER TABLE `webauthn_credential` CHANGE credential_id_bytes credential_id VARBINARY(1024)"); err != nil {
|
||||
return err
|
||||
}
|
||||
case setting.Database.UseMSSQL:
|
||||
if _, err := sess.Exec("sp_rename 'webauthn_credential.credential_id_bytes', 'credential_id', 'COLUMN'"); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if _, err := sess.Exec("ALTER TABLE `webauthn_credential` RENAME COLUMN credential_id_bytes TO credential_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return sess.Commit()
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create webauthnCredential table
|
||||
type webauthnCredential struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Name string
|
||||
LowerName string `xorm:"unique(s)"`
|
||||
UserID int64 `xorm:"INDEX unique(s)"`
|
||||
CredentialID []byte `xorm:"INDEX VARBINARY(1024)"` // CredentialID is at most 1023 bytes as per spec released 20 July 2022
|
||||
PublicKey []byte
|
||||
AttestationType string
|
||||
AAGUID []byte
|
||||
SignCount uint32 `xorm:"BIGINT"`
|
||||
CloneWarning bool
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
return x.Sync2(&webauthnCredential{})
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||
|
||||
"xorm.io/builder"
|
||||
@@ -210,6 +211,7 @@ func SearchImageTags(ctx context.Context, opts *ImageTagsSearchOptions) ([]*pack
|
||||
return pvs, count, err
|
||||
}
|
||||
|
||||
// SearchExpiredUploadedBlobs gets all uploaded blobs which are older than specified
|
||||
func SearchExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) ([]*packages.PackageFile, error) {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
"package_version.is_internal": true,
|
||||
@@ -225,3 +227,37 @@ func SearchExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) ([
|
||||
Where(cond).
|
||||
Find(&pfs)
|
||||
}
|
||||
|
||||
// GetRepositories gets a sorted list of all repositories
|
||||
func GetRepositories(ctx context.Context, actor *user_model.User, n int, last string) ([]string, error) {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
"package.type": packages.TypeContainer,
|
||||
"package_property.ref_type": packages.PropertyTypePackage,
|
||||
"package_property.name": container_module.PropertyRepository,
|
||||
}
|
||||
|
||||
cond = cond.And(builder.Exists(
|
||||
builder.
|
||||
Select("package_version.id").
|
||||
Where(builder.Eq{"package_version.is_internal": false}.And(builder.Expr("package.id = package_version.package_id"))).
|
||||
From("package_version"),
|
||||
))
|
||||
|
||||
if last != "" {
|
||||
cond = cond.And(builder.Gt{"package_property.value": strings.ToLower(last)})
|
||||
}
|
||||
|
||||
cond = cond.And(user_model.BuildCanSeeUserCondition(actor))
|
||||
|
||||
sess := db.GetEngine(ctx).
|
||||
Table("package").
|
||||
Select("package_property.value").
|
||||
Join("INNER", "user", "`user`.id = package.owner_id").
|
||||
Join("INNER", "package_property", "package_property.ref_id = package.id").
|
||||
Where(cond).
|
||||
Asc("package_property.value").
|
||||
Limit(n)
|
||||
|
||||
repositories := make([]string, 0, n)
|
||||
return repositories, sess.Find(&repositories)
|
||||
}
|
||||
|
||||
@@ -40,15 +40,16 @@ func (l PackagePropertyList) GetByName(name string) string {
|
||||
|
||||
// PackageDescriptor describes a package
|
||||
type PackageDescriptor struct {
|
||||
Package *Package
|
||||
Owner *user_model.User
|
||||
Repository *repo_model.Repository
|
||||
Version *PackageVersion
|
||||
SemVer *version.Version
|
||||
Creator *user_model.User
|
||||
Properties PackagePropertyList
|
||||
Metadata interface{}
|
||||
Files []*PackageFileDescriptor
|
||||
Package *Package
|
||||
Owner *user_model.User
|
||||
Repository *repo_model.Repository
|
||||
Version *PackageVersion
|
||||
SemVer *version.Version
|
||||
Creator *user_model.User
|
||||
PackageProperties PackagePropertyList
|
||||
VersionProperties PackagePropertyList
|
||||
Metadata interface{}
|
||||
Files []*PackageFileDescriptor
|
||||
}
|
||||
|
||||
// PackageFileDescriptor describes a package file
|
||||
@@ -102,6 +103,10 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
pps, err := GetProperties(ctx, PropertyTypePackage, p.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pvps, err := GetProperties(ctx, PropertyTypeVersion, pv.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -152,15 +157,16 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
}
|
||||
|
||||
return &PackageDescriptor{
|
||||
Package: p,
|
||||
Owner: o,
|
||||
Repository: repository,
|
||||
Version: pv,
|
||||
SemVer: semVer,
|
||||
Creator: creator,
|
||||
Properties: PackagePropertyList(pvps),
|
||||
Metadata: metadata,
|
||||
Files: pfds,
|
||||
Package: p,
|
||||
Owner: o,
|
||||
Repository: repository,
|
||||
Version: pv,
|
||||
SemVer: semVer,
|
||||
Creator: creator,
|
||||
PackageProperties: PackagePropertyList(pps),
|
||||
VersionProperties: PackagePropertyList(pvps),
|
||||
Metadata: metadata,
|
||||
Files: pfds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,12 @@ func TryInsertPackage(ctx context.Context, p *Package) (*Package, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// DeletePackageByID deletes a package by id
|
||||
func DeletePackageByID(ctx context.Context, packageID int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(packageID).Delete(&Package{})
|
||||
return err
|
||||
}
|
||||
|
||||
// SetRepositoryLink sets the linked repository
|
||||
func SetRepositoryLink(ctx context.Context, packageID, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(packageID).Cols("repo_id").Update(&Package{RepoID: repoID})
|
||||
@@ -192,21 +198,20 @@ func GetPackagesByType(ctx context.Context, ownerID int64, packageType Type) ([]
|
||||
Find(&ps)
|
||||
}
|
||||
|
||||
// DeletePackagesIfUnreferenced deletes a package if there are no associated versions
|
||||
func DeletePackagesIfUnreferenced(ctx context.Context) error {
|
||||
// FindUnreferencedPackages gets all packages without associated versions
|
||||
func FindUnreferencedPackages(ctx context.Context) ([]*Package, error) {
|
||||
in := builder.
|
||||
Select("package.id").
|
||||
From("package").
|
||||
LeftJoin("package_version", "package_version.package_id = package.id").
|
||||
Where(builder.Expr("package_version.id IS NULL"))
|
||||
|
||||
_, err := db.GetEngine(ctx).
|
||||
ps := make([]*Package, 0, 10)
|
||||
return ps, db.GetEngine(ctx).
|
||||
// double select workaround for MySQL
|
||||
// https://stackoverflow.com/questions/4471277/mysql-delete-from-with-subquery-as-condition
|
||||
Where(builder.In("package.id", builder.Select("id").From(in, "temp"))).
|
||||
Delete(&Package{})
|
||||
|
||||
return err
|
||||
Find(&ps)
|
||||
}
|
||||
|
||||
// HasOwnerPackages tests if a user/org has packages
|
||||
|
||||
@@ -21,9 +21,11 @@ const (
|
||||
PropertyTypeVersion PropertyType = iota // 0
|
||||
// PropertyTypeFile means the reference is a package file
|
||||
PropertyTypeFile // 1
|
||||
// PropertyTypePackage means the reference is a package
|
||||
PropertyTypePackage // 2
|
||||
)
|
||||
|
||||
// PackageProperty represents a property of a package version or file
|
||||
// PackageProperty represents a property of a package, version or file
|
||||
type PackageProperty struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RefType PropertyType `xorm:"INDEX NOT NULL"`
|
||||
@@ -68,3 +70,9 @@ func DeletePropertyByID(ctx context.Context, propertyID int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(propertyID).Delete(&PackageProperty{})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeletePropertyByName deletes properties by name
|
||||
func DeletePropertyByName(ctx context.Context, refType PropertyType, refID int64, name string) error {
|
||||
_, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{})
|
||||
return err
|
||||
}
|
||||
|
||||
+24
-25
@@ -58,31 +58,7 @@ func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session {
|
||||
cond = cond.And(builder.In("visibility", opts.Visible))
|
||||
}
|
||||
|
||||
if opts.Actor != nil {
|
||||
var exprCond builder.Cond = builder.Expr("org_user.org_id = `user`.id")
|
||||
|
||||
// If Admin - they see all users!
|
||||
if !opts.Actor.IsAdmin {
|
||||
// Force visibility for privacy
|
||||
var accessCond builder.Cond
|
||||
if !opts.Actor.IsRestricted {
|
||||
accessCond = builder.Or(
|
||||
builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))),
|
||||
builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
|
||||
} else {
|
||||
// restricted users only see orgs they are a member of
|
||||
accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID})))
|
||||
}
|
||||
// Don't forget about self
|
||||
accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID})
|
||||
cond = cond.And(accessCond)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Force visibility for privacy
|
||||
// Not logged in - only public users
|
||||
cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
|
||||
}
|
||||
cond = cond.And(BuildCanSeeUserCondition(opts.Actor))
|
||||
|
||||
if opts.UID > 0 {
|
||||
cond = cond.And(builder.Eq{"id": opts.UID})
|
||||
@@ -170,3 +146,26 @@ func IterateUser(f func(user *User) error) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BuildCanSeeUserCondition creates a condition which can be used to restrict results to users/orgs the actor can see
|
||||
func BuildCanSeeUserCondition(actor *User) builder.Cond {
|
||||
if actor != nil {
|
||||
// If Admin - they see all users!
|
||||
if !actor.IsAdmin {
|
||||
// Users can see an organization they are a member of
|
||||
cond := builder.In("`user`.id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": actor.ID}))
|
||||
if !actor.IsRestricted {
|
||||
// Not-Restricted users can see public and limited users/organizations
|
||||
cond = cond.Or(builder.In("`user`.visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
|
||||
}
|
||||
// Don't forget about self
|
||||
return cond.Or(builder.Eq{"`user`.id": actor.ID})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Force visibility for privacy
|
||||
// Not logged in - only public users
|
||||
return builder.In("`user`.visibility", structs.VisibleTypePublic)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
@@ -268,6 +269,7 @@ func APIContexter() func(http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform")
|
||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||
|
||||
ctx.Data["Context"] = &ctx
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
mc "code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -767,6 +768,7 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform")
|
||||
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||
|
||||
ctx.Data["CsrfToken"] = ctx.csrf.GetToken()
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
@@ -52,14 +53,30 @@ func packageAssignment(ctx *Context, errCb func(int, string, interface{})) {
|
||||
}
|
||||
|
||||
if ctx.Package.Owner.IsOrganization() {
|
||||
org := organization.OrgFromUser(ctx.Package.Owner)
|
||||
|
||||
// 1. Get user max authorize level for the org (may be none, if user is not member of the org)
|
||||
if ctx.Doer != nil {
|
||||
var err error
|
||||
ctx.Package.AccessMode, err = organization.OrgFromUser(ctx.Package.Owner).GetOrgUserMaxAuthorizeLevel(ctx.Doer.ID)
|
||||
ctx.Package.AccessMode, err = org.GetOrgUserMaxAuthorizeLevel(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
errCb(http.StatusInternalServerError, "GetOrgUserMaxAuthorizeLevel", err)
|
||||
return
|
||||
}
|
||||
// If access mode is less than write check every team for more permissions
|
||||
if ctx.Package.AccessMode < perm.AccessModeWrite {
|
||||
teams, err := organization.GetUserOrgTeams(ctx, org.ID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
errCb(http.StatusInternalServerError, "GetUserOrgTeams", err)
|
||||
return
|
||||
}
|
||||
for _, t := range teams {
|
||||
perm := t.UnitAccessModeCtx(ctx, unit.TypePackages)
|
||||
if ctx.Package.AccessMode < perm {
|
||||
ctx.Package.AccessMode = perm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. If authorize level is none, check if org is visible to user
|
||||
if ctx.Package.AccessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, ctx.Package.Owner, ctx.Doer) {
|
||||
|
||||
@@ -17,16 +17,23 @@ import (
|
||||
)
|
||||
|
||||
// AddCacheControlToHeader adds suitable cache-control headers to response
|
||||
func AddCacheControlToHeader(h http.Header, d time.Duration) {
|
||||
func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) {
|
||||
directives := make([]string, 0, 2+len(additionalDirectives))
|
||||
|
||||
if setting.IsProd {
|
||||
h.Set("Cache-Control", "private, max-age="+strconv.Itoa(int(d.Seconds())))
|
||||
if maxAge == 0 {
|
||||
directives = append(directives, "no-store")
|
||||
} else {
|
||||
directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds())))
|
||||
}
|
||||
} else {
|
||||
h.Set("Cache-Control", "no-store")
|
||||
directives = append(directives, "no-store")
|
||||
|
||||
// to remind users they are using non-prod setting.
|
||||
// some users may be confused by "Cache-Control: no-store" in their setup if they did wrong to `RUN_MODE` in `app.ini`.
|
||||
h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode)
|
||||
h.Add("X-Gitea-Debug", "CacheControl=no-store")
|
||||
}
|
||||
|
||||
h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", "))
|
||||
}
|
||||
|
||||
// generateETag generates an ETag based on size, filename and file modification time
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
goversion "github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -56,7 +55,9 @@ func NewRecipeReference(name, version, user, channel, revision string) (*RecipeR
|
||||
if !namePattern.MatchString(name) {
|
||||
return nil, ErrValidation
|
||||
}
|
||||
if _, err := goversion.NewSemver(version); err != nil {
|
||||
|
||||
v := strings.TrimSpace(version)
|
||||
if v == "" {
|
||||
return nil, ErrValidation
|
||||
}
|
||||
if user != "" && !namePattern.MatchString(user) {
|
||||
@@ -69,7 +70,7 @@ func NewRecipeReference(name, version, user, channel, revision string) (*RecipeR
|
||||
return nil, ErrValidation
|
||||
}
|
||||
|
||||
return &RecipeReference{name, version, user, channel, revision}, nil
|
||||
return &RecipeReference{name, v, user, channel, revision}, nil
|
||||
}
|
||||
|
||||
func (r *RecipeReference) RevisionOrDefault() string {
|
||||
|
||||
@@ -34,6 +34,7 @@ func TestNewRecipeReference(t *testing.T) {
|
||||
{"name", "1.0", "_", "_", "", true},
|
||||
{"name", "1.0", "_", "_", "0", true},
|
||||
{"name", "1.0", "", "", "0", true},
|
||||
{"name", "1.0.0q", "", "", "0", true},
|
||||
{"name", "1.0", "", "", "000000000000000000000000000000000000000000000000000000000000", false},
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
PropertyRepository = "container.repository"
|
||||
PropertyDigest = "container.digest"
|
||||
PropertyMediaType = "container.mediatype"
|
||||
PropertyManifestTagged = "container.manifest.tagged"
|
||||
|
||||
@@ -80,7 +80,6 @@ type gemspec struct {
|
||||
VersionRequirements requirement `yaml:"version_requirements"`
|
||||
} `yaml:"dependencies"`
|
||||
Description string `yaml:"description"`
|
||||
Email string `yaml:"email"`
|
||||
Executables []string `yaml:"executables"`
|
||||
Extensions []interface{} `yaml:"extensions"`
|
||||
ExtraRdocFiles []string `yaml:"extra_rdoc_files"`
|
||||
|
||||
+7
-2
@@ -11,6 +11,7 @@ import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -142,10 +143,14 @@ func sessionHandler(session ssh.Session) {
|
||||
// Wait for the command to exit and log any errors we get
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
log.Error("SSH: Wait: %v", err)
|
||||
// Cannot use errors.Is here because ExitError doesn't implement Is
|
||||
// Thus errors.Is will do equality test NOT type comparison
|
||||
if _, ok := err.(*exec.ExitError); !ok {
|
||||
log.Error("SSH: Wait: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := session.Exit(getExitStatusFromError(err)); err != nil {
|
||||
if err := session.Exit(getExitStatusFromError(err)); err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Error("Session failed to exit. %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ type SubmitPullReviewOptions struct {
|
||||
// DismissPullReviewOptions are options to dismiss a pull review
|
||||
type DismissPullReviewOptions struct {
|
||||
Message string `json:"message"`
|
||||
Priors bool `json:"priors"`
|
||||
}
|
||||
|
||||
// PullReviewRequestOptions are options to add or remove pull review requests
|
||||
|
||||
@@ -70,6 +70,16 @@ func (ct SniffedType) IsRepresentableAsText() bool {
|
||||
return ct.IsText() || ct.IsSvgImage()
|
||||
}
|
||||
|
||||
// IsBrowsableType returns whether a non-text type can be displayed in a browser
|
||||
func (ct SniffedType) IsBrowsableBinaryType() bool {
|
||||
return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio()
|
||||
}
|
||||
|
||||
// GetMimeType returns the mime type
|
||||
func (ct SniffedType) GetMimeType() string {
|
||||
return strings.SplitN(ct.contentType, ";", 2)[0]
|
||||
}
|
||||
|
||||
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty.
|
||||
func DetectContentType(data []byte) SniffedType {
|
||||
if len(data) == 0 {
|
||||
|
||||
@@ -3038,6 +3038,7 @@ title = Packages
|
||||
desc = Manage repository packages.
|
||||
empty = There are no packages yet.
|
||||
empty.documentation = For more information on the package registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/overview">the documentation</a>.
|
||||
empty.repo = Did you upload a package, but it's not shown here? Go to <a href="%[1]s">package settings</a> and link it to this repo.
|
||||
filter.type = Type
|
||||
filter.type.all = All
|
||||
filter.no_result = Your filter produced no results.
|
||||
|
||||
@@ -257,6 +257,7 @@ func ContainerRoutes() *web.Route {
|
||||
|
||||
r.Get("", container.ReqContainerAccess, container.DetermineSupport)
|
||||
r.Get("/token", container.Authenticate)
|
||||
r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList)
|
||||
r.Group("/{username}", func() {
|
||||
r.Group("/{image}", func() {
|
||||
r.Group("/blobs/uploads", func() {
|
||||
|
||||
@@ -88,7 +88,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
|
||||
|
||||
for _, pd := range pds {
|
||||
packageType := ""
|
||||
for _, pvp := range pd.Properties {
|
||||
for _, pvp := range pd.VersionProperties {
|
||||
if pvp.Name == composer_module.TypeProperty {
|
||||
packageType = pvp.Value
|
||||
break
|
||||
|
||||
@@ -225,7 +225,7 @@ func UploadPackage(ctx *context.Context) {
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: cp.Metadata,
|
||||
Properties: map[string]string{
|
||||
VersionProperties: map[string]string{
|
||||
composer_module.TypeProperty: cp.Type,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
|
||||
contentStore := packages_module.NewContentStore()
|
||||
|
||||
err := db.WithTx(func(ctx context.Context) error {
|
||||
created := true
|
||||
p := &packages_model.Package{
|
||||
OwnerID: pi.Owner.ID,
|
||||
Type: packages_model.TypeContainer,
|
||||
@@ -37,12 +38,21 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
|
||||
}
|
||||
var err error
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackage {
|
||||
if err == packages_model.ErrDuplicatePackage {
|
||||
created = false
|
||||
} else {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if created {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(pi.Owner.LowerName+"/"+pi.Name)); err != nil {
|
||||
log.Error("Error setting package property: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pv := &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: pi.Owner.ID,
|
||||
|
||||
@@ -112,7 +112,7 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
|
||||
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost for anonymous access)
|
||||
func ReqContainerAccess(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token"`)
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
|
||||
apiErrorDefined(ctx, errUnauthorized)
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,39 @@ func Authenticate(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// https://docs.docker.com/registry/spec/api/#listing-repositories
|
||||
func GetRepositoryList(ctx *context.Context) {
|
||||
n := ctx.FormInt("n")
|
||||
if n <= 0 || n > 100 {
|
||||
n = 100
|
||||
}
|
||||
last := ctx.FormTrim("last")
|
||||
|
||||
repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
type RepositoryList struct {
|
||||
Repositories []string `json:"repositories"`
|
||||
}
|
||||
|
||||
if len(repositories) == n {
|
||||
v := url.Values{}
|
||||
if n > 0 {
|
||||
v.Add("n", strconv.Itoa(n))
|
||||
}
|
||||
v.Add("last", repositories[len(repositories)-1])
|
||||
|
||||
ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode()))
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, RepositoryList{
|
||||
Repositories: repositories,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
|
||||
|
||||
@@ -267,6 +267,7 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H
|
||||
}
|
||||
|
||||
func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) {
|
||||
created := true
|
||||
p := &packages_model.Package{
|
||||
OwnerID: mci.Owner.ID,
|
||||
Type: packages_model.TypeContainer,
|
||||
@@ -275,12 +276,21 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
||||
}
|
||||
var err error
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackage {
|
||||
if err == packages_model.ErrDuplicatePackage {
|
||||
created = false
|
||||
} else {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if created {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(mci.Owner.LowerName+"/"+mci.Image)); err != nil {
|
||||
log.Error("Error setting package property: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
metadata.IsTagged = mci.IsTagged
|
||||
|
||||
metadataJSON, err := json.Marshal(metadata)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
@@ -15,8 +16,6 @@ import (
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -97,8 +96,7 @@ func UploadPackage(ctx *context.Context) {
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Creator: ctx.Doer,
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
@@ -157,10 +155,10 @@ func sanitizeParameters(ctx *context.Context) (string, string, string, error) {
|
||||
return "", "", "", errors.New("Invalid package name or filename")
|
||||
}
|
||||
|
||||
v, err := version.NewSemver(ctx.Params("packageversion"))
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
packageVersion := strings.TrimSpace(ctx.Params("packageversion"))
|
||||
if packageVersion == "" {
|
||||
return "", "", "", errors.New("Invalid package version")
|
||||
}
|
||||
|
||||
return packageName, v.String(), filename, nil
|
||||
return packageName, packageVersion, filename, nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
|
||||
for _, pd := range pds {
|
||||
versions[pd.SemVer.String()] = createPackageMetadataVersion(registryURL, pd)
|
||||
|
||||
for _, pvp := range pd.Properties {
|
||||
for _, pvp := range pd.VersionProperties {
|
||||
if pvp.Name == npm_module.TagProperty {
|
||||
distTags[pvp.Value] = pd.Version.Version
|
||||
}
|
||||
|
||||
@@ -823,7 +823,7 @@ func DismissPullReview(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
opts := web.GetForm(ctx).(*api.DismissPullReviewOptions)
|
||||
dismissReview(ctx, opts.Message, true)
|
||||
dismissReview(ctx, opts.Message, true, opts.Priors)
|
||||
}
|
||||
|
||||
// UnDismissPullReview cancel to dismiss a review for a pull request
|
||||
@@ -863,10 +863,10 @@ func UnDismissPullReview(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
dismissReview(ctx, "", false)
|
||||
dismissReview(ctx, "", false, false)
|
||||
}
|
||||
|
||||
func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
|
||||
func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) {
|
||||
if !ctx.Repo.IsAdmin() {
|
||||
ctx.Error(http.StatusForbidden, "", "Must be repo admin")
|
||||
return
|
||||
@@ -886,7 +886,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss)
|
||||
_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
|
||||
return
|
||||
|
||||
+57
-39
@@ -7,12 +7,13 @@ package common
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
charsetModule "code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
@@ -42,7 +43,7 @@ func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) err
|
||||
}
|
||||
|
||||
// ServeData download file from io.Reader
|
||||
func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
|
||||
func ServeData(ctx *context.Context, filePath string, size int64, reader io.Reader) error {
|
||||
buf := make([]byte, 1024)
|
||||
n, err := util.ReadAtMost(reader, buf)
|
||||
if err != nil {
|
||||
@@ -52,56 +53,73 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader)
|
||||
buf = buf[:n]
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
|
||||
httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute)
|
||||
|
||||
if size >= 0 {
|
||||
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
|
||||
} else {
|
||||
log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
|
||||
log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
|
||||
}
|
||||
name = path.Base(name)
|
||||
|
||||
// Google Chrome dislike commas in filenames, so let's change it to a space
|
||||
name = strings.ReplaceAll(name, ",", " ")
|
||||
fileName := path.Base(filePath)
|
||||
sniffedType := typesniffer.DetectContentType(buf)
|
||||
isPlain := sniffedType.IsText() || ctx.FormBool("render")
|
||||
mimeType := ""
|
||||
charset := ""
|
||||
|
||||
st := typesniffer.DetectContentType(buf)
|
||||
|
||||
mappedMimeType := ""
|
||||
if setting.MimeTypeMap.Enabled {
|
||||
fileExtension := strings.ToLower(filepath.Ext(name))
|
||||
mappedMimeType = setting.MimeTypeMap.Map[fileExtension]
|
||||
fileExtension := strings.ToLower(filepath.Ext(fileName))
|
||||
mimeType = setting.MimeTypeMap.Map[fileExtension]
|
||||
}
|
||||
if st.IsText() || ctx.FormBool("render") {
|
||||
cs, err := charset.DetectEncoding(buf)
|
||||
if err != nil {
|
||||
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
|
||||
cs = "utf-8"
|
||||
}
|
||||
if mappedMimeType == "" {
|
||||
mappedMimeType = "text/plain"
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", mappedMimeType+"; charset="+strings.ToLower(cs))
|
||||
} else {
|
||||
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
if mappedMimeType != "" {
|
||||
ctx.Resp.Header().Set("Content-Type", mappedMimeType)
|
||||
}
|
||||
if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
|
||||
if st.IsSvgImage() || st.IsPDF() {
|
||||
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
if st.IsSvgImage() {
|
||||
ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
|
||||
} else {
|
||||
ctx.Resp.Header().Set("Content-Type", typesniffer.ApplicationOctetStream)
|
||||
}
|
||||
}
|
||||
|
||||
if mimeType == "" {
|
||||
if sniffedType.IsBrowsableBinaryType() {
|
||||
mimeType = sniffedType.GetMimeType()
|
||||
} else if isPlain {
|
||||
mimeType = "text/plain"
|
||||
} else {
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
|
||||
mimeType = typesniffer.ApplicationOctetStream
|
||||
}
|
||||
}
|
||||
|
||||
if isPlain {
|
||||
charset, err = charsetModule.DetectEncoding(buf)
|
||||
if err != nil {
|
||||
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
|
||||
charset = "utf-8"
|
||||
}
|
||||
}
|
||||
|
||||
if charset != "" {
|
||||
ctx.Resp.Header().Set("Content-Type", mimeType+"; charset="+strings.ToLower(charset))
|
||||
} else {
|
||||
ctx.Resp.Header().Set("Content-Type", mimeType)
|
||||
}
|
||||
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
isSVG := sniffedType.IsSvgImage()
|
||||
|
||||
// serve types that can present a security risk with CSP
|
||||
if isSVG {
|
||||
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
} else if sniffedType.IsPDF() {
|
||||
// no sandbox attribute for pdf as it breaks rendering in at least safari. this
|
||||
// should generally be safe as scripts inside PDF can not escape the PDF document
|
||||
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
|
||||
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
||||
}
|
||||
|
||||
disposition := "inline"
|
||||
if isSVG && !setting.UI.SVG.Enabled {
|
||||
disposition = "attachment"
|
||||
}
|
||||
|
||||
// encode filename per https://datatracker.ietf.org/doc/html/rfc5987
|
||||
encodedFileName := `filename*=UTF-8''` + url.PathEscape(fileName)
|
||||
|
||||
ctx.Resp.Header().Set("Content-Disposition", disposition+"; "+encodedFileName)
|
||||
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
|
||||
_, err = ctx.Resp.Write(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -62,6 +63,7 @@ func installRecovery() func(next http.Handler) http.Handler {
|
||||
"SignedUserName": "",
|
||||
}
|
||||
|
||||
httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform")
|
||||
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||
|
||||
if !setting.IsProd {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
@@ -132,7 +131,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Success! Get the credential and update the sign count with the new value we received.
|
||||
dbCred, err := auth.GetWebAuthnCredentialByCredID(user.ID, base32.HexEncoding.EncodeToString(cred.ID))
|
||||
dbCred, err := auth.GetWebAuthnCredentialByCredID(user.ID, cred.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWebAuthnCredentialByCredID", err)
|
||||
return
|
||||
|
||||
@@ -158,6 +158,7 @@ func Recovery() func(next http.Handler) http.Handler {
|
||||
store["SignedUserName"] = ""
|
||||
}
|
||||
|
||||
httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform")
|
||||
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
|
||||
|
||||
if !setting.IsProd {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/org"
|
||||
container_service "code.gitea.io/gitea/services/packages/container"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
@@ -88,6 +89,12 @@ func SettingsPost(ctx *context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := container_service.UpdateRepositoryNames(ctx, org.AsUser(), form.Name); err != nil {
|
||||
ctx.ServerError("UpdateRepositoryNames", err)
|
||||
return
|
||||
}
|
||||
|
||||
// reset ctx.org.OrgLink with new name
|
||||
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(form.Name)
|
||||
log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -60,6 +61,9 @@ func Packages(ctx *context.Context) {
|
||||
ctx.Data["Query"] = query
|
||||
ctx.Data["PackageType"] = packageType
|
||||
ctx.Data["HasPackages"] = hasPackages
|
||||
if ctx.Repo != nil {
|
||||
ctx.Data["CanWritePackages"] = ctx.IsUserRepoWriter([]unit.Type{unit.TypePackages}) || ctx.IsUserSiteAdmin()
|
||||
}
|
||||
ctx.Data["PackageDescriptors"] = pds
|
||||
ctx.Data["Total"] = total
|
||||
ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
|
||||
|
||||
@@ -242,7 +242,7 @@ func SubmitReview(ctx *context.Context) {
|
||||
// DismissReview dismissing stale review by repo admin
|
||||
func DismissReview(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.DismissReviewForm)
|
||||
comm, err := pull_service.DismissReview(ctx, form.ReviewID, ctx.Repo.Repository.ID, form.Message, ctx.Doer, true)
|
||||
comm, err := pull_service.DismissReview(ctx, form.ReviewID, ctx.Repo.Repository.ID, form.Message, ctx.Doer, true, true)
|
||||
if err != nil {
|
||||
ctx.ServerError("pull_service.DismissReview", err)
|
||||
return
|
||||
|
||||
@@ -474,7 +474,7 @@ func SettingsPost(ctx *context.Context) {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
|
||||
}
|
||||
|
||||
if form.EnablePackages && !unit_model.TypeProjects.UnitGlobalDisabled() {
|
||||
if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: unit_model.TypePackages,
|
||||
|
||||
@@ -902,10 +902,14 @@ func renderCode(ctx *context.Context) {
|
||||
ctx.Data["PageIsViewCode"] = true
|
||||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
|
||||
if err != nil {
|
||||
ctx.ServerError("GitRepo.IsEmpty", err)
|
||||
return
|
||||
reallyEmpty := true
|
||||
var err error
|
||||
if ctx.Repo.GitRepo != nil {
|
||||
reallyEmpty, err = ctx.Repo.GitRepo.IsEmpty()
|
||||
if err != nil {
|
||||
ctx.ServerError("GitRepo.IsEmpty", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if reallyEmpty {
|
||||
ctx.HTML(http.StatusOK, tplRepoEMPTY)
|
||||
|
||||
@@ -591,6 +591,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
LabelIDs: opts.LabelIDs,
|
||||
Org: org,
|
||||
Team: team,
|
||||
RepoCond: opts.RepoCond,
|
||||
}
|
||||
|
||||
issueStats, err = issues_model.GetUserIssueStats(statsOpts)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
container_model "code.gitea.io/gitea/models/packages/container"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
@@ -91,6 +92,21 @@ func ListPackages(ctx *context.Context) {
|
||||
ctx.Data["Total"] = total
|
||||
ctx.Data["RepositoryAccessMap"] = repositoryAccessMap
|
||||
|
||||
// TODO: context/org -> HandleOrgAssignment() can not be used
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
org := org_model.OrgFromUser(ctx.ContextUser)
|
||||
ctx.Data["Org"] = org
|
||||
ctx.Data["OrgLink"] = ctx.ContextUser.OrganisationLink()
|
||||
|
||||
if ctx.Doer != nil {
|
||||
ctx.Data["IsOrganizationMember"], _ = org_model.IsOrganizationMember(ctx, org.ID, ctx.Doer.ID)
|
||||
ctx.Data["IsOrganizationOwner"], _ = org_model.IsOrganizationOwner(ctx, org.ID, ctx.Doer.ID)
|
||||
} else {
|
||||
ctx.Data["IsOrganizationMember"] = false
|
||||
ctx.Data["IsOrganizationOwner"] = false
|
||||
}
|
||||
}
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
|
||||
pager.AddParam(ctx, "q", "Query")
|
||||
pager.AddParam(ctx, "type", "PackageType")
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/agit"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
container_service "code.gitea.io/gitea/services/packages/container"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
@@ -90,6 +91,11 @@ func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName s
|
||||
return err
|
||||
}
|
||||
|
||||
if err := container_service.UpdateRepositoryNames(ctx, user, newName); err != nil {
|
||||
ctx.ServerError("UpdateRepositoryNames", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Trace("User name changed: %s -> %s", user.Name, newName)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,10 +6,13 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
container_model "code.gitea.io/gitea/models/packages/container"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
@@ -78,3 +81,25 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRepositoryNames updates the repository name property for all packages of the specific owner
|
||||
func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwnerName string) error {
|
||||
ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeContainer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newOwnerName = strings.ToLower(newOwnerName)
|
||||
|
||||
for _, p := range ps {
|
||||
if err := packages_model.DeletePropertyByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, newOwnerName+"/"+p.LowerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,10 +32,11 @@ type PackageInfo struct {
|
||||
// PackageCreationInfo describes a package to create
|
||||
type PackageCreationInfo struct {
|
||||
PackageInfo
|
||||
SemverCompatible bool
|
||||
Creator *user_model.User
|
||||
Metadata interface{}
|
||||
Properties map[string]string
|
||||
SemverCompatible bool
|
||||
Creator *user_model.User
|
||||
Metadata interface{}
|
||||
PackageProperties map[string]string
|
||||
VersionProperties map[string]string
|
||||
}
|
||||
|
||||
// PackageFileInfo describes a package file
|
||||
@@ -108,8 +109,9 @@ func createPackageAndAddFile(pvci *PackageCreationInfo, pfci *PackageFileCreatio
|
||||
}
|
||||
|
||||
func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, bool, error) {
|
||||
log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.Properties, allowDuplicate)
|
||||
log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.PackageProperties, pvci.VersionProperties, allowDuplicate)
|
||||
|
||||
packageCreated := true
|
||||
p := &packages_model.Package{
|
||||
OwnerID: pvci.Owner.ID,
|
||||
Type: pvci.PackageType,
|
||||
@@ -119,18 +121,29 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
|
||||
}
|
||||
var err error
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackage {
|
||||
if err == packages_model.ErrDuplicatePackage {
|
||||
packageCreated = false
|
||||
} else {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
if packageCreated {
|
||||
for name, value := range pvci.PackageProperties {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, name, value); err != nil {
|
||||
log.Error("Error setting package property: %v", err)
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadataJSON, err := json.Marshal(pvci.Metadata)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
created := true
|
||||
versionCreated := true
|
||||
pv := &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: pvci.Creator.ID,
|
||||
@@ -140,7 +153,7 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
|
||||
}
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
created = false
|
||||
versionCreated = false
|
||||
}
|
||||
if err != packages_model.ErrDuplicatePackageVersion || !allowDuplicate {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
@@ -148,8 +161,8 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
|
||||
}
|
||||
}
|
||||
|
||||
if created {
|
||||
for name, value := range pvci.Properties {
|
||||
if versionCreated {
|
||||
for name, value := range pvci.VersionProperties {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, name, value); err != nil {
|
||||
log.Error("Error setting package version property: %v", err)
|
||||
return nil, false, err
|
||||
@@ -157,7 +170,7 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
|
||||
}
|
||||
}
|
||||
|
||||
return pv, created, nil
|
||||
return pv, versionCreated, nil
|
||||
}
|
||||
|
||||
// AddFileToExistingPackage adds a file to an existing package. If the package does not exist, ErrPackageNotExist is returned
|
||||
@@ -348,9 +361,18 @@ func Cleanup(unused context.Context, olderThan time.Duration) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := packages_model.DeletePackagesIfUnreferenced(ctx); err != nil {
|
||||
ps, err := packages_model.FindUnreferencedPackages(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range ps {
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pbs, err := packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan)
|
||||
if err != nil {
|
||||
|
||||
@@ -124,6 +124,7 @@ func (e *errMergeConflict) Error() string {
|
||||
}
|
||||
|
||||
func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, gitRepo *git.Repository) error {
|
||||
log.Trace("Attempt to merge:\n%v", file)
|
||||
switch {
|
||||
case file.stage1 != nil && (file.stage2 == nil || file.stage3 == nil):
|
||||
// 1. Deleted in one or both:
|
||||
@@ -295,7 +296,8 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *
|
||||
var treeHash string
|
||||
treeHash, _, err = git.NewCommand(ctx, "write-tree").RunStdString(&git.RunOpts{Dir: tmpBasePath})
|
||||
if err != nil {
|
||||
return false, err
|
||||
lsfiles, _, _ := git.NewCommand(ctx, "ls-files", "-u").RunStdString(&git.RunOpts{Dir: tmpBasePath})
|
||||
return false, fmt.Errorf("unable to write unconflicted tree: %w\n`git ls-files -u`:\n%s", err, lsfiles)
|
||||
}
|
||||
treeHash = strings.TrimSpace(treeHash)
|
||||
baseTree, err := gitRepo.GetTree("base")
|
||||
|
||||
@@ -42,6 +42,17 @@ func (line *lsFileLine) SameAs(other *lsFileLine) bool {
|
||||
line.path == other.path
|
||||
}
|
||||
|
||||
// String provides a string representation for logging
|
||||
func (line *lsFileLine) String() string {
|
||||
if line == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
if line.err != nil {
|
||||
return fmt.Sprintf("%d %s %s %s %v", line.stage, line.mode, line.path, line.sha, line.err)
|
||||
}
|
||||
return fmt.Sprintf("%d %s %s %s", line.stage, line.mode, line.path, line.sha)
|
||||
}
|
||||
|
||||
// readUnmergedLsFileLines calls git ls-files -u -z and parses the lines into mode-sha-stage-path quadruplets
|
||||
// it will push these to the provided channel closing it at the end
|
||||
func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan chan *lsFileLine) {
|
||||
@@ -118,6 +129,17 @@ type unmergedFile struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// String provides a string representation of the an unmerged file for logging
|
||||
func (u *unmergedFile) String() string {
|
||||
if u == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
if u.err != nil {
|
||||
return fmt.Sprintf("error: %v\n%v\n%v\n%v", u.err, u.stage1, u.stage2, u.stage3)
|
||||
}
|
||||
return fmt.Sprintf("%v\n%v\n%v", u.stage1, u.stage2, u.stage3)
|
||||
}
|
||||
|
||||
// unmergedFiles will collate the output from readUnstagedLsFileLines in to file triplets and send them
|
||||
// to the provided channel, closing at the end.
|
||||
func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmergedFile) {
|
||||
@@ -138,6 +160,7 @@ func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmer
|
||||
|
||||
next := &unmergedFile{}
|
||||
for line := range lsFileLineChan {
|
||||
log.Trace("Got line: %v Current State:\n%v", line, next)
|
||||
if line.err != nil {
|
||||
log.Error("Unable to run ls-files -u -z! Error: %v", line.err)
|
||||
unmerged <- &unmergedFile{err: fmt.Errorf("unable to run ls-files -u -z! Error: %v", line.err)}
|
||||
@@ -149,7 +172,7 @@ func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmer
|
||||
case 0:
|
||||
// Should not happen as this represents successfully merged file - we will tolerate and ignore though
|
||||
case 1:
|
||||
if next.stage1 != nil {
|
||||
if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil {
|
||||
// We need to handle the unstaged file stage1,stage2,stage3
|
||||
unmerged <- next
|
||||
}
|
||||
|
||||
+18
-1
@@ -20,6 +20,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/notification"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// CreateCodeComment creates a comment on the code line
|
||||
@@ -271,7 +272,7 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
|
||||
}
|
||||
|
||||
// DismissReview dismissing stale review by repo admin
|
||||
func DismissReview(ctx context.Context, reviewID, repoID int64, message string, doer *user_model.User, isDismiss bool) (comment *issues_model.Comment, err error) {
|
||||
func DismissReview(ctx context.Context, reviewID, repoID int64, message string, doer *user_model.User, isDismiss, dismissPriors bool) (comment *issues_model.Comment, err error) {
|
||||
review, err := issues_model.GetReviewByID(ctx, reviewID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -295,6 +296,22 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
|
||||
return
|
||||
}
|
||||
|
||||
if dismissPriors {
|
||||
reviews, err := issues_model.GetReviews(ctx, &issues_model.GetReviewOptions{
|
||||
IssueID: review.IssueID,
|
||||
ReviewerID: review.ReviewerID,
|
||||
Dismissed: util.OptionalBoolFalse,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, oldReview := range reviews {
|
||||
if err = issues_model.DismissReview(oldReview, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isDismiss {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{AppSubUrl}}/notifications" class="item tooltip not-mobile" data-content='{{.i18n.Tr "notifications"}}'>
|
||||
<a href="{{AppSubUrl}}/notifications" class="item tooltip not-mobile" data-content="{{.i18n.Tr "notifications"}}" aria-label="{{.i18n.Tr "notifications"}}">
|
||||
<span class="text">
|
||||
<span class="fitted">{{svg "octicon-bell"}}</span>
|
||||
<span class="ui red label {{if not $notificationUnreadCount}}hidden{{end}} notification_count">
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
<div class="empty center">
|
||||
{{svg "octicon-package" 32}}
|
||||
<h2>{{.i18n.Tr "packages.empty"}}</h2>
|
||||
{{if and .Repository .CanWritePackages}}
|
||||
{{$packagesUrl := URLJoin .Owner.HTMLURL "-" "packages" }}
|
||||
<p>{{.i18n.Tr "packages.empty.repo" $packagesUrl | Safe}}</p>
|
||||
{{end}}
|
||||
<p>{{.i18n.Tr "packages.empty.documentation" | Safe}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
document.getElementById('repo-clone-url').value = btn ? btn.getAttribute('data-link') : '';
|
||||
})();
|
||||
</script>
|
||||
<button class="ui basic icon button tooltip" id="clipboard-btn" data-content="{{.i18n.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url">
|
||||
<button class="ui basic icon button tooltip" id="clipboard-btn" data-content="{{.i18n.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{.i18n.Tr "copy_url"}}">
|
||||
{{svg "octicon-paste"}}
|
||||
</button>
|
||||
|
||||
@@ -14803,6 +14803,10 @@
|
||||
"message": {
|
||||
"type": "string",
|
||||
"x-go-name": "Message"
|
||||
},
|
||||
"priors": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "Priors"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
<div class="header-wrapper">
|
||||
<div class="ui container">
|
||||
<div class="repo-header">
|
||||
<div class="repo-title-wrap df fc">
|
||||
<div class="repo-title">
|
||||
{{avatar .ContextUser 32}}
|
||||
<a href="{{.ContextUser.HTMLURL}}">{{.ContextUser.Name}}</a>
|
||||
<!-- TODO: make template org and user can share -->
|
||||
{{with .ContextUser}}
|
||||
<div class="ui container">
|
||||
<div class="ui vertically grid head">
|
||||
<div class="column">
|
||||
<div class="ui header">
|
||||
{{avatar . 100}}
|
||||
<span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span>
|
||||
<span class="org-visibility">
|
||||
{{if .Visibility.IsLimited}}<div class="ui medium orange horizontal label">{{$.i18n.Tr "org.settings.visibility.limited_shortname"}}</div>{{end}}
|
||||
{{if .Visibility.IsPrivate}}<div class="ui medium red horizontal label">{{$.i18n.Tr "org.settings.visibility.private_shortname"}}</div>{{end}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="ui tabs container">
|
||||
<div class="ui tabular stackable menu navbar">
|
||||
<a class="item" href="{{.ContextUser.HomeLink}}">
|
||||
@@ -19,6 +27,32 @@
|
||||
{{svg "octicon-package"}} {{.i18n.Tr "packages.title"}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .ContextUser.IsOrganization}}
|
||||
{{if .IsOrganizationMember}}
|
||||
<a class="item" href="{{$.OrgLink}}/members">
|
||||
{{svg "octicon-organization"}} {{$.i18n.Tr "org.people"}}
|
||||
{{if .NumMembers}}
|
||||
<div class="ui primary label">{{.NumMembers}}</div>
|
||||
{{end}}
|
||||
</a>
|
||||
<a class="item" href="{{$.OrgLink}}/teams">
|
||||
{{svg "octicon-people"}} {{$.i18n.Tr "org.teams"}}
|
||||
{{if .NumTeams}}
|
||||
<div class="ui black label">{{.NumTeams}}</div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .IsOrganizationOwner}}
|
||||
<div class="right menu">
|
||||
<a class="item" href="{{.OrgLink}}/settings">
|
||||
{{svg "octicon-tools"}} {{.i18n.Tr "repo.settings"}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui tabs divider"></div>
|
||||
|
||||
@@ -389,7 +389,8 @@ export function initGlobalButtons() {
|
||||
*/
|
||||
export function checkAppUrl() {
|
||||
const curUrl = window.location.href;
|
||||
if (curUrl.startsWith(appUrl)) {
|
||||
// some users visit "https://domain/gitea" while appUrl is "https://domain/gitea/", there should be no warning
|
||||
if (curUrl.startsWith(appUrl) || `${curUrl}/` === appUrl) {
|
||||
return;
|
||||
}
|
||||
if (document.querySelector('.page-content.install')) {
|
||||
|
||||
@@ -28,14 +28,12 @@ async function receiveUpdateCount(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
const notificationCount = document.querySelector('.notification_count');
|
||||
if (data.Count > 0) {
|
||||
notificationCount.classList.remove('hidden');
|
||||
} else {
|
||||
notificationCount.classList.add('hidden');
|
||||
const notificationCounts = document.querySelectorAll('.notification_count');
|
||||
for (const count of notificationCounts) {
|
||||
count.classList.toggle('hidden', data.Count === 0);
|
||||
count.textContent = `${data.Count}`;
|
||||
}
|
||||
|
||||
notificationCount.textContent = `${data.Count}`;
|
||||
await updateNotificationTable();
|
||||
} catch (error) {
|
||||
console.error(error, event);
|
||||
|
||||
@@ -2204,5 +2204,9 @@ table th[data-sortt-desc] {
|
||||
> .dropdown.item {
|
||||
position: initial;
|
||||
}
|
||||
|
||||
.menu {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,12 @@
|
||||
|
||||
.ui.dropdown {
|
||||
max-width: 100%;
|
||||
|
||||
@media @mediaSm {
|
||||
> .menu {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user