mirror of
https://github.com/go-gitea/gitea
synced 2026-02-05 13:31:45 +00:00
Compare commits
82 Commits
v1.25.1
...
release/v1.25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
885f2b89d6 | ||
|
|
57ce10c0ca | ||
|
|
25785041e7 | ||
|
|
ff3d11034d | ||
|
|
750649c1ef | ||
|
|
eb95bbc1fd | ||
|
|
369830bada | ||
|
|
d7d6533311 | ||
|
|
c326369f47 | ||
|
|
4cdb8a7f96 | ||
|
|
38125a8d1d | ||
|
|
175a425825 | ||
|
|
6132f639e7 | ||
|
|
dfe4055b92 | ||
|
|
5fe9703586 | ||
|
|
53d67dae28 | ||
|
|
ef6ab681f7 | ||
|
|
812a3cffb3 | ||
|
|
669b22100b | ||
|
|
a0c77673ff | ||
|
|
d96b68cbf5 | ||
|
|
f8ec5b3e43 | ||
|
|
11891c2dac | ||
|
|
2c778ff067 | ||
|
|
83ce45b186 | ||
|
|
39e83bd3fd | ||
|
|
c2f9edd673 | ||
|
|
aa575672ac | ||
|
|
e9c14723b6 | ||
|
|
76b6e94b5b | ||
|
|
163113d173 | ||
|
|
7d010c6932 | ||
|
|
b71e688634 | ||
|
|
e147a8223a | ||
|
|
9a7cfd8620 | ||
|
|
79f4cd754b | ||
|
|
522cc25921 | ||
|
|
a99ccfdf74 | ||
|
|
d448ab9ad4 | ||
|
|
c97b89a662 | ||
|
|
2dd8ef8368 | ||
|
|
432e128074 | ||
|
|
8d6442a43e | ||
|
|
3d66e75a47 | ||
|
|
e98d9bb93e | ||
|
|
a601c09826 | ||
|
|
b5f50ff63b | ||
|
|
b1b35e934e | ||
|
|
544450a212 | ||
|
|
0ab447005d | ||
|
|
52902d4ece | ||
|
|
0e91c8a068 | ||
|
|
45cdc5d8fd | ||
|
|
b276849cd8 | ||
|
|
46d1d154e8 | ||
|
|
f164e38e04 | ||
|
|
d4d338f1c1 | ||
|
|
f6895f632e | ||
|
|
eaa916a786 | ||
|
|
91901c2a60 | ||
|
|
20cf4b7849 | ||
|
|
5e7207d428 | ||
|
|
e3bfee80dd | ||
|
|
f93e2cf301 | ||
|
|
1b01d6de82 | ||
|
|
d67cd622d0 | ||
|
|
15f3e9d5a5 | ||
|
|
01fa8b2b7e | ||
|
|
1d9ae7ac23 | ||
|
|
01873a99c1 | ||
|
|
ce70863793 | ||
|
|
327f2207dc | ||
|
|
db876d8f17 | ||
|
|
2b71bf283b | ||
|
|
1ca4fef611 | ||
|
|
70ee6b9029 | ||
|
|
e5b404ec53 | ||
|
|
5842cd23a6 | ||
|
|
289bd9694b | ||
|
|
154d7521a5 | ||
|
|
24189dcced | ||
|
|
f84bf259ad |
@@ -25,6 +25,10 @@ insert_final_newline = false
|
||||
[templates/user/auth/oidc_wellknown.tmpl]
|
||||
indent_style = space
|
||||
|
||||
[templates/shared/actions/runner_badge_*.tmpl]
|
||||
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
|
||||
charset = unset
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
if: github.repository == 'go-gitea/gitea'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -190,7 +190,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- "9000:9000"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
- "993:993"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -218,7 +218,7 @@ jobs:
|
||||
- 10000:10000
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -4,6 +4,94 @@ This changelog goes through 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.com).
|
||||
|
||||
## [1.25.4](https://github.com/go-gitea/gitea/releases/tag/1.25.4) - 2026-01-15
|
||||
|
||||
* SECURITY
|
||||
* Release attachments must belong to the intended repo (#36347) (#36375)
|
||||
* Fix permission check on org project operations (#36318) (#36373)
|
||||
* Clean watches when make a repository private and check permission when send release emails (#36319) (#36370)
|
||||
* Add more check for stopwatch read or list (#36340) (#36368)
|
||||
* Fix openid setting check (#36346) (#36361)
|
||||
* Fix cancel auto merge bug (#36341) (#36356)
|
||||
* Fix delete attachment check (#36320) (#36355)
|
||||
* LFS locks must belong to the intended repo (#36344) (#36349)
|
||||
* Fix bug on notification read (#36339) #36387
|
||||
* ENHANCEMENTS
|
||||
* Add more routes to the "expensive" list (#36290)
|
||||
* Make "commit statuses" API accept slashes in "ref" (#36264) (#36275)
|
||||
* BUGFIXES
|
||||
* Fix git http service handling #36396
|
||||
* Fix markdown newline handling during IME composition (#36421) #36424
|
||||
* Fix missing repository id when migrating release attachments (#36389)
|
||||
* Fix bug when compare in the pull request (#36363) (#36372)
|
||||
* Fix incorrect text content detection (#36364) (#36369)
|
||||
* Fill missing `has_code` in repository api (#36338) (#36359)
|
||||
* Fix notifications pagination query parameters (#36351) (#36358)
|
||||
* Fix some trivial problems (#36336) (#36337)
|
||||
* Prevent panic when GitLab release has more links than sources (#36295) (#36305)
|
||||
* Fix stats bug when syncing release (#36285) (#36294)
|
||||
* Always honor user's choice for "delete branch after merge" (#36281) (#36286)
|
||||
* Use the requested host for LFS links (#36242) (#36258)
|
||||
* Fix panic when get editor config file (#36241) (#36247)
|
||||
* Fix regression in writing authorized principals (#36213) (#36218)
|
||||
* Fix WebAuthn error checking (#36219) (#36235)
|
||||
|
||||
## [1.25.3](https://github.com/go-gitea/gitea/releases/tag/1.25.3) - 2025-12-17
|
||||
|
||||
* SECURITY
|
||||
* Bump toolchain to go1.25.5, misc fixes (#36082)
|
||||
* ENHANCEMENTS
|
||||
* Add strikethrough button to markdown editor (#36087) (#36104)
|
||||
* Add "site admin" back to profile menu (#36010) (#36013)
|
||||
* Improve math rendering (#36124) (#36125)
|
||||
* BUGFIXES
|
||||
* Check user visibility when redirecting to a renamed user (#36148) (#36159)
|
||||
* Fix various bugs (#36139) (#36151)
|
||||
* Fix bug when viewing the commit diff page with non-ANSI files (#36149) (#36150)
|
||||
* Hide RSS icon when viewing a file not under a branch (#36135) (#36141)
|
||||
* Fix SVG size calulation, only use `style` attribute (#36133) (#36134)
|
||||
* Make Golang correctly delete temp files during uploading (#36128) (#36129)
|
||||
* Fix the bug when ssh clone with redirect user or repository (#36039) (#36090)
|
||||
* Use Golang net/smtp instead of gomail's smtp to send email (#36055) (#36083)
|
||||
* Fix edit user email bug in API (#36068) (#36081)
|
||||
* Fix bug when updating user email (#36058) (#36066)
|
||||
* Fix incorrect viewed files counter if file has changed (#36009) (#36047)
|
||||
* Fix container registry error handling (#36021) (#36037)
|
||||
* Fix webAuthn insecure error view (#36165) (#36179)
|
||||
* Fix some file icon ui (#36078) (#36088)
|
||||
* Fix Actions `pull_request.paths` being triggered incorrectly by rebase (#36045) (#36054)
|
||||
* Fix error handling in mailer and wiki services (#36041) (#36053)
|
||||
* Fix bugs when comparing and creating pull request (#36166) (#36144)
|
||||
|
||||
## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/1.25.2) - 2025-11-23
|
||||
|
||||
* SECURITY
|
||||
* Upgrade golang.org/x/crypto to 0.45.0 (#35985) (#35988)
|
||||
* Fix various permission & login related bugs (#36002) (#36004)
|
||||
* ENHANCEMENTS
|
||||
* Display source code downloads last for release attachments (#35897) (#35903)
|
||||
* Change project default column icon to 'star' (#35967) (#35979)
|
||||
* BUGFIXES
|
||||
* Allow empty commit when merging pull request with squash style (#35989) (#36003)
|
||||
* Fix container push tag overwriting (#35936) (#35954)
|
||||
* Fix corrupted external render content (#35946) and upgrade golang.org/x packages (#35950)
|
||||
* Limit reading bytes instead of ReadAll (#35928) (#35934)
|
||||
* Use correct form field for allowed force push users in branch protection API (#35894) (#35908)
|
||||
* Fix team member access check (#35899) (#35905)
|
||||
* Fix conda null depend issue (#35900) (#35902)
|
||||
* Set the dates to now when not specified by the caller (#35861) (#35874)
|
||||
* Fix gogit ListEntriesRecursiveWithSize (#35862)
|
||||
* Misc CSS fixes (#35888) (#35981)
|
||||
* Don't show unnecessary error message to end users for DeleteBranchAfterMerge (#35937) (#35941)
|
||||
* Load jQuery as early as possible to support custom scripts (#35926) (#35929)
|
||||
* Allow to display embed images/pdfs when SERVE_DIRECT was enabled on MinIO storage (#35882) (#35917)
|
||||
* Make OAuth2 issuer configurable (#35915) (#35916)
|
||||
* Fix #35763: Add proper page title for project pages (#35773) (#35909)
|
||||
* Fix avatar upload error handling (#35887) (#35890)
|
||||
* Contribution heatmap improvements (#35876) (#35880)
|
||||
* Remove padding override on `.ui .sha.label` (#35864) (#35873)
|
||||
* Fix pull description code label background (#35865) (#35870)
|
||||
|
||||
## [1.25.1](https://github.com/go-gitea/gitea/releases/tag/v1.25.1) - 2025-11-03
|
||||
|
||||
* BUGFIXES
|
||||
|
||||
@@ -35,7 +35,7 @@ SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@717e3cb29becaaf0
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.8
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
|
||||
GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0
|
||||
|
||||
|
||||
+13
-11
@@ -163,6 +163,14 @@ func (n *nilWriter) WriteString(s string) (int, error) {
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) {
|
||||
fields := strings.Split(line, " ")
|
||||
if len(fields) != 3 {
|
||||
return "", "", "", false
|
||||
}
|
||||
return fields[0], fields[1], git.RefName(fields[2]), true
|
||||
}
|
||||
|
||||
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
@@ -228,14 +236,11 @@ Gitea or set your environment appropriately.`, "")
|
||||
continue
|
||||
}
|
||||
|
||||
fields := bytes.Fields(scanner.Bytes())
|
||||
if len(fields) != 3 {
|
||||
oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
oldCommitID := string(fields[0])
|
||||
newCommitID := string(fields[1])
|
||||
refFullName := git.RefName(fields[2])
|
||||
total++
|
||||
lastline++
|
||||
|
||||
@@ -378,16 +383,13 @@ Gitea or set your environment appropriately.`, "")
|
||||
continue
|
||||
}
|
||||
|
||||
fields := bytes.Fields(scanner.Bytes())
|
||||
if len(fields) != 3 {
|
||||
var ok bool
|
||||
oldCommitIDs[count], newCommitIDs[count], refFullNames[count], ok = parseGitHookCommitRefLine(scanner.Text())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, ".")
|
||||
oldCommitIDs[count] = string(fields[0])
|
||||
newCommitIDs[count] = string(fields[1])
|
||||
refFullNames[count] = git.RefName(fields[2])
|
||||
|
||||
commitID, _ := git.NewIDFromString(newCommitIDs[count])
|
||||
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
|
||||
masterPushed = true
|
||||
|
||||
@@ -39,3 +39,17 @@ func TestPktLine(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("0007a\nb"), w.Bytes())
|
||||
}
|
||||
|
||||
func TestParseGitHookCommitRefLine(t *testing.T) {
|
||||
oldCommitID, newCommitID, refName, ok := parseGitHookCommitRefLine("a b c")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "a", oldCommitID)
|
||||
assert.Equal(t, "b", newCommitID)
|
||||
assert.Equal(t, "c", string(refName))
|
||||
|
||||
_, _, _, ok = parseGitHookCommitRefLine("a\tb\tc")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, _, _, ok = parseGitHookCommitRefLine("a b")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
@@ -567,6 +567,11 @@ ENABLED = true
|
||||
;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one
|
||||
;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret
|
||||
;;
|
||||
;; The "issuer" claim identifies the principal that issued the JWT.
|
||||
;; Gitea 1.25 makes it default to "ROOT_URL without the last slash" to follow the standard.
|
||||
;; If you have old logins from before 1.25, you may want to set it to the old (non-standard) value "ROOT_URL with the last slash".
|
||||
;JWT_CLAIM_ISSUER =
|
||||
;;
|
||||
;; Lifetime of an OAuth2 access token in seconds
|
||||
;ACCESS_TOKEN_EXPIRATION_TIME = 3600
|
||||
;;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
module code.gitea.io/gitea
|
||||
|
||||
go 1.25.3
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.25.6
|
||||
|
||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||
// But some CAs use negative serial number, just relax the check. related:
|
||||
@@ -116,13 +118,13 @@ require (
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
gitlab.com/gitlab-org/api/client-go v0.142.4
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/image v0.30.0
|
||||
golang.org/x/net v0.44.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/text v0.30.0
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/text v0.31.0
|
||||
google.golang.org/grpc v1.75.0
|
||||
google.golang.org/protobuf v1.36.8
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
@@ -279,9 +281,9 @@ require (
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
@@ -840,8 +840,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -878,8 +878,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -908,8 +908,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -932,8 +932,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -975,8 +975,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -987,8 +987,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1002,8 +1002,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
@@ -1039,8 +1039,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -50,12 +51,42 @@ func WriteAuthorizedStringForValidKey(key *PublicKey, w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
principalRegexp *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
// principalRegexp expresses whether a principal is considered valid.
|
||||
// This reverse engineers how sshd parses the authorized keys file,
|
||||
// see e.g. https://github.com/openssh/openssh-portable/blob/32deb00b38b4ee2b3302f261ea1e68c04e020a08/auth2-pubkeyfile.c#L221-L256
|
||||
// Any newline or # comment will be stripped when parsing, so don't allow
|
||||
// those. Also, if any space or tab is present in the principal, the part
|
||||
// proceeding this would be parsed as an option, so just avoid any whitespace
|
||||
// altogether.
|
||||
ret.principalRegexp = regexp.MustCompile(`^[^\s#]+$`)
|
||||
return ret
|
||||
})
|
||||
|
||||
func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) {
|
||||
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s %s` + "\n"
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||
if err != nil {
|
||||
return false, err
|
||||
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n"
|
||||
|
||||
var sshKey string
|
||||
|
||||
if key.Type == KeyTypePrincipal {
|
||||
// TODO: actually using PublicKey to store "principal" is an abuse
|
||||
if !globalVars().principalRegexp.MatchString(key.Content) {
|
||||
return false, fmt.Errorf("invalid principal key: %s", key.Content)
|
||||
}
|
||||
sshKey = fmt.Sprintf("%s # user-%d", key.Content, key.OwnerID)
|
||||
} else {
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
||||
sshKey = fmt.Sprintf("%s user-%d", sshKeyMarshalled, key.OwnerID)
|
||||
}
|
||||
|
||||
// now the key is valid, the code below could only return template/IO related errors
|
||||
sbCmd := &strings.Builder{}
|
||||
err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{
|
||||
@@ -69,9 +100,7 @@ func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, er
|
||||
return true, err
|
||||
}
|
||||
sshCommandEscaped := util.ShellEscape(sbCmd.String())
|
||||
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
||||
sshKeyComment := fmt.Sprintf("user-%d", key.OwnerID)
|
||||
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKeyMarshalled, sshKeyComment)
|
||||
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKey)
|
||||
return true, err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package asymkey
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWriteAuthorizedStringForKey(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppPath, "/tmp/gitea")()
|
||||
defer test.MockVariableValue(&setting.CustomConf, "/tmp/app.ini")()
|
||||
writeKey := func(t *testing.T, key *PublicKey) (bool, string, error) {
|
||||
sb := &strings.Builder{}
|
||||
valid, err := writeAuthorizedStringForKey(key, sb)
|
||||
return valid, sb.String(), err
|
||||
}
|
||||
const validKeyContent = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf`
|
||||
|
||||
testValid := func(t *testing.T, key *PublicKey, expected string) {
|
||||
valid, content, err := writeKey(t, key)
|
||||
assert.True(t, valid)
|
||||
assert.Equal(t, expected, content)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
testInvalid := func(t *testing.T, key *PublicKey) {
|
||||
valid, content, err := writeKey(t, key)
|
||||
assert.False(t, valid)
|
||||
assert.Empty(t, content)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
t.Run("PublicKey", func(t *testing.T) {
|
||||
testValid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: validKeyContent + " any-comment",
|
||||
Type: KeyTypeUser,
|
||||
}, `# gitea public key
|
||||
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
|
||||
`)
|
||||
})
|
||||
|
||||
t.Run("PublicKeyWithNewLine", func(t *testing.T) {
|
||||
testValid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: validKeyContent + "\nany-more", // the new line should be ignored
|
||||
Type: KeyTypeUser,
|
||||
}, `# gitea public key
|
||||
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
|
||||
`)
|
||||
})
|
||||
|
||||
t.Run("PublicKeyInvalid", func(t *testing.T) {
|
||||
testInvalid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: validKeyContent + "any-more",
|
||||
Type: KeyTypeUser,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Principal", func(t *testing.T) {
|
||||
testValid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: "any-content",
|
||||
Type: KeyTypePrincipal,
|
||||
}, `# gitea public key
|
||||
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict any-content # user-123
|
||||
`)
|
||||
})
|
||||
|
||||
t.Run("PrincipalInvalid", func(t *testing.T) {
|
||||
testInvalid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: "a b",
|
||||
Type: KeyTypePrincipal,
|
||||
})
|
||||
testInvalid(t, &PublicKey{
|
||||
OwnerID: 123,
|
||||
Content: "a\nb",
|
||||
Type: KeyTypePrincipal,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -108,10 +108,10 @@ func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// GetLFSLockByID returns release by given id.
|
||||
func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) {
|
||||
// GetLFSLockByIDAndRepo returns lfs lock by given id and repository id.
|
||||
func GetLFSLockByIDAndRepo(ctx context.Context, id, repoID int64) (*LFSLock, error) {
|
||||
lock := new(LFSLock)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(lock)
|
||||
has, err := db.GetEngine(ctx).ID(id).And("repo_id = ?", repoID).Get(lock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@@ -160,7 +160,7 @@ func CountLFSLockByRepoID(ctx context.Context, repoID int64) (int64, error) {
|
||||
// DeleteLFSLockByID deletes a lock by given ID.
|
||||
func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
|
||||
lock, err := GetLFSLockByID(ctx, id)
|
||||
lock, err := GetLFSLockByIDAndRepo(ctx, id, repo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createTestLock(t *testing.T, repo *repo_model.Repository, owner *user_model.User) *LFSLock {
|
||||
t.Helper()
|
||||
|
||||
path := fmt.Sprintf("%s-%d-%d", t.Name(), repo.ID, time.Now().UnixNano())
|
||||
lock, err := CreateLFSLock(t.Context(), repo, &LFSLock{
|
||||
OwnerID: owner.ID,
|
||||
Path: path,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return lock
|
||||
}
|
||||
|
||||
func TestGetLFSLockByIDAndRepo(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
lockRepo1 := createTestLock(t, repo1, user2)
|
||||
lockRepo3 := createTestLock(t, repo3, user4)
|
||||
|
||||
fetched, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo1.ID, fetched.ID)
|
||||
assert.Equal(t, repo1.ID, fetched.RepoID)
|
||||
|
||||
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo3.ID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrLFSLockNotExist(err))
|
||||
|
||||
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo1.ID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrLFSLockNotExist(err))
|
||||
}
|
||||
|
||||
func TestDeleteLFSLockByIDRequiresRepoMatch(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
lockRepo1 := createTestLock(t, repo1, user2)
|
||||
lockRepo3 := createTestLock(t, repo3, user4)
|
||||
|
||||
_, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo1, user2, true)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrLFSLockNotExist(err))
|
||||
|
||||
existing, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo3.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo3.ID, existing.ID)
|
||||
|
||||
deleted, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo3, user4, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo3.ID, deleted.ID)
|
||||
|
||||
deleted, err = DeleteLFSLockByID(t.Context(), lockRepo1.ID, repo1, user2, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo1.ID, deleted.ID)
|
||||
}
|
||||
@@ -466,11 +466,13 @@ func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, c
|
||||
return currentWhitelist, nil
|
||||
}
|
||||
|
||||
prUserIDs, err := access_model.GetUserIDsWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
whitelist = make([]int64, 0, len(newWhitelist))
|
||||
for _, userID := range newWhitelist {
|
||||
if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil {
|
||||
return nil, err
|
||||
} else if !reader {
|
||||
if !prUserIDs.Contains(userID) {
|
||||
continue
|
||||
}
|
||||
whitelist = append(whitelist, userID)
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Stopwatch represents a stopwatch for time tracking.
|
||||
@@ -232,3 +234,14 @@ func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (
|
||||
})
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// RemoveStopwatchesByRepoID removes all stopwatches for a user in a specific repository
|
||||
// this function should be called before removing all the issues of the repository
|
||||
func RemoveStopwatchesByRepoID(ctx context.Context, userID, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Where("`stopwatch`.user_id = ?", userID).
|
||||
And(builder.In("`stopwatch`.issue_id",
|
||||
builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repoID}))).
|
||||
Delete(new(Stopwatch))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -53,24 +53,45 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
|
||||
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
||||
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
|
||||
teams := make([]*Team, 0, 5)
|
||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) {
|
||||
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(teamIDs) == 0 {
|
||||
return teams, nil
|
||||
}
|
||||
err = db.GetEngine(ctx).Where(builder.In("id", teamIDs)).OrderBy("team.name").Find(&teams)
|
||||
return teams, err
|
||||
}
|
||||
|
||||
func getTeamIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teamIDs []int64, err error) {
|
||||
sub := builder.Select("team_id").From("team_unit").
|
||||
Where(builder.Expr("team_unit.team_id = team.id")).
|
||||
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
||||
And(builder.Expr("team_unit.access_mode >= ?", mode))
|
||||
|
||||
err := db.GetEngine(ctx).
|
||||
err = db.GetEngine(ctx).
|
||||
Select("team.id").
|
||||
Table("team").
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
And("team_repo.org_id = ?", orgID).
|
||||
And("team_repo.repo_id = ?", repoID).
|
||||
And("team_repo.org_id = ? AND team_repo.repo_id = ?", orgID, repoID).
|
||||
And(builder.Or(
|
||||
builder.Expr("team.authorize >= ?", mode),
|
||||
builder.In("team.id", sub),
|
||||
)).
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
|
||||
return teams, err
|
||||
Find(&teamIDs)
|
||||
return teamIDs, err
|
||||
}
|
||||
|
||||
func GetTeamUserIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (userIDs []int64, err error) {
|
||||
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(teamIDs) == 0 {
|
||||
return userIDs, nil
|
||||
}
|
||||
err = db.GetEngine(ctx).Table("team_user").Select("uid").Where(builder.In("team_id", teamIDs)).Find(&userIDs)
|
||||
return userIDs, err
|
||||
}
|
||||
|
||||
@@ -43,13 +43,15 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
|
||||
|
||||
existing := &PackageBlob{}
|
||||
|
||||
has, err := e.Where(builder.Eq{
|
||||
hashCond := builder.Eq{
|
||||
"size": pb.Size,
|
||||
"hash_md5": pb.HashMD5,
|
||||
"hash_sha1": pb.HashSHA1,
|
||||
"hash_sha256": pb.HashSHA256,
|
||||
"hash_sha512": pb.HashSHA512,
|
||||
}).Get(existing)
|
||||
}
|
||||
|
||||
has, err := e.Where(hashCond).Get(existing)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -57,6 +59,11 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
|
||||
return existing, true, nil
|
||||
}
|
||||
if _, err = e.Insert(pb); err != nil {
|
||||
// Handle race condition: another request may have inserted the same blob
|
||||
// between our SELECT and INSERT. Retry the SELECT to get the existing blob.
|
||||
if has, _ = e.Where(hashCond).Get(existing); has {
|
||||
return existing, true, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
return pb, false, nil
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func TestGetOrInsertBlobConcurrent(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testBlob := PackageBlob{
|
||||
Size: 123,
|
||||
HashMD5: "md5",
|
||||
HashSHA1: "sha1",
|
||||
HashSHA256: "sha256",
|
||||
HashSHA512: "sha512",
|
||||
}
|
||||
|
||||
const numGoroutines = 3
|
||||
var wg errgroup.Group
|
||||
results := make([]*PackageBlob, numGoroutines)
|
||||
existed := make([]bool, numGoroutines)
|
||||
for idx := range numGoroutines {
|
||||
wg.Go(func() error {
|
||||
blob := testBlob // Create a copy of the test blob for each goroutine
|
||||
var err error
|
||||
results[idx], existed[idx], err = GetOrInsertBlob(t.Context(), &blob)
|
||||
return err
|
||||
})
|
||||
}
|
||||
require.NoError(t, wg.Wait())
|
||||
|
||||
// then: all GetOrInsertBlob succeeds with the same blob ID, and only one indicates it did not exist before
|
||||
existedCount := 0
|
||||
assert.NotNil(t, results[0])
|
||||
for i := range numGoroutines {
|
||||
assert.Equal(t, results[0].ID, results[i].ID)
|
||||
if existed[i] {
|
||||
existedCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, numGoroutines-1, existedCount)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -458,54 +459,44 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi
|
||||
return perm.HasAnyUnitAccess(), nil
|
||||
}
|
||||
|
||||
// getUsersWithAccessMode returns users that have at least given access mode to the repository.
|
||||
func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) {
|
||||
if err = repo.LoadOwner(ctx); err != nil {
|
||||
func GetUsersWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (users []*user_model.User, err error) {
|
||||
userIDs, err := GetUserIDsWithUnitAccess(ctx, repo, mode, unitType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
accesses := make([]*Access, 0, 10)
|
||||
if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
||||
if len(userIDs) == 0 {
|
||||
return users, nil
|
||||
}
|
||||
if err = db.GetEngine(ctx).In("id", userIDs.Values()).OrderBy("`name`").Find(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Leave a seat for owner itself to append later, but if owner is an organization
|
||||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
||||
users := make([]*user_model.User, 0, len(accesses)+1)
|
||||
if len(accesses) > 0 {
|
||||
userIDs := make([]int64, len(accesses))
|
||||
for i := 0; i < len(accesses); i++ {
|
||||
userIDs[i] = accesses[i].UserID
|
||||
}
|
||||
|
||||
if err = e.In("id", userIDs).Find(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !repo.Owner.IsOrganization() {
|
||||
users = append(users, repo.Owner)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetRepoReaders returns all users that have explicit read access or higher to the repository.
|
||||
func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead)
|
||||
}
|
||||
|
||||
// GetRepoWriters returns all users that have write access to the repository.
|
||||
func GetRepoWriters(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeWrite)
|
||||
}
|
||||
|
||||
// IsRepoReader returns true if user has explicit read access or higher to the repository.
|
||||
func IsRepoReader(ctx context.Context, repo *repo_model.Repository, userID int64) (bool, error) {
|
||||
if repo.OwnerID == userID {
|
||||
return true, nil
|
||||
func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (container.Set[int64], error) {
|
||||
userIDs := container.Set[int64]{}
|
||||
e := db.GetEngine(ctx)
|
||||
accesses := make([]*Access, 0, 10)
|
||||
if err := e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db.GetEngine(ctx).Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, perm_model.AccessModeRead).Get(&Access{})
|
||||
for _, a := range accesses {
|
||||
userIDs.Add(a.UserID)
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !repo.Owner.IsOrganization() {
|
||||
userIDs.Add(repo.Owner.ID)
|
||||
} else {
|
||||
teamUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, mode, unitType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs.AddMultiple(teamUserIDs...)
|
||||
}
|
||||
return userIDs, nil
|
||||
}
|
||||
|
||||
// CheckRepoUnitUser check whether user could visit the unit of this repository
|
||||
|
||||
@@ -169,9 +169,9 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
|
||||
require.NoError(t, db.Insert(ctx, team))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||
|
||||
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
@@ -219,6 +219,15 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||
|
||||
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeRead, unit.TypeIssues)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 1)
|
||||
assert.Equal(t, user.ID, users[0].ID)
|
||||
|
||||
users, err = GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, users)
|
||||
})
|
||||
|
||||
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||
@@ -229,5 +238,10 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues])
|
||||
|
||||
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users, 1)
|
||||
assert.Equal(t, user.ID, users[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,6 +213,18 @@ func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
|
||||
return column, nil
|
||||
}
|
||||
|
||||
func GetColumnByIDAndProjectID(ctx context.Context, columnID, projectID int64) (*Column, error) {
|
||||
column := new(Column)
|
||||
has, err := db.GetEngine(ctx).ID(columnID).And("project_id=?", projectID).Get(column)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectColumnNotExist{ColumnID: columnID}
|
||||
}
|
||||
|
||||
return column, nil
|
||||
}
|
||||
|
||||
// UpdateColumn updates a project column
|
||||
func UpdateColumn(ctx context.Context, column *Column) error {
|
||||
var fieldToUpdate []string
|
||||
|
||||
@@ -302,6 +302,18 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func GetProjectByIDAndOwner(ctx context.Context, id, ownerID int64) (*Project, error) {
|
||||
p := new(Project)
|
||||
has, err := db.GetEngine(ctx).ID(id).And("owner_id = ?", ownerID).Get(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectNotExist{ID: id}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetProjectForRepoByID returns the projects in a repository
|
||||
func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) {
|
||||
p := new(Project)
|
||||
|
||||
@@ -73,18 +73,18 @@ func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string)
|
||||
|
||||
// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not
|
||||
// The given map of files with their viewed state will be merged with the previous review, if present
|
||||
func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) error {
|
||||
func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) (*ReviewState, error) {
|
||||
log.Trace("Updating review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, updatedFiles)
|
||||
|
||||
review, exists, err := GetReviewState(ctx, userID, pullID, commitSHA)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
review.UpdatedFiles = mergeFiles(review.UpdatedFiles, updatedFiles)
|
||||
} else if previousReview, err := getNewestReviewStateApartFrom(ctx, userID, pullID, commitSHA); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
|
||||
// Overwrite the viewed files of the previous review if present
|
||||
} else if previousReview != nil {
|
||||
@@ -98,11 +98,11 @@ func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA stri
|
||||
if !exists {
|
||||
log.Trace("Inserting new review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, review.UpdatedFiles)
|
||||
_, err := engine.Insert(review)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
log.Trace("Updating already existing review with ID %d (user %d, repo %d, commit %s) with the updated files %v.", review.ID, userID, pullID, commitSHA, review.UpdatedFiles)
|
||||
_, err = engine.ID(review.ID).Update(&ReviewState{UpdatedFiles: review.UpdatedFiles})
|
||||
return err
|
||||
_, err = engine.ID(review.ID).Cols("updated_files").Update(review)
|
||||
return review, err
|
||||
}
|
||||
|
||||
// mergeFiles merges the given maps of files with their viewing state into one map.
|
||||
|
||||
@@ -166,6 +166,11 @@ func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, file
|
||||
return attach, nil
|
||||
}
|
||||
|
||||
func GetUnlinkedAttachmentsByUserID(ctx context.Context, userID int64) ([]*Attachment, error) {
|
||||
attachments := make([]*Attachment, 0, 10)
|
||||
return attachments, db.GetEngine(ctx).Where("uploader_id = ? AND issue_id = 0 AND release_id = 0 AND comment_id = 0", userID).Find(&attachments)
|
||||
}
|
||||
|
||||
// DeleteAttachment deletes the given attachment and optionally the associated file.
|
||||
func DeleteAttachment(ctx context.Context, a *Attachment, remove bool) error {
|
||||
_, err := DeleteAttachments(ctx, []*Attachment{a}, remove)
|
||||
|
||||
@@ -101,3 +101,19 @@ func TestGetAttachmentsByUUIDs(t *testing.T) {
|
||||
assert.Equal(t, int64(1), attachList[0].IssueID)
|
||||
assert.Equal(t, int64(5), attachList[1].IssueID)
|
||||
}
|
||||
|
||||
func TestGetUnlinkedAttachmentsByUserID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
attachments, err := repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 8)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, attachments, 1)
|
||||
assert.Equal(t, int64(10), attachments[0].ID)
|
||||
assert.Zero(t, attachments[0].IssueID)
|
||||
assert.Zero(t, attachments[0].ReleaseID)
|
||||
assert.Zero(t, attachments[0].CommentID)
|
||||
|
||||
attachments, err = repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, attachments)
|
||||
}
|
||||
|
||||
+34
-8
@@ -93,15 +93,25 @@ func init() {
|
||||
db.RegisterModel(new(Release))
|
||||
}
|
||||
|
||||
// LoadAttributes load repo and publisher attributes for a release
|
||||
func (r *Release) LoadAttributes(ctx context.Context) error {
|
||||
var err error
|
||||
if r.Repo == nil {
|
||||
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// LegacyAttachmentMissingRepoIDCutoff marks the date when repo_id started to be written during uploads
|
||||
// (2026-01-16T00:00:00Z). Older rows might have repo_id=0 and should be tolerated once.
|
||||
const LegacyAttachmentMissingRepoIDCutoff timeutil.TimeStamp = 1768521600
|
||||
|
||||
func (r *Release) LoadRepo(ctx context.Context) (err error) {
|
||||
if r.Repo != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadAttributes load repo and publisher attributes for a release
|
||||
func (r *Release) LoadAttributes(ctx context.Context) (err error) {
|
||||
if err := r.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Publisher == nil {
|
||||
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
|
||||
if err != nil {
|
||||
@@ -168,6 +178,11 @@ func UpdateReleaseNumCommits(ctx context.Context, rel *Release) error {
|
||||
|
||||
// AddReleaseAttachments adds a release attachments
|
||||
func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) {
|
||||
rel, err := GetReleaseByID(ctx, releaseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check attachments
|
||||
attachments, err := GetAttachmentsByUUIDs(ctx, attachmentUUIDs)
|
||||
if err != nil {
|
||||
@@ -175,6 +190,17 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
|
||||
}
|
||||
|
||||
for i := range attachments {
|
||||
if attachments[i].RepoID == 0 && attachments[i].CreatedUnix < LegacyAttachmentMissingRepoIDCutoff {
|
||||
attachments[i].RepoID = rel.RepoID
|
||||
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("repo_id").Update(attachments[i]); err != nil {
|
||||
return fmt.Errorf("update attachment repo_id [%d]: %w", attachments[i].ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if attachments[i].RepoID != rel.RepoID {
|
||||
return util.NewPermissionDeniedErrorf("attachment belongs to different repository")
|
||||
}
|
||||
|
||||
if attachments[i].ReleaseID != 0 {
|
||||
return util.NewPermissionDeniedErrorf("release permission denied")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ package repo
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -37,3 +39,54 @@ func Test_FindTagsByCommitIDs(t *testing.T) {
|
||||
assert.Equal(t, "delete-tag", rels[1].TagName)
|
||||
assert.Equal(t, "v1.0", rels[2].TagName)
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsRejectsDifferentRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
uuid := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12" // attachment 2 belongs to repo 2
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{uuid})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), uuid)
|
||||
assert.NoError(t, err)
|
||||
assert.Zero(t, attach.ReleaseID, "attachment should not be linked to release on failure")
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsAllowsLegacyMissingRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
legacyUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20" // attachment 10 has repo_id 0
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{legacyUUID})
|
||||
assert.NoError(t, err)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), legacyUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, attach.RepoID)
|
||||
assert.EqualValues(t, 1, attach.ReleaseID)
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsRejectsRecentZeroRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
recentUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd3800aa"
|
||||
attachment := &Attachment{
|
||||
UUID: recentUUID,
|
||||
RepoID: 0,
|
||||
IssueID: 0,
|
||||
ReleaseID: 0,
|
||||
CommentID: 0,
|
||||
Name: "recent-zero",
|
||||
CreatedUnix: LegacyAttachmentMissingRepoIDCutoff + 1,
|
||||
}
|
||||
assert.NoError(t, db.Insert(t.Context(), attachment))
|
||||
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{recentUUID})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), recentUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.Zero(t, attach.ReleaseID)
|
||||
assert.Zero(t, attach.RepoID)
|
||||
}
|
||||
|
||||
@@ -176,3 +176,13 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
|
||||
}
|
||||
return watchRepoMode(ctx, watch, WatchModeAuto)
|
||||
}
|
||||
|
||||
// ClearRepoWatches clears all watches for a repository and from the user that watched it.
|
||||
// Used when a repository is set to private.
|
||||
func ClearRepoWatches(ctx context.Context, repoID int64) error {
|
||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_watches = 0 WHERE id = ?", repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.DeleteBeans(ctx, Watch{RepoID: repoID})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsWatching(t *testing.T) {
|
||||
@@ -119,3 +120,21 @@ func TestWatchIfAuto(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, watchers, prevCount)
|
||||
}
|
||||
|
||||
func TestClearRepoWatches(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const repoID int64 = 1
|
||||
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, watchers)
|
||||
|
||||
assert.NoError(t, repo_model.ClearRepoWatches(t.Context(), repoID))
|
||||
|
||||
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, watchers)
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
assert.Zero(t, repo.NumWatches)
|
||||
}
|
||||
|
||||
@@ -102,7 +102,13 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
|
||||
}
|
||||
|
||||
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
|
||||
func ToggleUserOpenIDVisibility(ctx context.Context, id int64) (err error) {
|
||||
_, err = db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ?", id)
|
||||
return err
|
||||
func ToggleUserOpenIDVisibility(ctx context.Context, id int64, user *User) error {
|
||||
affected, err := db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ? AND uid = ?", id, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := affected.RowsAffected(); n != 1 {
|
||||
return util.NewNotExistErrorf("OpenID is unknown")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -33,12 +34,14 @@ func TestGetUserOpenIDs(t *testing.T) {
|
||||
|
||||
func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, oids, 1)
|
||||
assert.True(t, oids[0].Show)
|
||||
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
@@ -46,7 +49,7 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
require.Len(t, oids, 1)
|
||||
|
||||
assert.False(t, oids[0].Show)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
@@ -55,3 +58,13 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
assert.True(t, oids[0].Show)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToggleUserOpenIDVisibilityRequiresOwnership(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
unauthorizedUser, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), int64(1), unauthorizedUser)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
||||
|
||||
@@ -1444,3 +1444,15 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
|
||||
}
|
||||
return &setting.Admin.UserDisabledFeatures
|
||||
}
|
||||
|
||||
// GetUserOrOrgByName returns the user or org by name
|
||||
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
|
||||
var u User
|
||||
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrUserNotExist{Name: name}
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/nektos/act/pkg/jobparser"
|
||||
@@ -77,7 +77,7 @@ func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := io.ReadAll(f)
|
||||
content, err := util.ReadWithLimit(f, 1024*1024)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+39
-31
@@ -26,9 +26,11 @@ type ConvertOpts struct {
|
||||
KeepBOM bool
|
||||
}
|
||||
|
||||
var ToUTF8WithFallbackReaderPrefetchSize = 16 * 1024
|
||||
|
||||
// ToUTF8WithFallbackReader detects the encoding of content and converts to UTF-8 reader if possible
|
||||
func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
|
||||
buf := make([]byte, 2048)
|
||||
buf := make([]byte, ToUTF8WithFallbackReaderPrefetchSize)
|
||||
n, err := util.ReadAtMost(rd, buf)
|
||||
if err != nil {
|
||||
return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd)
|
||||
@@ -41,6 +43,7 @@ func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
|
||||
|
||||
encoding, _ := charset.Lookup(charsetLabel)
|
||||
if encoding == nil {
|
||||
log.Error("Unknown encoding: %s", charsetLabel)
|
||||
return io.MultiReader(bytes.NewReader(buf[:n]), rd)
|
||||
}
|
||||
|
||||
@@ -54,17 +57,18 @@ func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
|
||||
}
|
||||
|
||||
// ToUTF8 converts content to UTF8 encoding
|
||||
func ToUTF8(content []byte, opts ConvertOpts) (string, error) {
|
||||
func ToUTF8(content []byte, opts ConvertOpts) ([]byte, error) {
|
||||
charsetLabel, err := DetectEncoding(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return content, err
|
||||
} else if charsetLabel == "UTF-8" {
|
||||
return string(MaybeRemoveBOM(content, opts)), nil
|
||||
return MaybeRemoveBOM(content, opts), nil
|
||||
}
|
||||
|
||||
encoding, _ := charset.Lookup(charsetLabel)
|
||||
if encoding == nil {
|
||||
return string(content), fmt.Errorf("Unknown encoding: %s", charsetLabel)
|
||||
log.Error("Unknown encoding: %s", charsetLabel)
|
||||
return content, fmt.Errorf("unknown encoding: %s", charsetLabel)
|
||||
}
|
||||
|
||||
// If there is an error, we concatenate the nicely decoded part and the
|
||||
@@ -76,7 +80,7 @@ func ToUTF8(content []byte, opts ConvertOpts) (string, error) {
|
||||
|
||||
result = MaybeRemoveBOM(result, opts)
|
||||
|
||||
return string(result), err
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ToUTF8WithFallback detects the encoding of content and converts to UTF-8 if possible
|
||||
@@ -94,6 +98,7 @@ func ToUTF8DropErrors(content []byte, opts ConvertOpts) []byte {
|
||||
|
||||
encoding, _ := charset.Lookup(charsetLabel)
|
||||
if encoding == nil {
|
||||
log.Error("Unknown encoding: %s", charsetLabel)
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -130,28 +135,37 @@ func MaybeRemoveBOM(content []byte, opts ConvertOpts) []byte {
|
||||
}
|
||||
|
||||
// DetectEncoding detect the encoding of content
|
||||
func DetectEncoding(content []byte) (string, error) {
|
||||
// it always returns a detected or guessed "encoding" string, no matter error happens or not
|
||||
func DetectEncoding(content []byte) (encoding string, _ error) {
|
||||
// First we check if the content represents valid utf8 content excepting a truncated character at the end.
|
||||
|
||||
// Now we could decode all the runes in turn but this is not necessarily the cheapest thing to do
|
||||
// instead we walk backwards from the end to trim off a the incomplete character
|
||||
// instead we walk backwards from the end to trim off the incomplete character
|
||||
toValidate := content
|
||||
end := len(toValidate) - 1
|
||||
|
||||
if end < 0 {
|
||||
// no-op
|
||||
} else if toValidate[end]>>5 == 0b110 {
|
||||
// Incomplete 1 byte extension e.g. © <c2><a9> which has been truncated to <c2>
|
||||
toValidate = toValidate[:end]
|
||||
} else if end > 0 && toValidate[end]>>6 == 0b10 && toValidate[end-1]>>4 == 0b1110 {
|
||||
// Incomplete 2 byte extension e.g. ⛔ <e2><9b><94> which has been truncated to <e2><9b>
|
||||
toValidate = toValidate[:end-1]
|
||||
} else if end > 1 && toValidate[end]>>6 == 0b10 && toValidate[end-1]>>6 == 0b10 && toValidate[end-2]>>3 == 0b11110 {
|
||||
// Incomplete 3 byte extension e.g. 💩 <f0><9f><92><a9> which has been truncated to <f0><9f><92>
|
||||
toValidate = toValidate[:end-2]
|
||||
// U+0000 U+007F 0yyyzzzz
|
||||
// U+0080 U+07FF 110xxxyy 10yyzzzz
|
||||
// U+0800 U+FFFF 1110wwww 10xxxxyy 10yyzzzz
|
||||
// U+010000 U+10FFFF 11110uvv 10vvwwww 10xxxxyy 10yyzzzz
|
||||
cnt := 0
|
||||
for end >= 0 && cnt < 4 {
|
||||
c := toValidate[end]
|
||||
if c>>5 == 0b110 || c>>4 == 0b1110 || c>>3 == 0b11110 {
|
||||
// a leading byte
|
||||
toValidate = toValidate[:end]
|
||||
break
|
||||
} else if c>>6 == 0b10 {
|
||||
// a continuation byte
|
||||
end--
|
||||
} else {
|
||||
// not an utf-8 byte
|
||||
break
|
||||
}
|
||||
cnt++
|
||||
}
|
||||
|
||||
if utf8.Valid(toValidate) {
|
||||
log.Debug("Detected encoding: utf-8 (fast)")
|
||||
return "UTF-8", nil
|
||||
}
|
||||
|
||||
@@ -160,7 +174,7 @@ func DetectEncoding(content []byte) (string, error) {
|
||||
if len(content) < 1024 {
|
||||
// Check if original content is valid
|
||||
if _, err := textDetector.DetectBest(content); err != nil {
|
||||
return "", err
|
||||
return util.IfZero(setting.Repository.AnsiCharset, "UTF-8"), err
|
||||
}
|
||||
times := 1024 / len(content)
|
||||
detectContent = make([]byte, 0, times*len(content))
|
||||
@@ -171,14 +185,10 @@ func DetectEncoding(content []byte) (string, error) {
|
||||
detectContent = content
|
||||
}
|
||||
|
||||
// Now we can't use DetectBest or just results[0] because the result isn't stable - so we need a tie break
|
||||
// Now we can't use DetectBest or just results[0] because the result isn't stable - so we need a tie-break
|
||||
results, err := textDetector.DetectAll(detectContent)
|
||||
if err != nil {
|
||||
if err == chardet.NotDetectedError && len(setting.Repository.AnsiCharset) > 0 {
|
||||
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
|
||||
return setting.Repository.AnsiCharset, nil
|
||||
}
|
||||
return "", err
|
||||
return util.IfZero(setting.Repository.AnsiCharset, "UTF-8"), err
|
||||
}
|
||||
|
||||
topConfidence := results[0].Confidence
|
||||
@@ -201,11 +211,9 @@ func DetectEncoding(content []byte) (string, error) {
|
||||
}
|
||||
|
||||
// FIXME: to properly decouple this function the fallback ANSI charset should be passed as an argument
|
||||
if topResult.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 {
|
||||
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
|
||||
if topResult.Charset != "UTF-8" && setting.Repository.AnsiCharset != "" {
|
||||
return setting.Repository.AnsiCharset, err
|
||||
}
|
||||
|
||||
log.Debug("Detected encoding: %s", topResult.Charset)
|
||||
return topResult.Charset, err
|
||||
return topResult.Charset, nil
|
||||
}
|
||||
|
||||
+35
-146
@@ -4,12 +4,12 @@
|
||||
package charset
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -47,12 +47,12 @@ func TestToUTF8(t *testing.T) {
|
||||
|
||||
res, err := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ABC", res)
|
||||
assert.Equal(t, "ABC", string(res))
|
||||
|
||||
// "áéíóú"
|
||||
res, err = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||
|
||||
// "áéíóú"
|
||||
res, err = ToUTF8([]byte{
|
||||
@@ -60,7 +60,7 @@ func TestToUTF8(t *testing.T) {
|
||||
0xc3, 0xba,
|
||||
}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
|
||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||
|
||||
res, err = ToUTF8([]byte{
|
||||
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
|
||||
@@ -96,12 +96,11 @@ func TestToUTF8(t *testing.T) {
|
||||
assert.Equal(t, []byte{
|
||||
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
|
||||
0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82,
|
||||
},
|
||||
[]byte(res))
|
||||
}, res)
|
||||
|
||||
res, err = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, []byte(res))
|
||||
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res)
|
||||
}
|
||||
|
||||
func TestToUTF8WithFallback(t *testing.T) {
|
||||
@@ -231,152 +230,42 @@ func TestDetectEncoding(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func stringMustStartWith(t *testing.T, expected, value string) {
|
||||
assert.Equal(t, expected, value[:len(expected)])
|
||||
func stringMustStartWith(t *testing.T, expected string, value []byte) {
|
||||
assert.Equal(t, expected, string(value[:len(expected)]))
|
||||
}
|
||||
|
||||
func stringMustEndWith(t *testing.T, expected, value string) {
|
||||
assert.Equal(t, expected, value[len(value)-len(expected):])
|
||||
func stringMustEndWith(t *testing.T, expected string, value []byte) {
|
||||
assert.Equal(t, expected, string(value[len(value)-len(expected):]))
|
||||
}
|
||||
|
||||
func TestToUTF8WithFallbackReader(t *testing.T) {
|
||||
resetDefaultCharsetsOrder()
|
||||
test.MockVariableValue(&ToUTF8WithFallbackReaderPrefetchSize)
|
||||
|
||||
for testLen := range 2048 {
|
||||
pattern := " test { () }\n"
|
||||
input := ""
|
||||
for len(input) < testLen {
|
||||
input += pattern
|
||||
}
|
||||
input = input[:testLen]
|
||||
input += "// Выключаем"
|
||||
rd := ToUTF8WithFallbackReader(bytes.NewReader([]byte(input)), ConvertOpts{})
|
||||
block := "aá啊🤔"
|
||||
runes := []rune(block)
|
||||
assert.Len(t, string(runes[0]), 1)
|
||||
assert.Len(t, string(runes[1]), 2)
|
||||
assert.Len(t, string(runes[2]), 3)
|
||||
assert.Len(t, string(runes[3]), 4)
|
||||
|
||||
content := strings.Repeat(block, 2)
|
||||
for i := 1; i < len(content); i++ {
|
||||
encoding, err := DetectEncoding([]byte(content[:i]))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "UTF-8", encoding)
|
||||
|
||||
ToUTF8WithFallbackReaderPrefetchSize = i
|
||||
rd := ToUTF8WithFallbackReader(strings.NewReader(content), ConvertOpts{})
|
||||
r, _ := io.ReadAll(rd)
|
||||
assert.Equalf(t, input, string(r), "testing string len=%d", testLen)
|
||||
assert.Equal(t, content, string(r))
|
||||
}
|
||||
for _, r := range runes {
|
||||
content = "abc abc " + string(r) + string(r) + string(r)
|
||||
for i := 0; i < len(content); i++ {
|
||||
encoding, err := DetectEncoding([]byte(content[:i]))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "UTF-8", encoding)
|
||||
}
|
||||
}
|
||||
|
||||
truncatedOneByteExtension := failFastBytes
|
||||
encoding, _ := DetectEncoding(truncatedOneByteExtension)
|
||||
assert.Equal(t, "UTF-8", encoding)
|
||||
|
||||
truncatedTwoByteExtension := failFastBytes
|
||||
truncatedTwoByteExtension[len(failFastBytes)-1] = 0x9b
|
||||
truncatedTwoByteExtension[len(failFastBytes)-2] = 0xe2
|
||||
|
||||
encoding, _ = DetectEncoding(truncatedTwoByteExtension)
|
||||
assert.Equal(t, "UTF-8", encoding)
|
||||
|
||||
truncatedThreeByteExtension := failFastBytes
|
||||
truncatedThreeByteExtension[len(failFastBytes)-1] = 0x92
|
||||
truncatedThreeByteExtension[len(failFastBytes)-2] = 0x9f
|
||||
truncatedThreeByteExtension[len(failFastBytes)-3] = 0xf0
|
||||
|
||||
encoding, _ = DetectEncoding(truncatedThreeByteExtension)
|
||||
assert.Equal(t, "UTF-8", encoding)
|
||||
}
|
||||
|
||||
var failFastBytes = []byte{
|
||||
0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x6f, 0x72, 0x67, 0x2e, 0x61, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x74, 0x6f,
|
||||
0x6f, 0x6c, 0x73, 0x2e, 0x61, 0x6e, 0x74, 0x2e, 0x74, 0x61, 0x73, 0x6b, 0x64, 0x65, 0x66, 0x73, 0x2e, 0x63, 0x6f, 0x6e,
|
||||
0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4f, 0x73, 0x0a, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x6f, 0x72, 0x67,
|
||||
0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f,
|
||||
0x74, 0x2e, 0x67, 0x72, 0x61, 0x64, 0x6c, 0x65, 0x2e, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x2e, 0x72, 0x75, 0x6e, 0x2e, 0x42,
|
||||
0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x0a, 0x0a, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20,
|
||||
0x20, 0x20, 0x69, 0x64, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d,
|
||||
0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x22, 0x29, 0x0a, 0x7d, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x65,
|
||||
0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65,
|
||||
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a,
|
||||
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x61, 0x70, 0x69, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d,
|
||||
0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74,
|
||||
0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x61, 0x70, 0x69, 0x2d, 0x64, 0x6f, 0x63, 0x73, 0x22, 0x29,
|
||||
0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x64, 0x62,
|
||||
0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a,
|
||||
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65,
|
||||
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a,
|
||||
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x66,
|
||||
0x73, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
|
||||
0x3a, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x6d, 0x71, 0x22, 0x29, 0x29, 0x0a, 0x0a,
|
||||
0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22,
|
||||
0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
|
||||
0x2d, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2d, 0x73, 0x74, 0x61, 0x72, 0x74,
|
||||
0x65, 0x72, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d, 0x68, 0x61, 0x6c, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c,
|
||||
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e,
|
||||
0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x22, 0x29, 0x0a,
|
||||
0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28,
|
||||
0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b,
|
||||
0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x74,
|
||||
0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x77, 0x65, 0x62, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c,
|
||||
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69,
|
||||
0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72,
|
||||
0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x61, 0x6f, 0x70,
|
||||
0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f,
|
||||
0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d,
|
||||
0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x61, 0x63, 0x74, 0x75, 0x61, 0x74, 0x6f, 0x72, 0x22, 0x29, 0x0a, 0x20,
|
||||
0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f,
|
||||
0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63,
|
||||
0x6c, 0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74,
|
||||
0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x22, 0x29, 0x0a, 0x20, 0x20,
|
||||
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72,
|
||||
0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6c,
|
||||
0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74, 0x61,
|
||||
0x72, 0x74, 0x65, 0x72, 0x2d, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2d, 0x61, 0x6c, 0x6c, 0x22, 0x29, 0x0a, 0x20, 0x20,
|
||||
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72,
|
||||
0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6c,
|
||||
0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74, 0x61,
|
||||
0x72, 0x74, 0x65, 0x72, 0x2d, 0x73, 0x6c, 0x65, 0x75, 0x74, 0x68, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d,
|
||||
0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70,
|
||||
0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x72, 0x65, 0x74, 0x72, 0x79, 0x3a,
|
||||
0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x72, 0x65, 0x74, 0x72, 0x79, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
|
||||
0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x63, 0x68, 0x2e, 0x71,
|
||||
0x6f, 0x73, 0x2e, 0x6c, 0x6f, 0x67, 0x62, 0x61, 0x63, 0x6b, 0x3a, 0x6c, 0x6f, 0x67, 0x62, 0x61, 0x63, 0x6b, 0x2d, 0x63,
|
||||
0x6c, 0x61, 0x73, 0x73, 0x69, 0x63, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d,
|
||||
0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x69, 0x6f, 0x2e, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x6d, 0x65,
|
||||
0x74, 0x65, 0x72, 0x3a, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x2d, 0x72, 0x65, 0x67, 0x69, 0x73,
|
||||
0x74, 0x72, 0x79, 0x2d, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20,
|
||||
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x6b, 0x6f, 0x74,
|
||||
0x6c, 0x69, 0x6e, 0x28, 0x22, 0x73, 0x74, 0x64, 0x6c, 0x69, 0x62, 0x22, 0x29, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
|
||||
0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
|
||||
0x2f, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64,
|
||||
0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
|
||||
0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74,
|
||||
0x65, 0x73, 0x74, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a,
|
||||
0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d,
|
||||
0x74, 0x65, 0x73, 0x74, 0x22, 0x29, 0x0a, 0x7d, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x20, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4a,
|
||||
0x61, 0x72, 0x20, 0x62, 0x79, 0x20, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
|
||||
0x69, 0x6e, 0x67, 0x28, 0x4a, 0x61, 0x72, 0x3a, 0x3a, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20,
|
||||
0x20, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2e,
|
||||
0x73, 0x65, 0x74, 0x28, 0x22, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
|
||||
0x76, 0x61, 0x6c, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x70, 0x61, 0x74, 0x68,
|
||||
0x20, 0x62, 0x79, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x67,
|
||||
0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
|
||||
0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65,
|
||||
0x73, 0x28, 0x22, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x2d, 0x50, 0x61, 0x74, 0x68, 0x22, 0x20, 0x74, 0x6f, 0x20, 0x6f, 0x62,
|
||||
0x6a, 0x65, 0x63, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70,
|
||||
0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x20, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x20, 0x3d,
|
||||
0x20, 0x22, 0x66, 0x69, 0x6c, 0x65, 0x3a, 0x2f, 0x2b, 0x22, 0x2e, 0x74, 0x6f, 0x52, 0x65, 0x67, 0x65, 0x78, 0x28, 0x29,
|
||||
0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64,
|
||||
0x65, 0x20, 0x66, 0x75, 0x6e, 0x20, 0x74, 0x6f, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x28, 0x29, 0x3a, 0x20, 0x53, 0x74,
|
||||
0x72, 0x69, 0x6e, 0x67, 0x20, 0x3d, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x70,
|
||||
0x61, 0x74, 0x68, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x6f, 0x53, 0x74, 0x72, 0x69,
|
||||
0x6e, 0x67, 0x28, 0x22, 0x20, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
|
||||
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x74, 0x2e, 0x74, 0x6f, 0x55, 0x52, 0x49, 0x28, 0x29, 0x2e, 0x74, 0x6f, 0x55,
|
||||
0x52, 0x4c, 0x28, 0x29, 0x2e, 0x74, 0x6f, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x28, 0x29, 0x2e, 0x72, 0x65, 0x70, 0x6c,
|
||||
0x61, 0x63, 0x65, 0x46, 0x69, 0x72, 0x73, 0x74, 0x28, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x2c, 0x20, 0x22, 0x2f,
|
||||
0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20,
|
||||
0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x7d, 0x0a, 0x0a, 0x74, 0x61, 0x73,
|
||||
0x6b, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x3c, 0x42, 0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x3e, 0x28, 0x22, 0x62,
|
||||
0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x4f,
|
||||
0x73, 0x2e, 0x69, 0x73, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x28, 0x4f, 0x73, 0x2e, 0x46, 0x41, 0x4d, 0x49, 0x4c, 0x59,
|
||||
0x5f, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x53, 0x29, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
|
||||
0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x70, 0x61, 0x74, 0x68, 0x20, 0x3d, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x28, 0x73,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x28, 0x22, 0x6d, 0x61, 0x69,
|
||||
0x6e, 0x22, 0x29, 0x2e, 0x6d, 0x61, 0x70, 0x20, 0x7b, 0x20, 0x69, 0x74, 0x2e, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x20,
|
||||
0x7d, 0x2c, 0x20, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4a, 0x61, 0x72, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a,
|
||||
0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0xd0,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -91,7 +92,13 @@ loop:
|
||||
}
|
||||
|
||||
for _, userStopwatches := range usersStopwatches {
|
||||
apiSWs, err := convert.ToStopWatches(ctx, userStopwatches.StopWatches)
|
||||
u, err := user_model.GetUserByID(ctx, userStopwatches.UserID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get user %d: %v", userStopwatches.UserID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
apiSWs, err := convert.ToStopWatches(ctx, u, userStopwatches.StopWatches)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrIssueNotExist(err) {
|
||||
log.Error("Unable to APIFormat stopwatches: %v", err)
|
||||
|
||||
@@ -76,7 +76,7 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
|
||||
if p.IconSVGs[svgID] == "" {
|
||||
p.IconSVGs[svgID] = svgHTML
|
||||
}
|
||||
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
||||
return template.HTML(`<svg ` + svgCommonAttrs + `><use href="#` + svgID + `"></use></svg>`)
|
||||
}
|
||||
|
||||
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
|
||||
|
||||
@@ -25,7 +25,7 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
|
||||
return ""
|
||||
}
|
||||
sb := &strings.Builder{}
|
||||
sb.WriteString(`<div class=tw-hidden>`)
|
||||
sb.WriteString(`<div class="svg-icon-container">`)
|
||||
for _, icon := range p.IconSVGs {
|
||||
sb.WriteString(string(icon))
|
||||
}
|
||||
|
||||
@@ -9,24 +9,38 @@ package git
|
||||
import (
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID ObjectID
|
||||
ID ObjectID
|
||||
repo *Repository
|
||||
name string
|
||||
}
|
||||
|
||||
gogitEncodedObj plumbing.EncodedObject
|
||||
name string
|
||||
func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
|
||||
return b.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(b.ID.RawValue()))
|
||||
}
|
||||
|
||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||
// Calling the Close function on the result will discard all unread output.
|
||||
func (b *Blob) DataAsync() (io.ReadCloser, error) {
|
||||
return b.gogitEncodedObj.Reader()
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.Reader()
|
||||
}
|
||||
|
||||
// Size returns the uncompressed size of the blob
|
||||
func (b *Blob) Size() int64 {
|
||||
return b.gogitEncodedObj.Size()
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err)
|
||||
return 0
|
||||
}
|
||||
return obj.Size()
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@ type Parser struct {
|
||||
func NewParser(r io.Reader, format Format) *Parser {
|
||||
scanner := bufio.NewScanner(r)
|
||||
|
||||
// default MaxScanTokenSize = 64 kiB may be too small for some references,
|
||||
// so allow the buffer to grow up to 4x if needed
|
||||
scanner.Buffer(nil, 4*bufio.MaxScanTokenSize)
|
||||
// default Scanner.MaxScanTokenSize = 64 kiB may be too small for some references,
|
||||
// so allow the buffer to be large enough in case the ref has long content (e.g.: a tag with long message)
|
||||
// as long as it doesn't exceed some reasonable limit (4 MiB here, or MAX_DISPLAY_FILE_SIZE=8MiB), it is OK
|
||||
// there are still some choices: 1. add a config option for the limit; 2. don't use scanner and write our own parser to fully handle large contents
|
||||
scanner.Buffer(nil, 4*1024*1024)
|
||||
|
||||
// in addition to the reference delimiter we specified in the --format,
|
||||
// `git for-each-ref` will always add a newline after every reference.
|
||||
|
||||
@@ -9,5 +9,11 @@ func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.getBlob(id)
|
||||
if id.IsZero() {
|
||||
return nil, ErrNotExist{id.String(), ""}
|
||||
}
|
||||
return &Blob{
|
||||
ID: id,
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
|
||||
encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(id.RawValue()))
|
||||
if err != nil {
|
||||
return nil, ErrNotExist{id.String(), ""}
|
||||
}
|
||||
|
||||
return &Blob{
|
||||
ID: id,
|
||||
gogitEncodedObj: encodedObj,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
|
||||
if id.IsZero() {
|
||||
return nil, ErrNotExist{id.String(), ""}
|
||||
}
|
||||
return &Blob{
|
||||
ID: id,
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
@@ -19,12 +18,17 @@ type TreeEntry struct {
|
||||
gogitTreeEntry *object.TreeEntry
|
||||
ptree *Tree
|
||||
|
||||
fullName string
|
||||
|
||||
size int64
|
||||
sized bool
|
||||
}
|
||||
|
||||
// Name returns the name of the entry
|
||||
func (te *TreeEntry) Name() string {
|
||||
if te.fullName != "" {
|
||||
return te.fullName
|
||||
}
|
||||
return te.gogitTreeEntry.Name
|
||||
}
|
||||
|
||||
@@ -78,14 +82,9 @@ func (te *TreeEntry) IsExecutable() bool {
|
||||
|
||||
// Blob returns the blob object the entry
|
||||
func (te *TreeEntry) Blob() *Blob {
|
||||
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Blob{
|
||||
ID: ParseGogitHash(te.gogitTreeEntry.Hash),
|
||||
gogitEncodedObj: encodedObj,
|
||||
name: te.Name(),
|
||||
ID: ParseGogitHash(te.gogitTreeEntry.Hash),
|
||||
repo: te.ptree.repo,
|
||||
name: te.Name(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
||||
seen := map[plumbing.Hash]bool{}
|
||||
walker := object.NewTreeWalker(t.gogitTree, true, seen)
|
||||
for {
|
||||
_, entry, err := walker.Next()
|
||||
fullName, entry, err := walker.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
@@ -84,6 +84,7 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
|
||||
ID: ParseGogitHash(entry.Hash),
|
||||
gogitTreeEntry: &entry,
|
||||
ptree: t,
|
||||
fullName: fullName,
|
||||
}
|
||||
entries = append(entries, convertedEntry)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
@@ -76,7 +75,7 @@ func unmarshalFromEntry(entry *git.TreeEntry, filename string) (*api.IssueTempla
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
content, err := io.ReadAll(r)
|
||||
content, err := util.ReadWithLimit(r, 1024*1024)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read all: %w", err)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ func TestMathRender(t *testing.T) {
|
||||
"$ a $",
|
||||
`<p><code class="language-math">a</code></p>` + nl,
|
||||
},
|
||||
{
|
||||
"$a$$b$",
|
||||
`<p><code class="language-math">a</code><code class="language-math">b</code></p>` + nl,
|
||||
},
|
||||
{
|
||||
"$a$ $b$",
|
||||
`<p><code class="language-math">a</code> <code class="language-math">b</code></p>` + nl,
|
||||
@@ -59,7 +63,7 @@ func TestMathRender(t *testing.T) {
|
||||
`<p>a$b $a a$b b$</p>` + nl,
|
||||
},
|
||||
{
|
||||
"a$x$",
|
||||
"a$x$", // Pattern: "word$other$" The real world example is: "Price is between US$1 and US$2.", so don't parse this.
|
||||
`<p>a$x$</p>` + nl,
|
||||
},
|
||||
{
|
||||
@@ -70,6 +74,10 @@ func TestMathRender(t *testing.T) {
|
||||
"$a$ ($b$) [$c$] {$d$}",
|
||||
`<p><code class="language-math">a</code> (<code class="language-math">b</code>) [$c$] {$d$}</p>` + nl,
|
||||
},
|
||||
{
|
||||
"[$a$](link)",
|
||||
`<p><a href="/link" rel="nofollow"><code class="language-math">a</code></a></p>` + nl,
|
||||
},
|
||||
{
|
||||
"$$a$$",
|
||||
`<p><code class="language-math">a</code></p>` + nl,
|
||||
|
||||
@@ -54,6 +54,10 @@ func isAlphanumeric(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||
}
|
||||
|
||||
func isInMarkdownLinkText(block text.Reader, lineAfter []byte) bool {
|
||||
return block.PrecendingCharacter() == '[' && bytes.HasPrefix(lineAfter, []byte("]("))
|
||||
}
|
||||
|
||||
// Parse parses the current line and returns a result of parsing.
|
||||
func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
||||
line, _ := block.PeekLine()
|
||||
@@ -115,7 +119,9 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
|
||||
}
|
||||
// check valid ending character
|
||||
isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) ||
|
||||
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0
|
||||
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 ||
|
||||
succeedingCharacter == '$' ||
|
||||
isInMarkdownLinkText(block, line[i+len(stopMark):])
|
||||
if checkSurrounding && !isValidEndingChar {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@ import (
|
||||
|
||||
// ReleaseAsset represents a release asset
|
||||
type ReleaseAsset struct {
|
||||
ID int64
|
||||
Name string
|
||||
ContentType *string `yaml:"content_type"`
|
||||
ID int64
|
||||
Name string
|
||||
|
||||
// There was a field "ContentType (content_type)" because Some forges can provide that for assets,
|
||||
// but we don't need it when migrating, so the field is omitted here.
|
||||
|
||||
Size *int
|
||||
DownloadCount *int `yaml:"download_count"`
|
||||
Created time.Time
|
||||
|
||||
@@ -62,7 +62,28 @@ type PackageMetadata struct {
|
||||
Author User `json:"author"`
|
||||
ReadmeFilename string `json:"readmeFilename,omitempty"`
|
||||
Users map[string]bool `json:"users,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
License License `json:"license,omitempty"`
|
||||
}
|
||||
|
||||
type License string
|
||||
|
||||
func (l *License) UnmarshalJSON(data []byte) error {
|
||||
switch data[0] {
|
||||
case '"':
|
||||
var value string
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*l = License(value)
|
||||
case '{':
|
||||
var values map[string]any
|
||||
if err := json.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
value, _ := values["type"].(string)
|
||||
*l = License(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PackageMetadataVersion documentation: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
|
||||
@@ -74,7 +95,7 @@ type PackageMetadataVersion struct {
|
||||
Description string `json:"description"`
|
||||
Author User `json:"author"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
License License `json:"license,omitempty"`
|
||||
Repository Repository `json:"repository"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParsePackage(t *testing.T) {
|
||||
@@ -291,11 +292,36 @@ func TestParsePackage(t *testing.T) {
|
||||
assert.Equal(t, packageDescription, p.Metadata.Readme)
|
||||
assert.Equal(t, packageAuthor, p.Metadata.Author)
|
||||
assert.Equal(t, packageBin, p.Metadata.Bin["bin"])
|
||||
assert.Equal(t, "MIT", p.Metadata.License)
|
||||
assert.Equal(t, "MIT", string(p.Metadata.License))
|
||||
assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL)
|
||||
assert.Contains(t, p.Metadata.Dependencies, "package")
|
||||
assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"])
|
||||
assert.Equal(t, repository.Type, p.Metadata.Repository.Type)
|
||||
assert.Equal(t, repository.URL, p.Metadata.Repository.URL)
|
||||
})
|
||||
|
||||
t.Run("ValidLicenseMap", func(t *testing.T) {
|
||||
packageJSON := `{
|
||||
"versions": {
|
||||
"0.1.1": {
|
||||
"name": "dev-null",
|
||||
"version": "0.1.1",
|
||||
"license": {
|
||||
"type": "MIT"
|
||||
},
|
||||
"dist": {
|
||||
"integrity": "sha256-"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_attachments": {
|
||||
"foo": {
|
||||
"data": "AAAA"
|
||||
}
|
||||
}
|
||||
}`
|
||||
p, err := ParsePackage(strings.NewReader(packageJSON))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "MIT", string(p.Metadata.License))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Metadata struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
License License `json:"license,omitempty"`
|
||||
ProjectURL string `json:"project_url,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
||||
|
||||
@@ -216,7 +216,7 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
|
||||
if p.Metadata.Readme != "" {
|
||||
f, err := archive.Open(p.Metadata.Readme)
|
||||
if err == nil {
|
||||
buf, _ := io.ReadAll(f)
|
||||
buf, _ := util.ReadWithLimit(f, 1024*1024)
|
||||
m.Readme = string(buf)
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func ParsePackage(r io.Reader) (*Package, error) {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.EqualFold(hd.Name, "readme.md") {
|
||||
data, err := io.ReadAll(tr)
|
||||
data, err := util.ReadWithLimit(tr, 1024*1024)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR
|
||||
return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err)
|
||||
}
|
||||
}
|
||||
added, deleted, updated = len(deletes), len(updates), len(inserts)
|
||||
added, deleted, updated = len(inserts), len(deletes), len(updates)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -327,14 +327,14 @@ func LogStartupProblem(skip int, level log.Level, format string, args ...any) {
|
||||
|
||||
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
|
||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
|
||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
|
||||
}
|
||||
}
|
||||
|
||||
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
|
||||
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
|
||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
|
||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ var OAuth2 = struct {
|
||||
InvalidateRefreshTokens bool
|
||||
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
||||
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
||||
JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"`
|
||||
MaxTokenLength int
|
||||
DefaultApplications []string
|
||||
}{
|
||||
|
||||
@@ -250,6 +250,7 @@ func (a *AzureBlobStorage) Delete(path string) error {
|
||||
func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) {
|
||||
blobClient := a.getBlobClient(path)
|
||||
|
||||
// TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage
|
||||
startTime := time.Now()
|
||||
u, err := blobClient.GetSASURL(sas.BlobPermissions{
|
||||
Read: true,
|
||||
|
||||
@@ -279,20 +279,44 @@ func (m *MinioStorage) Delete(path string) error {
|
||||
}
|
||||
|
||||
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
|
||||
func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
|
||||
func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
|
||||
// copy serveDirectReqParams
|
||||
reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we?
|
||||
reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
|
||||
|
||||
// Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head.
|
||||
// So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI.
|
||||
// Detect content type by extension name, only support the well-known safe types for inline rendering.
|
||||
// TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future
|
||||
ext := path.Ext(name)
|
||||
inlineExtMimeTypes := map[string]string{
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".avif": "image/avif",
|
||||
// ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy
|
||||
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline
|
||||
".pdf": "application/pdf",
|
||||
|
||||
// TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType"
|
||||
}
|
||||
if mimeType, ok := inlineExtMimeTypes[ext]; ok {
|
||||
reqParams.Set("response-content-type", mimeType)
|
||||
reqParams.Set("response-content-disposition", "inline")
|
||||
} else {
|
||||
reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name)))
|
||||
}
|
||||
|
||||
expires := 5 * time.Minute
|
||||
if method == http.MethodHead {
|
||||
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
|
||||
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
|
||||
return u, convertMinioErr(err)
|
||||
}
|
||||
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
|
||||
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
|
||||
return u, convertMinioErr(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,17 @@ func detectFileTypeBox(data []byte) (brands []string, found bool) {
|
||||
return brands, true
|
||||
}
|
||||
|
||||
func isEmbeddedOpenType(data []byte) bool {
|
||||
// https://www.w3.org/submissions/EOT
|
||||
if len(data) < 80 {
|
||||
return false
|
||||
}
|
||||
version := binary.LittleEndian.Uint32(data[8:]) // Actually this standard is abandoned (for IE6-IE11 only), there are only 3 versions defined
|
||||
magic := binary.LittleEndian.Uint16(data[34:36]) // MagicNumber: 0x504C ("LP")
|
||||
reserved := data[64:80] // Reserved 1-4 (each: unsigned long)
|
||||
return (version == 0x00010000 || version == 0x00020001 || version == 0x00020002) && magic == 0x504C && bytes.Count(reserved, []byte{0}) == len(reserved)
|
||||
}
|
||||
|
||||
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/plain if input is empty.
|
||||
func DetectContentType(data []byte) SniffedType {
|
||||
if len(data) == 0 {
|
||||
@@ -119,6 +130,18 @@ func DetectContentType(data []byte) SniffedType {
|
||||
data = data[:SniffContentSize]
|
||||
}
|
||||
|
||||
const typeMsFontObject = "application/vnd.ms-fontobject"
|
||||
if ct == typeMsFontObject {
|
||||
// Stupid Golang blindly detects any content with 34th-35th bytes being "LP" as font.
|
||||
// If it is not really for ".eot" content, we try to detect it again by hiding the "LP", see the test for more details.
|
||||
if isEmbeddedOpenType(data) {
|
||||
return SniffedType{typeMsFontObject}
|
||||
}
|
||||
data = slices.Clone(data)
|
||||
data[34] = 'l'
|
||||
ct = http.DetectContentType(data)
|
||||
}
|
||||
|
||||
vars := globalVars()
|
||||
// SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888
|
||||
detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")
|
||||
|
||||
@@ -6,6 +6,7 @@ package typesniffer
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -154,3 +155,25 @@ func TestDetectContentTypeAvif(t *testing.T) {
|
||||
st := DetectContentType(buf)
|
||||
assert.Equal(t, MimeTypeImageAvif, st.contentType)
|
||||
}
|
||||
|
||||
func TestDetectContentTypeIncorrectFont(t *testing.T) {
|
||||
s := "Stupid Golang keep detecting 34th LP as font"
|
||||
// They don't want to have any improvement to it: https://github.com/golang/go/issues/77172
|
||||
golangDetected := http.DetectContentType([]byte(s))
|
||||
assert.Equal(t, "application/vnd.ms-fontobject", golangDetected)
|
||||
// We have to make our patch to make it work correctly
|
||||
ourDetected := DetectContentType([]byte(s))
|
||||
assert.Equal(t, "text/plain; charset=utf-8", ourDetected.contentType)
|
||||
|
||||
// For binary content, ensure it still detects as font. The content is from "opensans-regular.eot"
|
||||
b := []byte{
|
||||
0x3d, 0x30, 0x00, 0x00, 0x6b, 0x2f, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00,
|
||||
0x02, 0x0b, 0x06, 0x06, 0x03, 0x05, 0x04, 0x02, 0x02, 0x04, 0x01, 0x00, 0x90, 0x01, 0x00, 0x00,
|
||||
0x04, 0x00, 0x4c, 0x50, 0xef, 0x02, 0x00, 0xe0, 0x5b, 0x20, 0x00, 0x40, 0x28, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x63, 0xf4, 0x17, 0x14,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x12, 0x00, 0x4f, 0x00, 0x70, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x20, 0x00, 0x53, 0x00,
|
||||
}
|
||||
assert.Equal(t, "application/vnd.ms-fontobject", http.DetectContentType(b))
|
||||
assert.Equal(t, "application/vnd.ms-fontobject", DetectContentType(b).contentType)
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) {
|
||||
// ReadWithLimit reads at most "limit" bytes from r into buf.
|
||||
// If EOF or ErrUnexpectedEOF occurs while reading, err will be nil.
|
||||
func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) {
|
||||
return readWithLimit(r, 1024, n)
|
||||
return readWithLimit(r, 4*1024, n)
|
||||
}
|
||||
|
||||
func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) {
|
||||
|
||||
@@ -46,11 +46,15 @@ func RouterMockPoint(pointName string) func(next http.Handler) http.Handler {
|
||||
//
|
||||
// Then the mock function will be executed as a middleware at the mock point.
|
||||
// It only takes effect in testing mode (setting.IsInTesting == true).
|
||||
func RouteMock(pointName string, h any) {
|
||||
func RouteMock(pointName string, h any) func() {
|
||||
if _, ok := routeMockPoints[pointName]; !ok {
|
||||
panic("route mock point not found: " + pointName)
|
||||
}
|
||||
old := routeMockPoints[pointName]
|
||||
routeMockPoints[pointName] = toHandlerProvider(h)
|
||||
return func() {
|
||||
routeMockPoints[pointName] = old
|
||||
}
|
||||
}
|
||||
|
||||
// RouteMockReset resets all mock points (no mock anymore)
|
||||
|
||||
@@ -55,7 +55,7 @@ func NewRouter() *Router {
|
||||
// Use supports two middlewares
|
||||
func (r *Router) Use(middlewares ...any) {
|
||||
for _, m := range middlewares {
|
||||
if m != nil {
|
||||
if !isNilOrFuncNil(m) {
|
||||
r.chiRouter.Use(toHandlerProvider(m))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ more = More
|
||||
buttons.heading.tooltip = Add heading
|
||||
buttons.bold.tooltip = Add bold text
|
||||
buttons.italic.tooltip = Add italic text
|
||||
buttons.strikethrough.tooltip = Add strikethrough text
|
||||
buttons.quote.tooltip = Quote text
|
||||
buttons.code.tooltip = Add code
|
||||
buttons.link.tooltip = Add a link
|
||||
@@ -1481,6 +1482,7 @@ projects.column.new_submit = "Create Column"
|
||||
projects.column.new = "New Column"
|
||||
projects.column.set_default = "Set Default"
|
||||
projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls"
|
||||
projects.column.default_column_hint = "New issues added to this project will be added to this column"
|
||||
projects.column.delete = "Delete Column"
|
||||
projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?"
|
||||
projects.column.color = "Color"
|
||||
@@ -1856,6 +1858,7 @@ pulls.desc = Enable pull requests and code reviews.
|
||||
pulls.new = New Pull Request
|
||||
pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
|
||||
pulls.new.must_collaborator = You must be a collaborator to create pull request.
|
||||
pulls.new.already_existed = A pull request between these branches already exists
|
||||
pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.
|
||||
pulls.view = View Pull Request
|
||||
pulls.compare_changes = New Pull Request
|
||||
|
||||
@@ -148,7 +148,7 @@ func EnumeratePackages(ctx *context.Context) {
|
||||
Timestamp: fileMetadata.Timestamp,
|
||||
Build: fileMetadata.Build,
|
||||
BuildNumber: fileMetadata.BuildNumber,
|
||||
Dependencies: fileMetadata.Dependencies,
|
||||
Dependencies: util.SliceNilAsEmpty(fileMetadata.Dependencies),
|
||||
License: versionMetadata.License,
|
||||
LicenseFamily: versionMetadata.LicenseFamily,
|
||||
HashMD5: pfd.Blob.HashMD5,
|
||||
|
||||
@@ -26,9 +26,18 @@ import (
|
||||
|
||||
// saveAsPackageBlob creates a package blob from an upload
|
||||
// The uploaded blob gets stored in a special upload version to link them to the package/image
|
||||
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam // PackageBlob is never used
|
||||
// There will be concurrent uploading for the same blob, so it needs a global lock per blob hash
|
||||
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam //returned PackageBlob is never used
|
||||
pb := packages_service.NewPackageBlob(hsr)
|
||||
err := globallock.LockAndDo(ctx, "container-blob:"+pb.HashSHA256, func(ctx context.Context) error {
|
||||
var err error
|
||||
pb, err = saveAsPackageBlobInternal(ctx, hsr, pci, pb)
|
||||
return err
|
||||
})
|
||||
return pb, err
|
||||
}
|
||||
|
||||
func saveAsPackageBlobInternal(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo, pb *packages_model.PackageBlob) (*packages_model.PackageBlob, error) {
|
||||
exists := false
|
||||
|
||||
contentStore := packages_module.NewContentStore()
|
||||
@@ -67,7 +76,7 @@ func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader
|
||||
return createFileForBlob(ctx, uploadVersion, pb)
|
||||
})
|
||||
if err != nil {
|
||||
if !exists {
|
||||
if !exists && pb != nil { // pb can be nil if GetOrInsertBlob failed
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
|
||||
@@ -290,8 +290,8 @@ func PostBlobsUploads(ctx *context.Context) {
|
||||
Creator: ctx.Doer,
|
||||
},
|
||||
); err != nil {
|
||||
switch err {
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
switch {
|
||||
case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize):
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@@ -439,8 +439,8 @@ func PutBlobsUpload(ctx *context.Context) {
|
||||
Creator: ctx.Doer,
|
||||
},
|
||||
); err != nil {
|
||||
switch err {
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
switch {
|
||||
case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize):
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@@ -592,13 +592,10 @@ func PutManifest(ctx *context.Context) {
|
||||
apiErrorDefined(ctx, namedError)
|
||||
} else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
|
||||
apiErrorDefined(ctx, errBlobUnknown)
|
||||
} else if errors.Is(err, packages_service.ErrQuotaTotalCount) || errors.Is(err, packages_service.ErrQuotaTypeSize) || errors.Is(err, packages_service.ErrQuotaTotalSize) {
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
} else {
|
||||
switch err {
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
@@ -83,9 +82,11 @@ type processManifestTxRet struct {
|
||||
}
|
||||
|
||||
func handleCreateManifestResult(ctx context.Context, err error, mci *manifestCreationInfo, contentStore *packages_module.ContentStore, txRet *processManifestTxRet) (string, error) {
|
||||
if err != nil && txRet.created && txRet.pb != nil {
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(txRet.pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
if err != nil {
|
||||
if txRet.created && txRet.pb != nil {
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(txRet.pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
@@ -199,14 +200,14 @@ func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *p
|
||||
if errors.Is(err, container_model.ErrContainerBlobNotExist) {
|
||||
return errManifestBlobUnknown
|
||||
}
|
||||
return err
|
||||
return fmt.Errorf("GetContainerBlob: %w", err)
|
||||
}
|
||||
|
||||
size, err := packages_model.CalculateFileSize(ctx, &packages_model.PackageFileSearchOptions{
|
||||
VersionID: pfd.File.VersionID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("CalculateFileSize: %w", err)
|
||||
}
|
||||
|
||||
metadata.Manifests = append(metadata.Manifests, &container_module.Manifest{
|
||||
@@ -218,7 +219,7 @@ func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *p
|
||||
|
||||
pv, err := createPackageAndVersion(ctx, mci, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("createPackageAndVersion: %w", err)
|
||||
}
|
||||
|
||||
txRet.pv = pv
|
||||
@@ -241,7 +242,7 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackage) {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("TryInsertPackage: %w", err)
|
||||
}
|
||||
created = false
|
||||
}
|
||||
@@ -249,7 +250,7 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
||||
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
|
||||
return nil, fmt.Errorf("InsertProperty(PropertyRepository): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,9 +258,16 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
||||
|
||||
metadataJSON, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("json.Marshal(metadata): %w", err)
|
||||
}
|
||||
|
||||
// "docker buildx imagetools create" multi-arch operations:
|
||||
// {"type":"oci","is_tagged":false,"platform":"unknown/unknown"}
|
||||
// {"type":"oci","is_tagged":false,"platform":"linux/amd64","layer_creation":["ADD file:9233f6f2237d79659a9521f7e390df217cec49f1a8aa3a12147bbca1956acdb9 in /","CMD [\"/bin/sh\"]"]}
|
||||
// {"type":"oci","is_tagged":false,"platform":"unknown/unknown"}
|
||||
// {"type":"oci","is_tagged":false,"platform":"linux/arm64","layer_creation":["ADD file:df53811312284306901fdaaff0a357a4bf40d631e662fe9ce6d342442e494b6c in /","CMD [\"/bin/sh\"]"]}
|
||||
// {"type":"oci","is_tagged":true,"manifests":[{"platform":"linux/amd64","digest":"sha256:72bb73e706c0dec424d00a1febb21deaf1175a70ead009ad8b159729cfcf5769","size":2819478},{"platform":"linux/arm64","digest":"sha256:9e1426dd084a3221663b85ca1ee99d140c50b153917a5c5604c1f9b78229fd24","size":2716499},{"platform":"unknown/unknown","digest":"sha256:b93f03d0ae11b988243e1b2cd8d29accf5b9670547b7bd8c7d96abecc7283e6e","size":1798},{"platform":"unknown/unknown","digest":"sha256:f034b182ba66366c63a5d195c6dfcd3333c027409c0ac98e55ade36aaa3b2963","size":1798}]}
|
||||
|
||||
_pv := &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: mci.Creator.ID,
|
||||
@@ -270,52 +278,43 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
||||
pv, err := packages_model.GetOrInsertVersion(ctx, _pv)
|
||||
if err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
log.Error("Error GetOrInsertVersion (first try) package: %v", err)
|
||||
return nil, fmt.Errorf("GetOrInsertVersion: first try: %w", err)
|
||||
}
|
||||
|
||||
if container_module.IsMediaTypeImageIndex(mci.MediaType) {
|
||||
if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) {
|
||||
if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// keep download count on overwriting
|
||||
_pv.DownloadCount = pv.DownloadCount
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = packages_model.UpdateVersion(ctx, &packages_model.PackageVersion{ID: pv.ID, MetadataJSON: _pv.MetadataJSON})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
return nil, fmt.Errorf("DeletePackageVersionAndReferences: %w", err)
|
||||
}
|
||||
// keep download count on overwriting
|
||||
_pv.DownloadCount = pv.DownloadCount
|
||||
pv, err = packages_model.GetOrInsertVersion(ctx, _pv)
|
||||
if err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
|
||||
log.Error("Error GetOrInsertVersion (second try) package: %v", err)
|
||||
return nil, fmt.Errorf("GetOrInsertVersion: second try: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := packages_service.CheckCountQuotaExceeded(ctx, mci.Creator, mci.Owner); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("CheckCountQuotaExceeded: %w", err)
|
||||
}
|
||||
|
||||
if mci.IsTagged {
|
||||
if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("InsertOrUpdateProperty(ManifestTagged): %w", err)
|
||||
}
|
||||
} else {
|
||||
if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("DeletePropertiesByName(ManifestTagged): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("DeletePropertiesByName(ManifestReference): %w", err)
|
||||
}
|
||||
for _, manifest := range metadata.Manifests {
|
||||
if _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("InsertProperty(ManifestReference): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -216,9 +216,12 @@ func EditUser(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if form.Email != nil {
|
||||
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
|
||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
|
||||
if !user_model.IsEmailDomainAllowed(*form.Email) {
|
||||
err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)
|
||||
}
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
case user_model.IsErrEmailAlreadyUsed(err):
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
@@ -227,10 +230,6 @@ func EditUser(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !user_model.IsEmailDomainAllowed(*form.Email) {
|
||||
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
|
||||
}
|
||||
}
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
|
||||
+14
-11
@@ -82,6 +82,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/activitypub"
|
||||
"code.gitea.io/gitea/routers/api/v1/admin"
|
||||
@@ -152,7 +153,7 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
|
||||
context.RedirectToUser(ctx.Base, userName, redirectUserID)
|
||||
context.RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID)
|
||||
} else if user_model.IsErrUserRedirectNotExist(err) {
|
||||
ctx.APIErrorNotFound("GetUserByName", err)
|
||||
} else {
|
||||
@@ -628,7 +629,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam("org"))
|
||||
if err == nil {
|
||||
context.RedirectToUser(ctx.Base, ctx.PathParam("org"), redirectUserID)
|
||||
context.RedirectToUser(ctx.Base, ctx.Doer, ctx.PathParam("org"), redirectUserID)
|
||||
} else if user_model.IsErrUserRedirectNotExist(err) {
|
||||
ctx.APIErrorNotFound("GetOrgByName", err)
|
||||
} else {
|
||||
@@ -791,7 +792,9 @@ func apiAuth(authMethod auth.Method) func(*context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
ar, err := common.AuthShared(ctx.Base, nil, authMethod)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnauthorized, err)
|
||||
msg, ok := auth.ErrAsUserAuthMessage(err)
|
||||
msg = util.Iif(ok, msg, "invalid username, password or token")
|
||||
ctx.APIError(http.StatusUnauthorized, msg)
|
||||
return
|
||||
}
|
||||
ctx.Doer = ar.Doer
|
||||
@@ -1397,19 +1400,19 @@ func Routes() *web.Router {
|
||||
})
|
||||
m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
|
||||
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
|
||||
m.Group("/statuses", func() {
|
||||
m.Group("/statuses", func() { // "/statuses/{sha}" only accepts commit ID
|
||||
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Group("/commits", func() {
|
||||
m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits)
|
||||
m.Group("/{ref}", func() {
|
||||
m.Get("/status", repo.GetCombinedCommitStatusByRef)
|
||||
m.Get("/statuses", repo.GetCommitStatusesByRef)
|
||||
}, context.ReferencesGitRepo())
|
||||
m.Group("/{sha}", func() {
|
||||
m.Get("/pull", repo.GetCommitPullRequest)
|
||||
}, context.ReferencesGitRepo())
|
||||
m.PathGroup("/*", func(g *web.RouterPathGroup) {
|
||||
// Mis-configured reverse proxy might decode the `%2F` to slash ahead, so we need to support both formats (escaped, unescaped) here.
|
||||
// It also matches GitHub's behavior
|
||||
g.MatchPath("GET", "/<ref:*>/status", repo.GetCombinedCommitStatusByRef)
|
||||
g.MatchPath("GET", "/<ref:*>/statuses", repo.GetCommitStatusesByRef)
|
||||
g.MatchPath("GET", "/<sha>/pull", repo.GetCommitPullRequest)
|
||||
})
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Group("/git", func() {
|
||||
m.Group("/commits", func() {
|
||||
|
||||
@@ -897,7 +897,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
whitelistUsers = protectBranch.WhitelistUserIDs
|
||||
}
|
||||
if form.ForcePushAllowlistDeployKeys != nil {
|
||||
if form.ForcePushAllowlistUsernames != nil {
|
||||
forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
|
||||
@@ -369,11 +369,11 @@ func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) {
|
||||
},
|
||||
Signoff: commonOpts.Signoff,
|
||||
}
|
||||
if commonOpts.Dates.Author.IsZero() {
|
||||
commonOpts.Dates.Author = time.Now()
|
||||
if changeFileOpts.Dates.Author.IsZero() {
|
||||
changeFileOpts.Dates.Author = time.Now()
|
||||
}
|
||||
if commonOpts.Dates.Committer.IsZero() {
|
||||
commonOpts.Dates.Committer = time.Now()
|
||||
if changeFileOpts.Dates.Committer.IsZero() {
|
||||
changeFileOpts.Dates.Committer = time.Now()
|
||||
}
|
||||
ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
dependencyPerm := getPermissionForRepo(ctx, target.Repo)
|
||||
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -262,7 +262,7 @@ func RemoveIssueDependency(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
dependencyPerm := getPermissionForRepo(ctx, target.Repo)
|
||||
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ func GetStopwatches(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
apiSWs, err := convert.ToStopWatches(ctx, sws)
|
||||
apiSWs, err := convert.ToStopWatches(ctx, ctx.Doer, sws)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
+13
-17
@@ -29,6 +29,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/automerge"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@@ -1076,7 +1077,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
} else if len(headInfos) == 2 {
|
||||
// There is a head repository (the head repository could also be the same base repo)
|
||||
headRefToGuess = headInfos[1]
|
||||
headUser, err = user_model.GetUserByName(ctx, headInfos[0])
|
||||
headUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIErrorNotFound("GetUserByName")
|
||||
@@ -1092,28 +1093,23 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
|
||||
isSameRepo := ctx.Repo.Owner.ID == headUser.ID
|
||||
|
||||
// Check if current user has fork of repository or in the same repository.
|
||||
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
|
||||
if headRepo == nil && !isSameRepo {
|
||||
err = baseRepo.GetBaseRepo(ctx)
|
||||
var headRepo *repo_model.Repository
|
||||
if isSameRepo {
|
||||
headRepo = baseRepo
|
||||
} else {
|
||||
headRepo, err = common.FindHeadRepo(ctx, baseRepo, headUser.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check if baseRepo's base repository is the same as headUser's repository.
|
||||
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
|
||||
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
|
||||
ctx.APIErrorNotFound("GetBaseRepo")
|
||||
if headRepo == nil {
|
||||
ctx.APIErrorNotFound("head repository not found")
|
||||
return nil, nil
|
||||
}
|
||||
// Assign headRepo so it can be used below.
|
||||
headRepo = baseRepo.BaseRepo
|
||||
}
|
||||
|
||||
var headGitRepo *git.Repository
|
||||
if isSameRepo {
|
||||
headRepo = ctx.Repo.Repository
|
||||
headGitRepo = ctx.Repo.GitRepo
|
||||
closer = func() {} // no need to close the head repo because it shares the base repo
|
||||
} else {
|
||||
@@ -1137,7 +1133,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
|
||||
if !permBase.CanRead(unit.TypeCode) {
|
||||
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
|
||||
ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
|
||||
return nil, nil
|
||||
@@ -1341,7 +1337,7 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if ctx.Doer.ID != autoMerge.DoerID {
|
||||
allowed, err := access_model.IsUserRepoAdmin(ctx, ctx.Repo.Repository, ctx.Doer)
|
||||
allowed, err := pull_service.IsUserAllowedToMerge(ctx, pull, ctx.Repo.Permission, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -1552,9 +1548,9 @@ func GetPullRequestFiles(ctx *context.APIContext) {
|
||||
|
||||
var prInfo *pull_service.CompareInfo
|
||||
if pr.HasMerged {
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), true, false)
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), false, false)
|
||||
} else {
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), true, false)
|
||||
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), false, false)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
||||
@@ -398,7 +398,6 @@ func DeleteReleaseAttachment(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
|
||||
|
||||
if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
release_service "code.gitea.io/gitea/services/release"
|
||||
@@ -58,6 +59,13 @@ func GetReleaseByTag(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if release.IsDraft { // only the users with write access can see draft releases
|
||||
if !ctx.IsSigned || !ctx.Repo.CanWrite(unit_model.TypeReleases) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = release.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -16,7 +16,7 @@ func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User {
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
if redirectUserID, err2 := user_model.LookupUserRedirect(ctx, username); err2 == nil {
|
||||
context.RedirectToUser(ctx.Base, username, redirectUserID)
|
||||
context.RedirectToUser(ctx.Base, ctx.Doer, username, redirectUserID)
|
||||
} else {
|
||||
ctx.APIErrorNotFound("GetUserByName", err)
|
||||
}
|
||||
|
||||
@@ -44,9 +44,11 @@ func isRoutePathExpensive(routePattern string) bool {
|
||||
"/{username}/{reponame}/blame/",
|
||||
"/{username}/{reponame}/commit/",
|
||||
"/{username}/{reponame}/commits/",
|
||||
"/{username}/{reponame}/compare/",
|
||||
"/{username}/{reponame}/graph",
|
||||
"/{username}/{reponame}/media/",
|
||||
"/{username}/{reponame}/raw/",
|
||||
"/{username}/{reponame}/rss/branch/",
|
||||
"/{username}/{reponame}/src/",
|
||||
|
||||
// issue & PR related (no trailing slash)
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@@ -20,3 +22,54 @@ type CompareInfo struct {
|
||||
HeadBranch string
|
||||
DirectComparison bool
|
||||
}
|
||||
|
||||
// maxForkTraverseLevel defines the maximum levels to traverse when searching for the head repository.
|
||||
const maxForkTraverseLevel = 10
|
||||
|
||||
// FindHeadRepo tries to find the head repository based on the base repository and head user ID.
|
||||
func FindHeadRepo(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64) (*repo_model.Repository, error) {
|
||||
if baseRepo.IsFork {
|
||||
curRepo := baseRepo
|
||||
for curRepo.OwnerID != headUserID { // We assume the fork deepth is not too deep.
|
||||
if err := curRepo.GetBaseRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if curRepo.BaseRepo == nil {
|
||||
return findHeadRepoFromRootBase(ctx, curRepo, headUserID, maxForkTraverseLevel)
|
||||
}
|
||||
curRepo = curRepo.BaseRepo
|
||||
}
|
||||
return curRepo, nil
|
||||
}
|
||||
|
||||
return findHeadRepoFromRootBase(ctx, baseRepo, headUserID, maxForkTraverseLevel)
|
||||
}
|
||||
|
||||
func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) {
|
||||
if traverseLevel == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// test if we are lucky
|
||||
repo, err := repo_model.GetUserFork(ctx, baseRepo.ID, headUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if repo != nil {
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
firstLevelForkedRepos, err := repo_model.GetRepositoriesByForkID(ctx, baseRepo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, repo := range firstLevelForkedRepos {
|
||||
forked, err := findHeadRepoFromRootBase(ctx, repo, headUserID, traverseLevel-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if forked != nil {
|
||||
return forked, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -71,8 +71,11 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
|
||||
req = req.WithContext(cache.WithCacheContext(ctx))
|
||||
ds.SetContextValue(httplib.RequestContextKey, req)
|
||||
ds.AddCleanUp(func() {
|
||||
if req.MultipartForm != nil {
|
||||
_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
||||
// The req in context might have changed due to the new req.WithContext calls
|
||||
// For example: in NewBaseContext, a new "req" with context is created, and the multipart-form is parsed there.
|
||||
ctxReq := ds.GetContextValue(httplib.RequestContextKey).(*http.Request)
|
||||
if ctxReq.MultipartForm != nil {
|
||||
_ = ctxReq.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
||||
}
|
||||
})
|
||||
next.ServeHTTP(respWriter, req)
|
||||
|
||||
+48
-39
@@ -108,21 +108,19 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||
results.RepoName = repoName[:len(repoName)-5]
|
||||
}
|
||||
|
||||
// Check if there is a user redirect for the requested owner
|
||||
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
|
||||
if err == nil {
|
||||
owner, err := user_model.GetUserByID(ctx, redirectedUserID)
|
||||
if err == nil {
|
||||
log.Info("User %s has been redirected to %s", results.OwnerName, owner.Name)
|
||||
results.OwnerName = owner.Name
|
||||
} else {
|
||||
log.Warn("User %s has a redirect to user with ID %d, but no user with this ID could be found. Trying without redirect...", results.OwnerName, redirectedUserID)
|
||||
}
|
||||
}
|
||||
|
||||
owner, err := user_model.GetUserByName(ctx, results.OwnerName)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there is a user redirect for the requested owner
|
||||
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
|
||||
if err != nil {
|
||||
// User is fetching/cloning a non-existent repository
|
||||
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
|
||||
ctx.JSON(http.StatusNotFound, private.Response{
|
||||
@@ -130,11 +128,20 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err),
|
||||
})
|
||||
return
|
||||
|
||||
redirectUser, err := user_model.GetUserByID(ctx, redirectedUserID)
|
||||
if err != nil {
|
||||
// User is fetching/cloning a non-existent repository
|
||||
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
|
||||
ctx.JSON(http.StatusNotFound, private.Response{
|
||||
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("User %s has been redirected to %s", results.OwnerName, redirectUser.Name)
|
||||
results.OwnerName = redirectUser.Name
|
||||
owner = redirectUser
|
||||
}
|
||||
if !owner.IsOrganization() && !owner.IsActive {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
@@ -143,24 +150,33 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||
return
|
||||
}
|
||||
|
||||
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
|
||||
if err == nil {
|
||||
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
|
||||
if err == nil {
|
||||
log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
|
||||
results.RepoName = redirectedRepo.Name
|
||||
results.OwnerName = redirectedRepo.OwnerName
|
||||
owner.ID = redirectedRepo.OwnerID
|
||||
} else {
|
||||
log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID)
|
||||
}
|
||||
}
|
||||
|
||||
// Now get the Repository and set the results section
|
||||
repoExist := true
|
||||
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
|
||||
if err == nil {
|
||||
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
|
||||
if err == nil {
|
||||
log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
|
||||
results.RepoName = redirectedRepo.Name
|
||||
results.OwnerName = redirectedRepo.OwnerName
|
||||
repo = redirectedRepo
|
||||
owner.ID = redirectedRepo.OwnerID
|
||||
} else {
|
||||
log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID)
|
||||
}
|
||||
}
|
||||
|
||||
if repo == nil {
|
||||
repoExist = false
|
||||
if mode == perm.AccessModeRead {
|
||||
// User is fetching/cloning a non-existent repository
|
||||
@@ -170,13 +186,6 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||
})
|
||||
return
|
||||
}
|
||||
// else fallthrough (push-to-create may kick in below)
|
||||
} else {
|
||||
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -409,7 +409,7 @@ func EditUserPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if form.Email != "" {
|
||||
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
|
||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, u, form.Email); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
|
||||
@@ -277,8 +277,11 @@ type LinkAccountData struct {
|
||||
GothUser goth.User
|
||||
}
|
||||
|
||||
func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
|
||||
func init() {
|
||||
gob.Register(LinkAccountData{})
|
||||
}
|
||||
|
||||
func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
|
||||
v, ok := ctx.Session.Get("linkAccountData").(LinkAccountData)
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -287,7 +290,6 @@ func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
|
||||
}
|
||||
|
||||
func Oauth2SetLinkAccountData(ctx *context.Context, linkAccountData LinkAccountData) error {
|
||||
gob.Register(LinkAccountData{})
|
||||
return updateSession(ctx, nil, map[string]any{
|
||||
"linkAccountData": linkAccountData,
|
||||
})
|
||||
|
||||
@@ -230,8 +230,7 @@ func AuthorizeOAuth(ctx *context.Context) {
|
||||
|
||||
// pkce support
|
||||
switch form.CodeChallengeMethod {
|
||||
case "S256":
|
||||
case "plain":
|
||||
case "S256", "plain":
|
||||
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
|
||||
handleAuthorizeError(ctx, AuthorizeError{
|
||||
ErrorCode: ErrorCodeServerError,
|
||||
|
||||
+29
-71
@@ -205,22 +205,24 @@ func ChangeProjectStatus(ctx *context.Context) {
|
||||
}
|
||||
id := ctx.PathParamInt64("id")
|
||||
|
||||
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
|
||||
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id))
|
||||
}
|
||||
|
||||
// DeleteProject delete a project
|
||||
func DeleteProject(ctx *context.Context) {
|
||||
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, id, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if p.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
|
||||
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, project.ID, toClose); err != nil {
|
||||
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, project.ID))
|
||||
}
|
||||
|
||||
// DeleteProject delete a project
|
||||
func DeleteProject(ctx *context.Context) {
|
||||
p, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -246,15 +248,11 @@ func RenderEditProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
p, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if p.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["projectID"] = p.ID
|
||||
ctx.Data["title"] = p.Title
|
||||
@@ -288,15 +286,11 @@ func EditProjectPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
p, err := project_model.GetProjectByID(ctx, projectID)
|
||||
p, err := project_model.GetProjectByIDAndOwner(ctx, projectID, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if p.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
p.Title = form.Title
|
||||
p.Description = form.Content
|
||||
@@ -316,15 +310,12 @@ func EditProjectPost(ctx *context.Context) {
|
||||
|
||||
// ViewProject renders the project with board view for a project
|
||||
func ViewProject(ctx *context.Context) {
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project.LoadOwner(ctx); err != nil {
|
||||
ctx.ServerError("LoadOwner", err)
|
||||
return
|
||||
@@ -436,6 +427,7 @@ func ViewProject(ctx *context.Context) {
|
||||
ctx.Data["Project"] = project
|
||||
ctx.Data["IssuesMap"] = issuesMap
|
||||
ctx.Data["Columns"] = columns
|
||||
ctx.Data["Title"] = fmt.Sprintf("%s - %s", project.Title, ctx.ContextUser.DisplayName())
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
@@ -454,28 +446,15 @@ func DeleteProjectColumn(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID"))
|
||||
_, err = project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectColumn", err)
|
||||
return
|
||||
}
|
||||
if pb.ProjectID != ctx.PathParamInt64("id") {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||
"message": fmt.Sprintf("ProjectColumn[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
|
||||
})
|
||||
ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -491,7 +470,7 @@ func DeleteProjectColumn(ctx *context.Context) {
|
||||
func AddColumnToProjectPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
@@ -519,30 +498,18 @@ func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.P
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID"))
|
||||
column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectColumn", err)
|
||||
return nil, nil
|
||||
}
|
||||
if column.ProjectID != ctx.PathParamInt64("id") {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
|
||||
})
|
||||
ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||
"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, project.ID),
|
||||
})
|
||||
return nil, nil
|
||||
}
|
||||
return project, column
|
||||
}
|
||||
|
||||
@@ -594,24 +561,15 @@ func MoveIssues(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID"))
|
||||
column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if column.ProjectID != project.ID {
|
||||
ctx.NotFound(nil)
|
||||
ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
package org_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/org"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -26,3 +29,30 @@ func TestCheckProjectColumnChangePermissions(t *testing.T) {
|
||||
assert.NotNil(t, column)
|
||||
assert.False(t, ctx.Written())
|
||||
}
|
||||
|
||||
func TestChangeProjectStatusRejectsForeignProjects(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
// project 4 is owned by user2 not user1
|
||||
ctx, _ := contexttest.MockContext(t, "user1/-/projects/4/close")
|
||||
contexttest.LoadUser(t, ctx, 1)
|
||||
ctx.ContextUser = ctx.Doer
|
||||
ctx.SetPathParam("action", "close")
|
||||
ctx.SetPathParam("id", "4")
|
||||
|
||||
org.ChangeProjectStatus(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
|
||||
func TestAddColumnToProjectPostRejectsForeignProjects(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "user1/-/projects/4/columns/new")
|
||||
contexttest.LoadUser(t, ctx, 1)
|
||||
ctx.ContextUser = ctx.Doer
|
||||
ctx.SetPathParam("id", "4")
|
||||
web.SetForm(ctx, &forms.EditProjectColumnForm{Title: "foreign"})
|
||||
|
||||
org.AddColumnToProjectPost(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -40,7 +41,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
|
||||
|
||||
file, header, err := ctx.Req.FormFile("file")
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
|
||||
ctx.ServerError("FormFile", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -56,7 +57,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err))
|
||||
ctx.ServerError("UploadAttachmentGeneralSizeLimit", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,13 +75,44 @@ func DeleteAttachment(ctx *context.Context) {
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != attach.UploaderID) {
|
||||
|
||||
if !ctx.IsSigned {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if attach.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.HTTPError(http.StatusBadRequest, "attachment does not belong to this repository")
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Doer.ID != attach.UploaderID {
|
||||
if attach.IssueID > 0 {
|
||||
issue, err := issues_model.GetIssueByID(ctx, attach.IssueID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetIssueByID", err)
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
} else if attach.ReleaseID > 0 {
|
||||
if !ctx.Repo.Permission.CanWrite(unit.TypeReleases) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !ctx.Repo.Permission.IsAdmin() && !ctx.Repo.Permission.IsOwner() {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = repo_model.DeleteAttachment(ctx, attach, true)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err))
|
||||
ctx.ServerError("DeleteAttachment", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]string{
|
||||
@@ -100,23 +132,41 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
return
|
||||
}
|
||||
|
||||
repository, unitType, err := repo_service.LinkedRepository(ctx, attach)
|
||||
if err != nil {
|
||||
ctx.ServerError("LinkedRepository", err)
|
||||
// prevent visiting attachment from other repository directly
|
||||
// The check will be ignored before this code merged.
|
||||
if attach.CreatedUnix > repo_model.LegacyAttachmentMissingRepoIDCutoff && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID != attach.RepoID {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if repository == nil { // If not linked
|
||||
unitType, repoID, err := repo_service.GetAttachmentLinkedTypeAndRepoID(ctx, attach)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAttachmentLinkedTypeAndRepoID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if unitType == unit.TypeInvalid { // unlinked attachment can only be accessed by the uploader
|
||||
if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
} else { // If we have the repository we check access
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
|
||||
return
|
||||
} else { // If we have the linked type, we need to check access
|
||||
var perm access_model.Permission
|
||||
if ctx.Repo.Repository == nil {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepositoryByID", err)
|
||||
return
|
||||
}
|
||||
perm, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
perm = ctx.Repo.Permission
|
||||
}
|
||||
|
||||
if !perm.CanRead(unitType) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
|
||||
@@ -258,7 +258,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
} else if len(headInfos) == 2 {
|
||||
headInfosSplit := strings.Split(headInfos[0], "/")
|
||||
if len(headInfosSplit) == 1 {
|
||||
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
|
||||
ci.HeadUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.NotFound(nil)
|
||||
|
||||
@@ -312,11 +312,13 @@ func EditFile(ctx *context.Context) {
|
||||
ctx.ServerError("ReadAll", err)
|
||||
return
|
||||
}
|
||||
var fileContent string
|
||||
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
|
||||
ctx.Data["FileContent"] = string(buf)
|
||||
fileContent = string(buf)
|
||||
} else {
|
||||
ctx.Data["FileContent"] = content
|
||||
fileContent = string(content)
|
||||
}
|
||||
ctx.Data["FileContent"] = fileContent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,7 @@ func CherryPick(ctx *context.Context) {
|
||||
ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
|
||||
} else {
|
||||
ctx.Data["CherryPickType"] = "cherry-pick"
|
||||
splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
|
||||
ctx.Data["commit_summary"] = splits[0]
|
||||
ctx.Data["commit_message"] = splits[1]
|
||||
ctx.Data["commit_summary"], ctx.Data["commit_message"], _ = strings.Cut(cherryPickCommit.Message(), "\n")
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplCherryPick)
|
||||
|
||||
+25
-30
@@ -31,6 +31,7 @@ import (
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
@@ -57,7 +58,7 @@ func CorsHandler() func(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// httpBase implementation git smart HTTP protocol
|
||||
func httpBase(ctx *context.Context) *serviceHandler {
|
||||
func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
username := ctx.PathParam("username")
|
||||
reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
|
||||
|
||||
@@ -67,18 +68,14 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
||||
}
|
||||
|
||||
var isPull, receivePack bool
|
||||
service := ctx.FormString("service")
|
||||
if service == "git-receive-pack" ||
|
||||
strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
|
||||
isPull = false
|
||||
switch util.OptionalArg(optGitService) {
|
||||
case "git-receive-pack":
|
||||
receivePack = true
|
||||
} else if service == "git-upload-pack" ||
|
||||
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
|
||||
case "git-upload-pack":
|
||||
isPull = true
|
||||
} else if service == "git-upload-archive" ||
|
||||
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
|
||||
case "git-upload-archive":
|
||||
isPull = true
|
||||
} else {
|
||||
default:
|
||||
isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
|
||||
}
|
||||
|
||||
@@ -147,7 +144,13 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
||||
// rely on the results of Contexter
|
||||
if !ctx.IsSigned {
|
||||
// TODO: support digit auth - which would be Authorization header with digit
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
||||
if setting.OAuth2.Enabled {
|
||||
// `Basic realm="Gitea"` tells the GCM to use builtin OAuth2 application: https://github.com/git-ecosystem/git-credential-manager/pull/1442
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
|
||||
} else {
|
||||
// If OAuth2 is disabled, then use another realm to avoid GCM OAuth2 attempt
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea (Basic Auth)"`)
|
||||
}
|
||||
ctx.HTTPError(http.StatusUnauthorized)
|
||||
return nil
|
||||
}
|
||||
@@ -405,13 +408,19 @@ func prepareGitCmdWithAllowedService(service string) (*gitcmd.Command, error) {
|
||||
return nil, fmt.Errorf("service %q is not allowed", service)
|
||||
}
|
||||
|
||||
func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
|
||||
func serviceRPC(ctx *context.Context, service string) {
|
||||
defer func() {
|
||||
if err := ctx.Req.Body.Close(); err != nil {
|
||||
log.Error("serviceRPC: Close: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
h := httpBase(ctx, "git-"+service)
|
||||
if h == nil {
|
||||
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
|
||||
if ctx.Req.Header.Get("Content-Type") != expectedContentType {
|
||||
log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
|
||||
@@ -466,26 +475,12 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
|
||||
|
||||
// ServiceUploadPack implements Git Smart HTTP protocol
|
||||
func ServiceUploadPack(ctx *context.Context) {
|
||||
h := httpBase(ctx)
|
||||
if h != nil {
|
||||
serviceRPC(ctx, h, "upload-pack")
|
||||
}
|
||||
serviceRPC(ctx, "upload-pack")
|
||||
}
|
||||
|
||||
// ServiceReceivePack implements Git Smart HTTP protocol
|
||||
func ServiceReceivePack(ctx *context.Context) {
|
||||
h := httpBase(ctx)
|
||||
if h != nil {
|
||||
serviceRPC(ctx, h, "receive-pack")
|
||||
}
|
||||
}
|
||||
|
||||
func getServiceType(ctx *context.Context) string {
|
||||
serviceType := ctx.Req.FormValue("service")
|
||||
if !strings.HasPrefix(serviceType, "git-") {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimPrefix(serviceType, "git-")
|
||||
serviceRPC(ctx, "receive-pack")
|
||||
}
|
||||
|
||||
func updateServerInfo(ctx gocontext.Context, dir string) []byte {
|
||||
@@ -506,12 +501,12 @@ func packetWrite(str string) []byte {
|
||||
|
||||
// GetInfoRefs implements Git dumb HTTP
|
||||
func GetInfoRefs(ctx *context.Context) {
|
||||
h := httpBase(ctx)
|
||||
service := strings.TrimPrefix(ctx.Req.FormValue("service"), "git-")
|
||||
h := httpBase(ctx, "git-"+service)
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
setHeaderNoCache(ctx)
|
||||
service := getServiceType(ctx)
|
||||
cmd, err := prepareGitCmdWithAllowedService(service)
|
||||
if err == nil {
|
||||
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
|
||||
|
||||
@@ -206,12 +206,11 @@ func SoftDeleteContentHistory(ctx *context.Context) {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
if history.CommentID != commentID {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
if commentID != 0 {
|
||||
if history.CommentID != commentID {
|
||||
ctx.NotFound(issues_model.ErrCommentNotExist{})
|
||||
return
|
||||
}
|
||||
|
||||
if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
|
||||
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
|
||||
return
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user