Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efe7561787 | ||
|
|
e01e78a947 | ||
|
|
04d489dbdd | ||
|
|
491ee43082 | ||
|
|
9107a87ff6 | ||
|
|
3dc2724d36 | ||
|
|
93fe0202cb | ||
|
|
13f304d89e | ||
|
|
805c5926ff | ||
|
|
b6eea680ce | ||
|
|
9c33aff689 | ||
|
|
a3e185bc5c | ||
|
|
2f1d968b27 | ||
|
|
0c212b3f08 | ||
|
|
ceedb4973e | ||
|
|
f5f4a8d02a | ||
|
|
543322f81f | ||
|
|
35a3b452d9 | ||
|
|
5a60e023af | ||
|
|
1170e067b2 | ||
|
|
8adc6a188e | ||
|
|
48eb5ac685 | ||
|
|
8f5b2f1ddf | ||
|
|
bbfd34575a | ||
|
|
760cf419ba | ||
|
|
90982bffa5 | ||
|
|
8fa62be905 | ||
|
|
7b3ffe5745 | ||
|
|
c50d4202ef | ||
|
|
660a83bd2e | ||
|
|
4c7786b3b6 | ||
|
|
c702e7995d | ||
|
|
b2e58edd74 | ||
|
|
98b7714c3b | ||
|
|
9da4642c8c | ||
|
|
1d191f9b5a | ||
|
|
2e1afd54b2 | ||
|
|
9e68261ca7 | ||
|
|
e4238583db | ||
|
|
656d5a144f | ||
|
|
43d1183f67 | ||
|
|
8fa419c4c1 | ||
|
|
77c89572e9 | ||
|
|
68b908d92a | ||
|
|
638fbd0b78 | ||
|
|
3647e62ef9 | ||
|
|
37bbf2c902 | ||
|
|
a239d6c4a9 | ||
|
|
ff2014690d | ||
|
|
03c644c48c | ||
|
|
965376d476 | ||
|
|
2e12161620 | ||
|
|
9cde526f87 | ||
|
|
4c20be7c00 | ||
|
|
263d06f616 | ||
|
|
6dc16c1154 | ||
|
|
fd2c250b52 | ||
|
|
e6d6bce1f6 | ||
|
|
a9ba7379fe | ||
|
|
6be1d71e2b | ||
|
|
9f5e44bf50 | ||
|
|
f204ff4ef7 | ||
|
|
f6cb7860a2 | ||
|
|
6068978c42 | ||
|
|
c320caed97 | ||
|
|
f1c826ed29 | ||
|
|
3c531d3957 | ||
|
|
1ae2525922 | ||
|
|
fd7ebaaa9c | ||
|
|
fa33271157 | ||
|
|
4b3e456afa | ||
|
|
63e5db5d7a | ||
|
|
e6e2c2f4a4 | ||
|
|
e902b98cc2 | ||
|
|
6992e72647 | ||
|
|
1bbf490926 | ||
|
|
45bdeac730 | ||
|
|
a32700d0fd | ||
|
|
a9400ba7a3 | ||
|
|
9a6d78eaa8 | ||
|
|
af8151cbb9 | ||
|
|
ee37edc465 | ||
|
|
29bbfcc118 | ||
|
|
f430050d24 | ||
|
|
510c811574 | ||
|
|
f93522ddae | ||
|
|
10c9f96a1e | ||
|
|
7b60d47c3c | ||
|
|
265d438a6e | ||
|
|
93e907de41 | ||
|
|
f3034b1fd9 | ||
|
|
d0c74dd2d2 | ||
|
|
2f91a12143 | ||
|
|
3ad62127df | ||
|
|
37e23c982f | ||
|
|
421d87933b | ||
|
|
426c0ad14c | ||
|
|
41a06d2e82 | ||
|
|
885082f7a7 | ||
|
|
32999e2511 | ||
|
|
16d7596635 | ||
|
|
adc0bcaebb | ||
|
|
0cca1e079b | ||
|
|
55c6433fac | ||
|
|
5b8763476a | ||
|
|
09c667eb45 | ||
|
|
791f290c26 | ||
|
|
58e642c1d6 | ||
|
|
72d1f9e63e | ||
|
|
0697075547 | ||
|
|
f1e07d8c87 | ||
|
|
443fd27a90 | ||
|
|
75f128ebf8 |
42
.drone.yml
42
.drone.yml
@@ -769,10 +769,16 @@ steps:
|
||||
image: woodpeckerci/plugin-s3:latest
|
||||
pull: always
|
||||
settings:
|
||||
acl: public-read
|
||||
bucket: gitea-artifacts
|
||||
endpoint: https://ams3.digitaloceanspaces.com
|
||||
path_style: true
|
||||
acl:
|
||||
from_secret: aws_s3_acl
|
||||
region:
|
||||
from_secret: aws_s3_region
|
||||
bucket:
|
||||
from_secret: aws_s3_bucket
|
||||
endpoint:
|
||||
from_secret: aws_s3_endpoint
|
||||
path_style:
|
||||
from_secret: aws_s3_path_style
|
||||
source: "dist/release/*"
|
||||
strip_prefix: dist/release/
|
||||
target: "/gitea/${DRONE_BRANCH##release/v}"
|
||||
@@ -790,10 +796,16 @@ steps:
|
||||
- name: release-main
|
||||
image: woodpeckerci/plugin-s3:latest
|
||||
settings:
|
||||
acl: public-read
|
||||
bucket: gitea-artifacts
|
||||
endpoint: https://ams3.digitaloceanspaces.com
|
||||
path_style: true
|
||||
acl:
|
||||
from_secret: aws_s3_acl
|
||||
region:
|
||||
from_secret: aws_s3_region
|
||||
bucket:
|
||||
from_secret: aws_s3_bucket
|
||||
endpoint:
|
||||
from_secret: aws_s3_endpoint
|
||||
path_style:
|
||||
from_secret: aws_s3_path_style
|
||||
source: "dist/release/*"
|
||||
strip_prefix: dist/release/
|
||||
target: /gitea/main
|
||||
@@ -892,10 +904,16 @@ steps:
|
||||
image: woodpeckerci/plugin-s3:latest
|
||||
pull: always
|
||||
settings:
|
||||
acl: public-read
|
||||
bucket: gitea-artifacts
|
||||
endpoint: https://ams3.digitaloceanspaces.com
|
||||
path_style: true
|
||||
acl:
|
||||
from_secret: aws_s3_acl
|
||||
region:
|
||||
from_secret: aws_s3_region
|
||||
bucket:
|
||||
from_secret: aws_s3_bucket
|
||||
endpoint:
|
||||
from_secret: aws_s3_endpoint
|
||||
path_style:
|
||||
from_secret: aws_s3_path_style
|
||||
source: "dist/release/*"
|
||||
strip_prefix: dist/release/
|
||||
target: "/gitea/${DRONE_TAG##v}"
|
||||
|
||||
@@ -173,3 +173,6 @@ issues:
|
||||
linters:
|
||||
- revive
|
||||
text: "exported: type name will be used as user.UserBadge by other packages, and that stutters; consider calling this Badge"
|
||||
- path: models/db/sql_postgres_with_schema.go
|
||||
linters:
|
||||
- nolintlint
|
||||
|
||||
111
CHANGELOG.md
111
CHANGELOG.md
@@ -4,6 +4,117 @@ This changelog goes through all the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
|
||||
## [1.18.5](https://github.com/go-gitea/gitea/releases/tag/v1.18.5) - 2023-02-21
|
||||
|
||||
* ENHANCEMENTS
|
||||
* Hide 2FA status from other members in organization members list (#22999) (#23023)
|
||||
* BUGFIXES
|
||||
* Add force_merge to merge request and fix checking mergable (#23010) (#23032)
|
||||
* Use `--message=%s` for git commit message (#23028) (#23029)
|
||||
* Render access log template as text instead of HTML (#23013) (#23025)
|
||||
* Fix the Manually Merged form (#23015) (#23017)
|
||||
* Use beforeCommit instead of baseCommit (#22949) (#22996)
|
||||
* Display attachments of review comment when comment content is blank (#23035) (#23046)
|
||||
* Return empty url for submodule tree entries (#23043) (#23048)
|
||||
|
||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20
|
||||
|
||||
* SECURITY
|
||||
* Provide the ability to set password hash algorithm parameters (#22942) (#22943)
|
||||
* Add command to bulk set must-change-password (#22823) (#22928)
|
||||
* ENHANCEMENTS
|
||||
* Use import of OCI structs (#22765) (#22805)
|
||||
* Fix color of tertiary button on dark theme (#22739) (#22744)
|
||||
* Link issue and pull requests status change in UI notifications directly to their event in the timelined view. (#22627) (#22642)
|
||||
* BUGFIXES
|
||||
* Notify on container image create (#22806) (#22965)
|
||||
* Fix blame view missing lines (#22826) (#22929)
|
||||
* Fix incorrect role labels for migrated issues and comments (#22914) (#22923)
|
||||
* Fix PR file tree folders no longer collapsing (#22864) (#22872)
|
||||
* Escape filename when assemble URL (#22850) (#22871)
|
||||
* Fix isAllowed of escapeStreamer (#22814) (#22837)
|
||||
* Load issue before accessing index in merge message (#22822) (#22830)
|
||||
* Improve trace logging for pulls and processes (#22633) (#22812)
|
||||
* Fix restore repo bug, clarify the problem of ForeignIndex (#22776) (#22794)
|
||||
* Add default user visibility to cli command "admin user create" (#22750) (#22760)
|
||||
* Escape path for the file list (#22741) (#22757)
|
||||
* Fix bugs with WebAuthn preventing sign in and registration. (#22651) (#22721)
|
||||
* Add missing close bracket in imagediff (#22710) (#22712)
|
||||
* Move code comments to a standalone file and fix the bug when adding a reply to an outdated review appears to not post(#20821) (#22707)
|
||||
* Fix line spacing for plaintext previews (#22699) (#22701)
|
||||
* Fix wrong hint when deleting a branch successfully from pull request UI (#22673) (#22698)
|
||||
* Fix README TOC links (#22577) (#22677)
|
||||
* Fix missing message in git hook when pull requests disabled on fork (#22625) (#22658)
|
||||
* Improve checkIfPRContentChanged (#22611) (#22644)
|
||||
* Prevent duplicate labels when importing more than 99 (#22591) (#22598)
|
||||
* Don't return duplicated users who can create org repo (#22560) (#22562)
|
||||
* BUILD
|
||||
* Upgrade golangcilint to v1.51.0 (#22764)
|
||||
* MISC
|
||||
* Use proxy for pull mirror (#22771) (#22772)
|
||||
* Use `--index-url` in PyPi description (#22620) (#22636)
|
||||
|
||||
## [1.18.3](https://github.com/go-gitea/gitea/releases/tag/v1.18.3) - 2023-01-23
|
||||
|
||||
* SECURITY
|
||||
* Prevent multiple `To` recipients (#22566) (#22569)
|
||||
* BUGFIXES
|
||||
* Truncate commit summary on repo files table. (#22551) (#22552)
|
||||
* Mute all links in issue timeline (#22534)
|
||||
|
||||
## [1.18.2](https://github.com/go-gitea/gitea/releases/tag/v1.18.2) - 2023-01-19
|
||||
|
||||
* BUGFIXES
|
||||
* When updating by rebase we need to set the environment for head repo (#22535) (#22536)
|
||||
* Fix issue not auto-closing when it includes a reference to a branch (#22514) (#22521)
|
||||
* Fix invalid issue branch reference if not specified in template (#22513) (#22520)
|
||||
* Fix 500 error viewing pull request when fork has pull requests disabled (#22512) (#22515)
|
||||
* Reliable selection of admin user (#22509) (#22511)
|
||||
* Set disable_gravatar/enable_federated_avatar when offline mode is true (#22479) (#22496)
|
||||
* BUILD
|
||||
* cgo cross-compile for freebsd (#22397) (#22519)
|
||||
|
||||
## [1.18.1](https://github.com/go-gitea/gitea/releases/tag/v1.18.1) - 2023-01-17
|
||||
|
||||
* API
|
||||
* Add `sync_on_commit` option for push mirrors api (#22271) (#22292)
|
||||
* BUGFIXES
|
||||
* Update `github.com/zeripath/zapx/v15` (#22485)
|
||||
* Fix pull request API field `closed_at` always being `null` (#22482) (#22483)
|
||||
* Fix container blob mount (#22226) (#22476)
|
||||
* Fix error when calculating repository size (#22392) (#22474)
|
||||
* Fix Operator does not exist bug on explore page with ONLY_SHOW_RELEVANT_REPOS (#22454) (#22472)
|
||||
* Fix environments for KaTeX and error reporting (#22453) (#22473)
|
||||
* Remove the netgo tag for Windows build (#22467) (#22468)
|
||||
* Fix migration from GitBucket (#22477) (#22465)
|
||||
* Prevent panic on looking at api "git" endpoints for empty repos (#22457) (#22458)
|
||||
* Fix PR status layout on mobile (#21547) (#22441)
|
||||
* Fix wechatwork webhook sends empty content in PR review (#21762) (#22440)
|
||||
* Remove duplicate "Actions" label in mobile view (#21974) (#22439)
|
||||
* Fix leaving organization bug on user settings -> orgs (#21983) (#22438)
|
||||
* Fixed colour transparency regex matching in project board sorting (#22092) (#22437)
|
||||
* Correctly handle select on multiple channels in Queues (#22146) (#22428)
|
||||
* Prepend refs/heads/ to issue template refs (#20461) (#22427)
|
||||
* Restore function to "Show more" buttons (#22399) (#22426)
|
||||
* Continue GCing other repos on error in one repo (#22422) (#22425)
|
||||
* Allow HOST has no port (#22280) (#22409)
|
||||
* Fix omit avatar_url in discord payload when empty (#22393) (#22394)
|
||||
* Don't display stop watch top bar icon when disabled and hidden when click other place (#22374) (#22387)
|
||||
* Don't lookup mail server when using sendmail (#22300) (#22383)
|
||||
* Fix gravatar disable bug (#22337)
|
||||
* Fix update settings table on install (#22326) (#22327)
|
||||
* Fix sitemap (#22272) (#22320)
|
||||
* Fix code search title translation (#22285) (#22316)
|
||||
* Fix due date rendering the wrong date in issue (#22302) (#22306)
|
||||
* Fix get system setting bug when enabled redis cache (#22298)
|
||||
* Fix bug of DisableGravatar default value (#22297)
|
||||
* Fix key signature error page (#22229) (#22230)
|
||||
* TESTING
|
||||
* Remove test session cache to reduce possible concurrent problem (#22199) (#22429)
|
||||
* MISC
|
||||
* Restore previous official review when an official review is deleted (#22449) (#22460)
|
||||
* Log STDERR of external renderer when it fails (#22442) (#22444)
|
||||
|
||||
## [1.18.0](https://github.com/go-gitea/gitea/releases/tag/1.18.0) - 2022-12-22
|
||||
|
||||
* SECURITY
|
||||
|
||||
17
Makefile
17
Makefile
@@ -28,8 +28,8 @@ XGO_VERSION := go-1.19.x
|
||||
AIR_PACKAGE ?= github.com/cosmtrek/air@v1.40.4
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.5.0
|
||||
ERRCHECK_PACKAGE ?= github.com/kisielk/errcheck@v1.6.1
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.3.1
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.0
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.4.0
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.0
|
||||
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
|
||||
MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.0
|
||||
@@ -733,16 +733,16 @@ $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
|
||||
|
||||
.PHONY: release
|
||||
release: frontend generate release-windows release-linux release-darwin release-copy release-compress vendor release-sources release-docs release-check
|
||||
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-docs release-check
|
||||
|
||||
$(DIST_DIRS):
|
||||
mkdir -p $(DIST_DIRS)
|
||||
|
||||
.PHONY: release-windows
|
||||
release-windows: | $(DIST_DIRS)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
|
||||
ifeq (,$(findstring gogit,$(TAGS)))
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
|
||||
endif
|
||||
ifeq ($(CI),true)
|
||||
cp /build/* $(DIST)/binaries
|
||||
@@ -762,6 +762,13 @@ ifeq ($(CI),true)
|
||||
cp /build/* $(DIST)/binaries
|
||||
endif
|
||||
|
||||
.PHONY: release-freebsd
|
||||
release-freebsd: | $(DIST_DIRS)
|
||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
|
||||
ifeq ($(CI),true)
|
||||
cp /build/* $(DIST)/binaries
|
||||
endif
|
||||
|
||||
.PHONY: release-copy
|
||||
release-copy: | $(DIST_DIRS)
|
||||
cd $(DIST); for file in `find . -type f -name "*"`; do cp $${file} ./release/; done;
|
||||
|
||||
2
assets/emoji.json
generated
2
assets/emoji.json
generated
File diff suppressed because one or more lines are too long
10
assets/go-licenses.json
generated
10
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
const (
|
||||
gemojiURL = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
|
||||
maxUnicodeVersion = 12
|
||||
maxUnicodeVersion = 14
|
||||
)
|
||||
|
||||
var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out")
|
||||
|
||||
403
cmd/admin.go
403
cmd/admin.go
@@ -6,7 +6,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -17,20 +16,14 @@ import (
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
pwd "code.gitea.io/gitea/modules/password"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/auth/source/smtp"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@@ -49,142 +42,6 @@ var (
|
||||
},
|
||||
}
|
||||
|
||||
subcmdUser = cli.Command{
|
||||
Name: "user",
|
||||
Usage: "Modify users",
|
||||
Subcommands: []cli.Command{
|
||||
microcmdUserCreate,
|
||||
microcmdUserList,
|
||||
microcmdUserChangePassword,
|
||||
microcmdUserDelete,
|
||||
microcmdUserGenerateAccessToken,
|
||||
},
|
||||
}
|
||||
|
||||
microcmdUserList = cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List users",
|
||||
Action: runListUsers,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "List only admin users",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
microcmdUserCreate = cli.Command{
|
||||
Name: "create",
|
||||
Usage: "Create a new user in database",
|
||||
Action: runCreateUser,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "Username. DEPRECATED: use username instead",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "username",
|
||||
Usage: "Username",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "User password",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "email",
|
||||
Usage: "User email address",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "User is an admin",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "random-password",
|
||||
Usage: "Generate a random password for the user",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "random-password-length",
|
||||
Usage: "Length of the random password to be generated",
|
||||
Value: 12,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "restricted",
|
||||
Usage: "Make a restricted user account",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
microcmdUserChangePassword = cli.Command{
|
||||
Name: "change-password",
|
||||
Usage: "Change a user's password",
|
||||
Action: runChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "username,u",
|
||||
Value: "",
|
||||
Usage: "The user to change password for",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password,p",
|
||||
Value: "",
|
||||
Usage: "New password to set for user",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
microcmdUserDelete = cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete specific user by id, name or email",
|
||||
Flags: []cli.Flag{
|
||||
cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of user of the user to delete",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "username,u",
|
||||
Usage: "Username of the user to delete",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "email,e",
|
||||
Usage: "Email of the user to delete",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "purge",
|
||||
Usage: "Purge user, all their repositories, organizations and comments",
|
||||
},
|
||||
},
|
||||
Action: runDeleteUser,
|
||||
}
|
||||
|
||||
microcmdUserGenerateAccessToken = cli.Command{
|
||||
Name: "generate-access-token",
|
||||
Usage: "Generate a access token for a specific user",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "username,u",
|
||||
Usage: "Username",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "token-name,t",
|
||||
Usage: "Token name",
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "raw",
|
||||
Usage: "Display only the token value",
|
||||
},
|
||||
},
|
||||
Action: runGenerateAccessToken,
|
||||
}
|
||||
|
||||
subcmdRepoSyncReleases = cli.Command{
|
||||
Name: "repo-sync-releases",
|
||||
Usage: "Synchronize repository releases with tags",
|
||||
@@ -304,6 +161,11 @@ var (
|
||||
Value: "false",
|
||||
Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "custom-tenant-id",
|
||||
Value: "",
|
||||
Usage: "Use custom Tenant ID for OAuth endpoints",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "custom-auth-url",
|
||||
Value: "",
|
||||
@@ -468,255 +330,6 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func runChangePassword(c *cli.Context) error {
|
||||
if err := argsSet(c, "username", "password"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(c.String("password")) < setting.MinPasswordLength {
|
||||
return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
|
||||
}
|
||||
|
||||
if !pwd.IsComplexEnough(c.String("password")) {
|
||||
return errors.New("Password does not meet complexity requirements")
|
||||
}
|
||||
pwned, err := pwd.IsPwned(context.Background(), c.String("password"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pwned {
|
||||
return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
|
||||
}
|
||||
uname := c.String("username")
|
||||
user, err := user_model.GetUserByName(ctx, uname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = user.SetPassword(c.String("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s's password has been successfully updated!\n", user.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCreateUser(c *cli.Context) error {
|
||||
if err := argsSet(c, "email"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.IsSet("name") && c.IsSet("username") {
|
||||
return errors.New("Cannot set both --name and --username flags")
|
||||
}
|
||||
if !c.IsSet("name") && !c.IsSet("username") {
|
||||
return errors.New("One of --name or --username flags must be set")
|
||||
}
|
||||
|
||||
if c.IsSet("password") && c.IsSet("random-password") {
|
||||
return errors.New("cannot set both -random-password and -password flags")
|
||||
}
|
||||
|
||||
var username string
|
||||
if c.IsSet("username") {
|
||||
username = c.String("username")
|
||||
} else {
|
||||
username = c.String("name")
|
||||
fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var password string
|
||||
if c.IsSet("password") {
|
||||
password = c.String("password")
|
||||
} else if c.IsSet("random-password") {
|
||||
var err error
|
||||
password, err = pwd.Generate(c.Int("random-password-length"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("generated random password is '%s'\n", password)
|
||||
} else {
|
||||
return errors.New("must set either password or random-password flag")
|
||||
}
|
||||
|
||||
// always default to true
|
||||
changePassword := true
|
||||
|
||||
// If this is the first user being created.
|
||||
// Take it as the admin and don't force a password update.
|
||||
if n := user_model.CountUsers(nil); n == 0 {
|
||||
changePassword = false
|
||||
}
|
||||
|
||||
if c.IsSet("must-change-password") {
|
||||
changePassword = c.Bool("must-change-password")
|
||||
}
|
||||
|
||||
restricted := util.OptionalBoolNone
|
||||
|
||||
if c.IsSet("restricted") {
|
||||
restricted = util.OptionalBoolOf(c.Bool("restricted"))
|
||||
}
|
||||
|
||||
u := &user_model.User{
|
||||
Name: username,
|
||||
Email: c.String("email"),
|
||||
Passwd: password,
|
||||
IsAdmin: c.Bool("admin"),
|
||||
MustChangePassword: changePassword,
|
||||
}
|
||||
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
IsActive: util.OptionalBoolTrue,
|
||||
IsRestricted: restricted,
|
||||
}
|
||||
|
||||
if err := user_model.CreateUser(u, overwriteDefault); err != nil {
|
||||
return fmt.Errorf("CreateUser: %w", err)
|
||||
}
|
||||
|
||||
if c.Bool("access-token") {
|
||||
t := &auth_model.AccessToken{
|
||||
Name: "gitea-admin",
|
||||
UID: u.ID,
|
||||
}
|
||||
|
||||
if err := auth_model.NewAccessToken(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Access token was successfully created... %s\n", t.Token)
|
||||
}
|
||||
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runListUsers(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users, err := user_model.GetAllUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
|
||||
|
||||
if c.IsSet("admin") {
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n")
|
||||
for _, u := range users {
|
||||
if u.IsAdmin {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
twofa := user_model.UserList(users).GetTwoFaStatus()
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
|
||||
for _, u := range users {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDeleteUser(c *cli.Context) error {
|
||||
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
|
||||
return fmt.Errorf("You must provide the id, username or email of a user to delete")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := storage.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var user *user_model.User
|
||||
if c.IsSet("email") {
|
||||
user, err = user_model.GetUserByEmail(c.String("email"))
|
||||
} else if c.IsSet("username") {
|
||||
user, err = user_model.GetUserByName(ctx, c.String("username"))
|
||||
} else {
|
||||
user, err = user_model.GetUserByID(c.Int64("id"))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
|
||||
return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
|
||||
}
|
||||
|
||||
if c.IsSet("id") && user.ID != c.Int64("id") {
|
||||
return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
|
||||
}
|
||||
|
||||
return user_service.DeleteUser(ctx, user, c.Bool("purge"))
|
||||
}
|
||||
|
||||
func runGenerateAccessToken(c *cli.Context) error {
|
||||
if !c.IsSet("username") {
|
||||
return fmt.Errorf("You must provide the username to generate a token for them")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := user_model.GetUserByName(ctx, c.String("username"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := &auth_model.AccessToken{
|
||||
Name: c.String("token-name"),
|
||||
UID: user.ID,
|
||||
}
|
||||
|
||||
if err := auth_model.NewAccessToken(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("raw") {
|
||||
fmt.Printf("%s\n", t.Token)
|
||||
} else {
|
||||
fmt.Printf("Access token was successfully created: %s\n", t.Token)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRepoSyncReleases(_ *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
@@ -814,6 +427,7 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
|
||||
AuthURL: c.String("custom-auth-url"),
|
||||
ProfileURL: c.String("custom-profile-url"),
|
||||
EmailURL: c.String("custom-email-url"),
|
||||
Tenant: c.String("custom-tenant-id"),
|
||||
}
|
||||
} else {
|
||||
customURLMapping = nil
|
||||
@@ -923,6 +537,7 @@ func runUpdateOauth(c *cli.Context) error {
|
||||
customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL
|
||||
customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL
|
||||
customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL
|
||||
customURLMapping.Tenant = oAuth2Config.CustomURLMapping.Tenant
|
||||
}
|
||||
if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") {
|
||||
customURLMapping.TokenURL = c.String("custom-token-url")
|
||||
@@ -940,6 +555,10 @@ func runUpdateOauth(c *cli.Context) error {
|
||||
customURLMapping.EmailURL = c.String("custom-email-url")
|
||||
}
|
||||
|
||||
if c.IsSet("use-custom-urls") && c.IsSet("custom-tenant-id") {
|
||||
customURLMapping.Tenant = c.String("custom-tenant-id")
|
||||
}
|
||||
|
||||
oAuth2Config.CustomURLMapping = customURLMapping
|
||||
source.Cfg = oAuth2Config
|
||||
|
||||
|
||||
21
cmd/admin_user.go
Normal file
21
cmd/admin_user.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var subcmdUser = cli.Command{
|
||||
Name: "user",
|
||||
Usage: "Modify users",
|
||||
Subcommands: []cli.Command{
|
||||
microcmdUserCreate,
|
||||
microcmdUserList,
|
||||
microcmdUserChangePassword,
|
||||
microcmdUserDelete,
|
||||
microcmdUserGenerateAccessToken,
|
||||
microcmdUserMustChangePassword,
|
||||
},
|
||||
}
|
||||
76
cmd/admin_user_change_password.go
Normal file
76
cmd/admin_user_change_password.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
pwd "code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var microcmdUserChangePassword = cli.Command{
|
||||
Name: "change-password",
|
||||
Usage: "Change a user's password",
|
||||
Action: runChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "username,u",
|
||||
Value: "",
|
||||
Usage: "The user to change password for",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password,p",
|
||||
Value: "",
|
||||
Usage: "New password to set for user",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runChangePassword(c *cli.Context) error {
|
||||
if err := argsSet(c, "username", "password"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(c.String("password")) < setting.MinPasswordLength {
|
||||
return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength)
|
||||
}
|
||||
|
||||
if !pwd.IsComplexEnough(c.String("password")) {
|
||||
return errors.New("Password does not meet complexity requirements")
|
||||
}
|
||||
pwned, err := pwd.IsPwned(context.Background(), c.String("password"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pwned {
|
||||
return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
|
||||
}
|
||||
uname := c.String("username")
|
||||
user, err := user_model.GetUserByName(ctx, uname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = user.SetPassword(c.String("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s's password has been successfully updated!\n", user.Name)
|
||||
return nil
|
||||
}
|
||||
169
cmd/admin_user_create.go
Normal file
169
cmd/admin_user_create.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
pwd "code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var microcmdUserCreate = cli.Command{
|
||||
Name: "create",
|
||||
Usage: "Create a new user in database",
|
||||
Action: runCreateUser,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "Username. DEPRECATED: use username instead",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "username",
|
||||
Usage: "Username",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "User password",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "email",
|
||||
Usage: "User email address",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "User is an admin",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "random-password",
|
||||
Usage: "Generate a random password for the user",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "must-change-password",
|
||||
Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "random-password-length",
|
||||
Usage: "Length of the random password to be generated",
|
||||
Value: 12,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "restricted",
|
||||
Usage: "Make a restricted user account",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runCreateUser(c *cli.Context) error {
|
||||
if err := argsSet(c, "email"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.IsSet("name") && c.IsSet("username") {
|
||||
return errors.New("Cannot set both --name and --username flags")
|
||||
}
|
||||
if !c.IsSet("name") && !c.IsSet("username") {
|
||||
return errors.New("One of --name or --username flags must be set")
|
||||
}
|
||||
|
||||
if c.IsSet("password") && c.IsSet("random-password") {
|
||||
return errors.New("cannot set both -random-password and -password flags")
|
||||
}
|
||||
|
||||
var username string
|
||||
if c.IsSet("username") {
|
||||
username = c.String("username")
|
||||
} else {
|
||||
username = c.String("name")
|
||||
fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var password string
|
||||
if c.IsSet("password") {
|
||||
password = c.String("password")
|
||||
} else if c.IsSet("random-password") {
|
||||
var err error
|
||||
password, err = pwd.Generate(c.Int("random-password-length"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("generated random password is '%s'\n", password)
|
||||
} else {
|
||||
return errors.New("must set either password or random-password flag")
|
||||
}
|
||||
|
||||
// always default to true
|
||||
changePassword := true
|
||||
|
||||
// If this is the first user being created.
|
||||
// Take it as the admin and don't force a password update.
|
||||
if n := user_model.CountUsers(nil); n == 0 {
|
||||
changePassword = false
|
||||
}
|
||||
|
||||
if c.IsSet("must-change-password") {
|
||||
changePassword = c.Bool("must-change-password")
|
||||
}
|
||||
|
||||
restricted := util.OptionalBoolNone
|
||||
|
||||
if c.IsSet("restricted") {
|
||||
restricted = util.OptionalBoolOf(c.Bool("restricted"))
|
||||
}
|
||||
|
||||
// default user visibility in app.ini
|
||||
visibility := setting.Service.DefaultUserVisibilityMode
|
||||
|
||||
u := &user_model.User{
|
||||
Name: username,
|
||||
Email: c.String("email"),
|
||||
Passwd: password,
|
||||
IsAdmin: c.Bool("admin"),
|
||||
MustChangePassword: changePassword,
|
||||
Visibility: visibility,
|
||||
}
|
||||
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
IsActive: util.OptionalBoolTrue,
|
||||
IsRestricted: restricted,
|
||||
}
|
||||
|
||||
if err := user_model.CreateUser(u, overwriteDefault); err != nil {
|
||||
return fmt.Errorf("CreateUser: %w", err)
|
||||
}
|
||||
|
||||
if c.Bool("access-token") {
|
||||
t := &auth_model.AccessToken{
|
||||
Name: "gitea-admin",
|
||||
UID: u.ID,
|
||||
}
|
||||
|
||||
if err := auth_model.NewAccessToken(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Access token was successfully created... %s\n", t.Token)
|
||||
}
|
||||
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
return nil
|
||||
}
|
||||
78
cmd/admin_user_delete.go
Normal file
78
cmd/admin_user_delete.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var microcmdUserDelete = cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete specific user by id, name or email",
|
||||
Flags: []cli.Flag{
|
||||
cli.Int64Flag{
|
||||
Name: "id",
|
||||
Usage: "ID of user of the user to delete",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "username,u",
|
||||
Usage: "Username of the user to delete",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "email,e",
|
||||
Usage: "Email of the user to delete",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "purge",
|
||||
Usage: "Purge user, all their repositories, organizations and comments",
|
||||
},
|
||||
},
|
||||
Action: runDeleteUser,
|
||||
}
|
||||
|
||||
func runDeleteUser(c *cli.Context) error {
|
||||
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
|
||||
return fmt.Errorf("You must provide the id, username or email of a user to delete")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := storage.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var user *user_model.User
|
||||
if c.IsSet("email") {
|
||||
user, err = user_model.GetUserByEmail(c.String("email"))
|
||||
} else if c.IsSet("username") {
|
||||
user, err = user_model.GetUserByName(ctx, c.String("username"))
|
||||
} else {
|
||||
user, err = user_model.GetUserByID(c.Int64("id"))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
|
||||
return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
|
||||
}
|
||||
|
||||
if c.IsSet("id") && user.ID != c.Int64("id") {
|
||||
return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
|
||||
}
|
||||
|
||||
return user_service.DeleteUser(ctx, user, c.Bool("purge"))
|
||||
}
|
||||
69
cmd/admin_user_generate_access_token.go
Normal file
69
cmd/admin_user_generate_access_token.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var microcmdUserGenerateAccessToken = cli.Command{
|
||||
Name: "generate-access-token",
|
||||
Usage: "Generate an access token for a specific user",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "username,u",
|
||||
Usage: "Username",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "token-name,t",
|
||||
Usage: "Token name",
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "raw",
|
||||
Usage: "Display only the token value",
|
||||
},
|
||||
},
|
||||
Action: runGenerateAccessToken,
|
||||
}
|
||||
|
||||
func runGenerateAccessToken(c *cli.Context) error {
|
||||
if !c.IsSet("username") {
|
||||
return fmt.Errorf("You must provide a username to generate a token for")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := user_model.GetUserByName(ctx, c.String("username"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := &auth_model.AccessToken{
|
||||
Name: c.String("token-name"),
|
||||
UID: user.ID,
|
||||
}
|
||||
|
||||
if err := auth_model.NewAccessToken(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("raw") {
|
||||
fmt.Printf("%s\n", t.Token)
|
||||
} else {
|
||||
fmt.Printf("Access token was successfully created: %s\n", t.Token)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
60
cmd/admin_user_list.go
Normal file
60
cmd/admin_user_list.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var microcmdUserList = cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List users",
|
||||
Action: runListUsers,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "List only admin users",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runListUsers(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users, err := user_model.GetAllUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
|
||||
|
||||
if c.IsSet("admin") {
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n")
|
||||
for _, u := range users {
|
||||
if u.IsAdmin {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
twofa := user_model.UserList(users).GetTwoFaStatus()
|
||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
|
||||
for _, u := range users {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID])
|
||||
}
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
58
cmd/admin_user_must_change_password.go
Normal file
58
cmd/admin_user_must_change_password.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var microcmdUserMustChangePassword = cli.Command{
|
||||
Name: "must-change-password",
|
||||
Usage: "Set the must change password flag for the provided users or all users",
|
||||
Action: runMustChangePassword,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "all,A",
|
||||
Usage: "All users must change password, except those explicitly excluded with --exclude",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "exclude,e",
|
||||
Usage: "Do not change the must-change-password flag for these users",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "unset",
|
||||
Usage: "Instead of setting the must-change-password flag, unset it",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func runMustChangePassword(c *cli.Context) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
||||
if c.NArg() == 0 && !c.IsSet("all") {
|
||||
return errors.New("either usernames or --all must be provided")
|
||||
}
|
||||
|
||||
mustChangePassword := !c.Bool("unset")
|
||||
all := c.Bool("all")
|
||||
exclude := c.StringSlice("exclude")
|
||||
|
||||
if err := initDB(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args(), exclude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword)
|
||||
return nil
|
||||
}
|
||||
25
cmd/serv.go
25
cmd/serv.go
@@ -12,6 +12,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -290,17 +291,21 @@ func runServ(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Special handle for Windows.
|
||||
if setting.IsWindows {
|
||||
verb = strings.Replace(verb, "-", " ", 1)
|
||||
}
|
||||
|
||||
var gitcmd *exec.Cmd
|
||||
verbs := strings.Split(verb, " ")
|
||||
if len(verbs) == 2 {
|
||||
gitcmd = exec.CommandContext(ctx, verbs[0], verbs[1], repoPath)
|
||||
} else {
|
||||
gitcmd = exec.CommandContext(ctx, verb, repoPath)
|
||||
gitBinPath := filepath.Dir(git.GitExecutable) // e.g. /usr/bin
|
||||
gitBinVerb := filepath.Join(gitBinPath, verb) // e.g. /usr/bin/git-upload-pack
|
||||
if _, err := os.Stat(gitBinVerb); err != nil {
|
||||
// if the command "git-upload-pack" doesn't exist, try to split "git-upload-pack" to use the sub-command with git
|
||||
// ps: Windows only has "git.exe" in the bin path, so Windows always uses this way
|
||||
verbFields := strings.SplitN(verb, "-", 2)
|
||||
if len(verbFields) == 2 {
|
||||
// use git binary with the sub-command part: "C:\...\bin\git.exe", "upload-pack", ...
|
||||
gitcmd = exec.CommandContext(ctx, git.GitExecutable, verbFields[1], repoPath)
|
||||
}
|
||||
}
|
||||
if gitcmd == nil {
|
||||
// by default, use the verb (it has been checked above by allowedCommands)
|
||||
gitcmd = exec.CommandContext(ctx, gitBinVerb, repoPath)
|
||||
}
|
||||
|
||||
process.SetSysProcAttribute(gitcmd)
|
||||
|
||||
@@ -523,7 +523,21 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
|
||||
- `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server.
|
||||
- `INTERNAL_TOKEN`: **\<random at every install if no uri set\>**: Secret used to validate communication within Gitea binary.
|
||||
- `INTERNAL_TOKEN_URI`: **<empty>**: Instead of defining INTERNAL_TOKEN in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
|
||||
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, scrypt, bcrypt\], argon2 will spend more memory than others.
|
||||
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, pbkdf2_v1, scrypt, bcrypt\], argon2 and scrypt will spend significant amounts of memory.
|
||||
- Note: The default parameters for `pbkdf2` hashing have changed - the previous settings are available as `pbkdf2_v1` but are not recommended.
|
||||
- The hash functions may be tuned by using `$` after the algorithm:
|
||||
- `argon2$<time>$<memory>$<threads>$<key-length>`
|
||||
- `bcrypt$<cost>`
|
||||
- `pbkdf2$<iterations>$<key-length>`
|
||||
- `scrypt$<n>$<r>$<p>$<key-length>`
|
||||
- The defaults are:
|
||||
- `argon2`: `argon2$2$65536$8$50`
|
||||
- `bcrypt`: `bcrypt$10`
|
||||
- `pbkdf2`: `pbkdf2$320000$50`
|
||||
- `pbkdf2_v1`: `pbkdf2$10000$50`
|
||||
- `pbkdf2_v2`: `pbkdf2$320000$50`
|
||||
- `scrypt`: `scrypt$65536$16$2$50`
|
||||
- Adjusting the algorithm parameters using this functionality is done at your own risk.
|
||||
- `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.
|
||||
- `MIN_PASSWORD_LENGTH`: **6**: Minimum password length for new users.
|
||||
- `PASSWORD_COMPLEXITY`: **off**: Comma separated list of character classes required to pass minimum complexity. If left empty or no valid values are specified, checking is disabled (off):
|
||||
@@ -735,9 +749,9 @@ and
|
||||
|
||||
- `GRAVATAR_SOURCE`: **gravatar**: Can be `gravatar`, `duoshuo` or anything like
|
||||
`http://cn.gravatar.com/avatar/`.
|
||||
- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only.
|
||||
- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only. **DEPRECATED [v1.18+]** moved to database. Use admin panel to configure.
|
||||
- `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see
|
||||
[http://www.libravatar.org](http://www.libravatar.org)).
|
||||
[http://www.libravatar.org](http://www.libravatar.org)). **DEPRECATED [v1.18+]** moved to database. Use admin panel to configure.
|
||||
|
||||
- `AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`.
|
||||
- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files.
|
||||
|
||||
@@ -7,7 +7,7 @@ toc: false
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "advanced"
|
||||
parent: "developers"
|
||||
name: "加入 Gitea 开源"
|
||||
weight: 10
|
||||
identifier: "hacking-on-gitea"
|
||||
@@ -15,7 +15,7 @@ menu:
|
||||
|
||||
# Hacking on Gitea
|
||||
|
||||
首先你需要一些运行环境,这和 [从源代码安装]({{< relref "from-source.zh-cn.md" >}}) 相同,如果你还没有设置好,可以先阅读那个章节。
|
||||
首先你需要一些运行环境,这和 [从源代码安装]({{< relref "doc/installation/from-source.zh-cn.md" >}}) 相同,如果你还没有设置好,可以先阅读那个章节。
|
||||
|
||||
如果你想为 Gitea 贡献代码,你需要 Fork 这个项目并且以 `master` 为开发分支。Gitea 使用 Govendor
|
||||
来管理依赖,因此所有依赖项都被工具自动 copy 在 vendor 子目录下。用下面的命令来下载源码:
|
||||
@@ -32,4 +32,4 @@ chmod +x gitea
|
||||
|
||||
## 需要帮助?
|
||||
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "seek-help.zh-cn.md" >}})
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "doc/help/seek-help.zh-cn.md" >}})
|
||||
|
||||
@@ -64,11 +64,11 @@ OpenSUSE 构建服务为 [openSUSE 和 SLE](https://software.opensuse.org/downlo
|
||||
choco install gitea
|
||||
```
|
||||
|
||||
你也可以 [从二进制安装]({{< relref "from-binary.zh-cn.md" >}}) 。
|
||||
你也可以 [从二进制安装]({{< relref "doc/installation/from-binary.zh-cn.md" >}}) 。
|
||||
|
||||
## macOS
|
||||
|
||||
macOS 平台下当前我们仅支持通过 `brew` 来安装。如果你没有安装 [Homebrew](http://brew.sh/),你也可以查看 [从二进制安装]({{< relref "from-binary.zh-cn.md" >}})。在你安装了 `brew` 之后, 你可以执行以下命令:
|
||||
macOS 平台下当前我们仅支持通过 `brew` 来安装。如果你没有安装 [Homebrew](http://brew.sh/),你也可以查看 [从二进制安装]({{< relref "doc/installation/from-binary.zh-cn.md" >}})。在你安装了 `brew` 之后, 你可以执行以下命令:
|
||||
|
||||
```
|
||||
brew tap gitea/tap https://gitea.com/gitea/homebrew-gitea
|
||||
@@ -105,4 +105,4 @@ make install clean
|
||||
|
||||
## 需要帮助?
|
||||
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "seek-help.zh-cn.md" >}})
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "doc/help/seek-help.zh-cn.md" >}})
|
||||
|
||||
@@ -54,7 +54,7 @@ git checkout v{{< version >}}
|
||||
|
||||
- `go` {{< min-go-version >}} 或以上版本, 详见[这里](https://golang.google.cn/doc/install)
|
||||
- `node` {{< min-node-version >}} 或以上版本,并且安装 `npm`, 详见[这里](https://nodejs.org/zh-cn/download/)
|
||||
- `make`, 详见[这里]({{< relref "make.zh-cn.md" >}})</a>
|
||||
- `make`, 详见[这里]({{< relref "doc/advanced/make.zh-cn.md" >}})
|
||||
|
||||
各种可用的 [make 任务](https://github.com/go-gitea/gitea/blob/main/Makefile)
|
||||
可以用来使编译过程更方便。
|
||||
@@ -104,4 +104,4 @@ CC=aarch64-unknown-linux-gnu-gcc GOOS=linux GOARCH=arm64 TAGS="bindata sqlite sq
|
||||
|
||||
## 需要帮助?
|
||||
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "seek-help.zh-cn.md" >}})
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "doc/help/seek-help.zh-cn.md" >}})
|
||||
|
||||
@@ -77,6 +77,8 @@ For example:
|
||||
pip install --index-url https://testuser:password123@gitea.example.com/api/packages/testuser/pypi/simple --no-deps test_package
|
||||
```
|
||||
|
||||
You can use `--extra-index-url` instead of `--index-url` but that makes you vulnerable to dependency confusion attacks because `pip` checks the official PyPi repository for the package before it checks the specified custom repository. Read the `pip` docs for more information.
|
||||
|
||||
## Supported commands
|
||||
|
||||
```
|
||||
|
||||
@@ -99,6 +99,13 @@ Admin operations:
|
||||
- `--password value`, `-p value`: New password. Required.
|
||||
- Examples:
|
||||
- `gitea admin user change-password --username myname --password asecurepassword`
|
||||
- `must-change-password`:
|
||||
- Args:
|
||||
- `[username...]`: Users that must change their passwords
|
||||
- Options:
|
||||
- `--all`, `-A`: Force a password change for all users
|
||||
- `--exclude username`, `-e username`: Exclude the given user. Can be set multiple times.
|
||||
- `--unset`: Revoke forced password change for the given users
|
||||
- `regenerate`
|
||||
- Options:
|
||||
- `hooks`: Regenerate Git Hooks for all repositories
|
||||
@@ -124,6 +131,7 @@ Admin operations:
|
||||
- `--secret`: Client Secret.
|
||||
- `--auto-discover-url`: OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider).
|
||||
- `--use-custom-urls`: Use custom URLs for GitLab/GitHub OAuth endpoints.
|
||||
- `--custom-tenant-id`: Use custom Tenant ID for OAuth endpoints.
|
||||
- `--custom-auth-url`: Use a custom Authorization URL (option for GitLab/GitHub).
|
||||
- `--custom-token-url`: Use a custom Token URL (option for GitLab/GitHub).
|
||||
- `--custom-profile-url`: Use a custom Profile URL (option for GitLab/GitHub).
|
||||
@@ -147,6 +155,7 @@ Admin operations:
|
||||
- `--secret`: Client Secret.
|
||||
- `--auto-discover-url`: OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider).
|
||||
- `--use-custom-urls`: Use custom URLs for GitLab/GitHub OAuth endpoints.
|
||||
- `--custom-tenant-id`: Use custom Tenant ID for OAuth endpoints.
|
||||
- `--custom-auth-url`: Use a custom Authorization URL (option for GitLab/GitHub).
|
||||
- `--custom-token-url`: Use a custom Token URL (option for GitLab/GitHub).
|
||||
- `--custom-profile-url`: Use a custom Profile URL (option for GitLab/GitHub).
|
||||
|
||||
@@ -70,4 +70,4 @@ Gitea的首要目标是创建一个极易安装,运行非常快速,安装和
|
||||
|
||||
## 需要帮助?
|
||||
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "seek-help.zh-cn.md" >}})
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "doc/help/seek-help.zh-cn.md" >}})
|
||||
|
||||
5
go.mod
5
go.mod
@@ -75,6 +75,8 @@ require (
|
||||
github.com/niklasfasching/go-org v1.6.5
|
||||
github.com/oliamb/cutter v0.2.2
|
||||
github.com/olivere/elastic/v7 v7.0.32
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
@@ -285,6 +287,7 @@ require (
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90 // indirect
|
||||
@@ -302,7 +305,7 @@ replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142
|
||||
|
||||
replace github.com/satori/go.uuid v1.2.0 => github.com/gofrs/uuid v4.2.0+incompatible
|
||||
|
||||
replace github.com/blevesearch/zapx/v15 v15.3.6 => github.com/zeripath/zapx/v15 v15.3.6-alignment-fix
|
||||
replace github.com/blevesearch/zapx/v15 v15.3.6 => github.com/zeripath/zapx/v15 v15.3.6-alignment-fix-2
|
||||
|
||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||
|
||||
|
||||
11
go.sum
11
go.sum
@@ -1174,6 +1174,10 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
@@ -1482,8 +1486,8 @@ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87/go.m
|
||||
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
|
||||
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/zeripath/zapx/v15 v15.3.6-alignment-fix h1:fKZ9OxEDoJKgM0KBXRbSb5IgKUEXis6C3zEIiMtzzQ0=
|
||||
github.com/zeripath/zapx/v15 v15.3.6-alignment-fix/go.mod h1:5DbhhDTGtuQSns1tS2aJxJLPc91boXCvjOMeCLD1saM=
|
||||
github.com/zeripath/zapx/v15 v15.3.6-alignment-fix-2 h1:IRB+69BV7fTT5ccw35ca7TCBe2b7dm5Q5y5tUMQmCvU=
|
||||
github.com/zeripath/zapx/v15 v15.3.6-alignment-fix-2/go.mod h1:5DbhhDTGtuQSns1tS2aJxJLPc91boXCvjOMeCLD1saM=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
|
||||
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
|
||||
@@ -1759,7 +1763,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
||||
@@ -157,7 +157,7 @@ func CreateRepoTransferNotification(doer, newOwner *user_model.User, repo *repo_
|
||||
}
|
||||
for i := range users {
|
||||
notify = append(notify, &Notification{
|
||||
UserID: users[i].ID,
|
||||
UserID: i,
|
||||
RepoID: repo.ID,
|
||||
Status: NotificationStatusUnread,
|
||||
UpdatedBy: doer.ID,
|
||||
|
||||
@@ -68,8 +68,16 @@ func (key *GPGKey) PaddedKeyID() string {
|
||||
if len(key.KeyID) > 15 {
|
||||
return key.KeyID
|
||||
}
|
||||
return PaddedKeyID(key.KeyID)
|
||||
}
|
||||
|
||||
// PaddedKeyID show KeyID padded to 16 characters
|
||||
func PaddedKeyID(keyID string) string {
|
||||
if len(keyID) > 15 {
|
||||
return keyID
|
||||
}
|
||||
zeros := "0000000000000000"
|
||||
return zeros[0:16-len(key.KeyID)] + key.KeyID
|
||||
return zeros[0:16-len(keyID)] + keyID
|
||||
}
|
||||
|
||||
// ListGPGKeys returns a list of public keys belongs to given user.
|
||||
|
||||
@@ -154,8 +154,7 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
|
||||
return DefaultAvatarLink()
|
||||
}
|
||||
|
||||
enableFederatedAvatarSetting, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar)
|
||||
enableFederatedAvatar := enableFederatedAvatarSetting.GetValueBool()
|
||||
enableFederatedAvatar := system_model.GetSettingBool(system_model.KeyPictureEnableFederatedAvatar)
|
||||
|
||||
var err error
|
||||
if enableFederatedAvatar && system_model.LibravatarService != nil {
|
||||
@@ -176,9 +175,7 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
|
||||
return urlStr
|
||||
}
|
||||
|
||||
disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
|
||||
|
||||
disableGravatar := disableGravatarSetting.GetValueBool()
|
||||
disableGravatar := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
|
||||
if !disableGravatar {
|
||||
// copy GravatarSourceURL, because we will modify its Path.
|
||||
avatarURLCopy := *system_model.GravatarSourceURL
|
||||
|
||||
@@ -24,8 +24,10 @@ type contextKey struct {
|
||||
}
|
||||
|
||||
// enginedContextKey is a context key. It is used with context.Value() to get the current Engined for the context
|
||||
var enginedContextKey = &contextKey{"engined"}
|
||||
var _ Engined = &Context{}
|
||||
var (
|
||||
enginedContextKey = &contextKey{"engined"}
|
||||
_ Engined = &Context{}
|
||||
)
|
||||
|
||||
// Context represents a db context
|
||||
type Context struct {
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@@ -19,6 +22,7 @@ const (
|
||||
type Paginator interface {
|
||||
GetSkipTake() (skip, take int)
|
||||
GetStartEnd() (start, end int)
|
||||
IsListAll() bool
|
||||
}
|
||||
|
||||
// GetPaginatedSession creates a paginated database session
|
||||
@@ -45,9 +49,12 @@ func SetEnginePagination(e Engine, p Paginator) Engine {
|
||||
// ListOptions options to paginate results
|
||||
type ListOptions struct {
|
||||
PageSize int
|
||||
Page int // start from 1
|
||||
Page int // start from 1
|
||||
ListAll bool // if true, then PageSize and Page will not be taken
|
||||
}
|
||||
|
||||
var _ Paginator = &ListOptions{}
|
||||
|
||||
// GetSkipTake returns the skip and take values
|
||||
func (opts *ListOptions) GetSkipTake() (skip, take int) {
|
||||
opts.SetDefaultValues()
|
||||
@@ -61,6 +68,11 @@ func (opts *ListOptions) GetStartEnd() (start, end int) {
|
||||
return start, end
|
||||
}
|
||||
|
||||
// IsListAll indicates PageSize and Page will be ignored
|
||||
func (opts *ListOptions) IsListAll() bool {
|
||||
return opts.ListAll
|
||||
}
|
||||
|
||||
// SetDefaultValues sets default values
|
||||
func (opts *ListOptions) SetDefaultValues() {
|
||||
if opts.PageSize <= 0 {
|
||||
@@ -80,6 +92,8 @@ type AbsoluteListOptions struct {
|
||||
take int
|
||||
}
|
||||
|
||||
var _ Paginator = &AbsoluteListOptions{}
|
||||
|
||||
// NewAbsoluteListOptions creates a list option with applied limits
|
||||
func NewAbsoluteListOptions(skip, take int) *AbsoluteListOptions {
|
||||
if skip < 0 {
|
||||
@@ -94,6 +108,11 @@ func NewAbsoluteListOptions(skip, take int) *AbsoluteListOptions {
|
||||
return &AbsoluteListOptions{skip, take}
|
||||
}
|
||||
|
||||
// IsListAll will always return false
|
||||
func (opts *AbsoluteListOptions) IsListAll() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetSkipTake returns the skip and take values
|
||||
func (opts *AbsoluteListOptions) GetSkipTake() (skip, take int) {
|
||||
return opts.skip, opts.take
|
||||
@@ -103,3 +122,32 @@ func (opts *AbsoluteListOptions) GetSkipTake() (skip, take int) {
|
||||
func (opts *AbsoluteListOptions) GetStartEnd() (start, end int) {
|
||||
return opts.skip, opts.skip + opts.take
|
||||
}
|
||||
|
||||
// FindOptions represents a find options
|
||||
type FindOptions interface {
|
||||
Paginator
|
||||
ToConds() builder.Cond
|
||||
}
|
||||
|
||||
// Find represents a common find function which accept an options interface
|
||||
func Find[T any](ctx context.Context, opts FindOptions, objects *[]T) error {
|
||||
sess := GetEngine(ctx).Where(opts.ToConds())
|
||||
if !opts.IsListAll() {
|
||||
sess.Limit(opts.GetSkipTake())
|
||||
}
|
||||
return sess.Find(&objects)
|
||||
}
|
||||
|
||||
// Count represents a common count function which accept an options interface
|
||||
func Count[T any](ctx context.Context, opts FindOptions, object T) (int64, error) {
|
||||
return GetEngine(ctx).Where(opts.ToConds()).Count(object)
|
||||
}
|
||||
|
||||
// FindAndCount represents a common findandcount function which accept an options interface
|
||||
func FindAndCount[T any](ctx context.Context, opts FindOptions, objects *[]T) (int64, error) {
|
||||
sess := GetEngine(ctx).Where(opts.ToConds())
|
||||
if !opts.IsListAll() {
|
||||
sess.Limit(opts.GetSkipTake())
|
||||
}
|
||||
return sess.FindAndCount(&objects)
|
||||
}
|
||||
|
||||
@@ -544,3 +544,16 @@
|
||||
repo_id: 51
|
||||
type: 2
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 80
|
||||
repo_id: 31
|
||||
type: 1
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 81
|
||||
repo_id: 31
|
||||
type: 3
|
||||
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
|
||||
created_unix: 946684810
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
fork_id: 0
|
||||
is_template: false
|
||||
template_id: 0
|
||||
size: 0
|
||||
size: 6708
|
||||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
||||
|
||||
|
||||
@@ -140,3 +140,14 @@
|
||||
num_members: 1
|
||||
includes_all_repositories: false
|
||||
can_create_org_repo: false
|
||||
|
||||
-
|
||||
id: 14
|
||||
org_id: 3
|
||||
lower_name: teamcreaterepo
|
||||
name: teamCreateRepo
|
||||
authorize: 2 # write
|
||||
num_repos: 0
|
||||
num_members: 1
|
||||
includes_all_repositories: false
|
||||
can_create_org_repo: true
|
||||
|
||||
@@ -93,3 +93,9 @@
|
||||
org_id: 19
|
||||
team_id: 6
|
||||
uid: 31
|
||||
|
||||
-
|
||||
id: 17
|
||||
org_id: 3
|
||||
team_id: 14
|
||||
uid: 2
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
email: user1@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user1
|
||||
@@ -45,8 +45,8 @@
|
||||
email: user2@example.com
|
||||
keep_email_private: true
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user2
|
||||
@@ -82,8 +82,8 @@
|
||||
email: user3@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: onmention
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user3
|
||||
@@ -104,7 +104,7 @@
|
||||
num_following: 0
|
||||
num_stars: 0
|
||||
num_repos: 3
|
||||
num_teams: 4
|
||||
num_teams: 5
|
||||
num_members: 3
|
||||
visibility: 0
|
||||
repo_admin_change_team_access: false
|
||||
@@ -119,8 +119,8 @@
|
||||
email: user4@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: onmention
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user4
|
||||
@@ -156,8 +156,8 @@
|
||||
email: user5@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user5
|
||||
@@ -193,8 +193,8 @@
|
||||
email: user6@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user6
|
||||
@@ -230,8 +230,8 @@
|
||||
email: user7@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: disabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user7
|
||||
@@ -267,8 +267,8 @@
|
||||
email: user8@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user8
|
||||
@@ -304,8 +304,8 @@
|
||||
email: user9@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: onmention
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user9
|
||||
@@ -341,8 +341,8 @@
|
||||
email: user10@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user10
|
||||
@@ -378,8 +378,8 @@
|
||||
email: user11@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user11
|
||||
@@ -415,8 +415,8 @@
|
||||
email: user12@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user12
|
||||
@@ -452,8 +452,8 @@
|
||||
email: user13@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user13
|
||||
@@ -489,8 +489,8 @@
|
||||
email: user14@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user14
|
||||
@@ -526,8 +526,8 @@
|
||||
email: user15@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user15
|
||||
@@ -563,8 +563,8 @@
|
||||
email: user16@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user16
|
||||
@@ -600,8 +600,8 @@
|
||||
email: user17@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user17
|
||||
@@ -637,8 +637,8 @@
|
||||
email: user18@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user18
|
||||
@@ -674,8 +674,8 @@
|
||||
email: user19@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user19
|
||||
@@ -711,8 +711,8 @@
|
||||
email: user20@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user20
|
||||
@@ -748,8 +748,8 @@
|
||||
email: user21@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user21
|
||||
@@ -785,8 +785,8 @@
|
||||
email: limited_org@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: limited_org
|
||||
@@ -822,8 +822,8 @@
|
||||
email: privated_org@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: privated_org
|
||||
@@ -859,8 +859,8 @@
|
||||
email: user24@example.com
|
||||
keep_email_private: true
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user24
|
||||
@@ -896,8 +896,8 @@
|
||||
email: org25@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: org25
|
||||
@@ -933,8 +933,8 @@
|
||||
email: org26@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: onmention
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: org26
|
||||
@@ -970,8 +970,8 @@
|
||||
email: user27@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user27
|
||||
@@ -1007,8 +1007,8 @@
|
||||
email: user28@example.com
|
||||
keep_email_private: true
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user28
|
||||
@@ -1044,8 +1044,8 @@
|
||||
email: user29@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user29
|
||||
@@ -1081,8 +1081,8 @@
|
||||
email: user30@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user30
|
||||
@@ -1118,8 +1118,8 @@
|
||||
email: user31@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user31
|
||||
@@ -1155,7 +1155,7 @@
|
||||
email: user32@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a
|
||||
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f47017
|
||||
passwd_hash_algo: argon2
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
@@ -1192,8 +1192,8 @@
|
||||
email: user33@example.com
|
||||
keep_email_private: false
|
||||
email_notifications_preference: enabled
|
||||
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
|
||||
passwd_hash_algo: argon2
|
||||
passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889
|
||||
passwd_hash_algo: pbkdf2$50000$50
|
||||
must_change_password: false
|
||||
login_source: 0
|
||||
login_name: user33
|
||||
|
||||
@@ -9,9 +9,7 @@ package issues
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -23,8 +21,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@@ -697,31 +693,6 @@ func (c *Comment) LoadReview() error {
|
||||
return c.loadReview(db.DefaultContext)
|
||||
}
|
||||
|
||||
var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`)
|
||||
|
||||
func (c *Comment) checkInvalidation(doer *user_model.User, repo *git.Repository, branch string) error {
|
||||
// FIXME differentiate between previous and proposed line
|
||||
commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine()))
|
||||
if err != nil && (strings.Contains(err.Error(), "fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
|
||||
c.Invalidated = true
|
||||
return UpdateComment(c, doer)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() {
|
||||
c.Invalidated = true
|
||||
return UpdateComment(c, doer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckInvalidation checks if the line of code comment got changed by another commit.
|
||||
// If the line got changed the comment is going to be invalidated.
|
||||
func (c *Comment) CheckInvalidation(repo *git.Repository, doer *user_model.User, branch string) error {
|
||||
return c.checkInvalidation(doer, repo, branch)
|
||||
}
|
||||
|
||||
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
|
||||
func (c *Comment) DiffSide() string {
|
||||
if c.Line < 0 {
|
||||
@@ -1065,23 +1036,28 @@ func GetCommentByID(ctx context.Context, id int64) (*Comment, error) {
|
||||
// FindCommentsOptions describes the conditions to Find comments
|
||||
type FindCommentsOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
IssueID int64
|
||||
ReviewID int64
|
||||
Since int64
|
||||
Before int64
|
||||
Line int64
|
||||
TreePath string
|
||||
Type CommentType
|
||||
RepoID int64
|
||||
IssueID int64
|
||||
ReviewID int64
|
||||
Since int64
|
||||
Before int64
|
||||
Line int64
|
||||
TreePath string
|
||||
Type CommentType
|
||||
IssueIDs []int64
|
||||
Invalidated util.OptionalBool
|
||||
}
|
||||
|
||||
func (opts *FindCommentsOptions) toConds() builder.Cond {
|
||||
// ToConds implements FindOptions interface
|
||||
func (opts *FindCommentsOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.IssueID > 0 {
|
||||
cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
|
||||
} else if len(opts.IssueIDs) > 0 {
|
||||
cond = cond.And(builder.In("comment.issue_id", opts.IssueIDs))
|
||||
}
|
||||
if opts.ReviewID > 0 {
|
||||
cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID})
|
||||
@@ -1101,13 +1077,16 @@ func (opts *FindCommentsOptions) toConds() builder.Cond {
|
||||
if len(opts.TreePath) > 0 {
|
||||
cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath})
|
||||
}
|
||||
if !opts.Invalidated.IsNone() {
|
||||
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// FindComments returns all comments according options
|
||||
func FindComments(ctx context.Context, opts *FindCommentsOptions) ([]*Comment, error) {
|
||||
comments := make([]*Comment, 0, 10)
|
||||
sess := db.GetEngine(ctx).Where(opts.toConds())
|
||||
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
||||
if opts.RepoID > 0 {
|
||||
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
||||
}
|
||||
@@ -1126,13 +1105,19 @@ func FindComments(ctx context.Context, opts *FindCommentsOptions) ([]*Comment, e
|
||||
|
||||
// CountComments count all comments according options by ignoring pagination
|
||||
func CountComments(opts *FindCommentsOptions) (int64, error) {
|
||||
sess := db.GetEngine(db.DefaultContext).Where(opts.toConds())
|
||||
sess := db.GetEngine(db.DefaultContext).Where(opts.ToConds())
|
||||
if opts.RepoID > 0 {
|
||||
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
||||
}
|
||||
return sess.Count(&Comment{})
|
||||
}
|
||||
|
||||
// UpdateCommentInvalidate updates comment invalidated column
|
||||
func UpdateCommentInvalidate(ctx context.Context, c *Comment) error {
|
||||
_, err := db.GetEngine(ctx).ID(c.ID).Cols("invalidated").Update(c)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateComment updates information of comment.
|
||||
func UpdateComment(c *Comment, doer *user_model.User) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
@@ -1191,120 +1176,6 @@ func DeleteComment(ctx context.Context, comment *Comment) error {
|
||||
return DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID})
|
||||
}
|
||||
|
||||
// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
|
||||
type CodeComments map[string]map[int64][]*Comment
|
||||
|
||||
// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
|
||||
func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User) (CodeComments, error) {
|
||||
return fetchCodeCommentsByReview(ctx, issue, currentUser, nil)
|
||||
}
|
||||
|
||||
func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review) (CodeComments, error) {
|
||||
pathToLineToComment := make(CodeComments)
|
||||
if review == nil {
|
||||
review = &Review{ID: 0}
|
||||
}
|
||||
opts := FindCommentsOptions{
|
||||
Type: CommentTypeCode,
|
||||
IssueID: issue.ID,
|
||||
ReviewID: review.ID,
|
||||
}
|
||||
|
||||
comments, err := findCodeComments(ctx, opts, issue, currentUser, review)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if pathToLineToComment[comment.TreePath] == nil {
|
||||
pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
|
||||
}
|
||||
pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
|
||||
}
|
||||
return pathToLineToComment, nil
|
||||
}
|
||||
|
||||
func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review) ([]*Comment, error) {
|
||||
var comments []*Comment
|
||||
if review == nil {
|
||||
review = &Review{ID: 0}
|
||||
}
|
||||
conds := opts.toConds()
|
||||
if review.ID == 0 {
|
||||
conds = conds.And(builder.Eq{"invalidated": false})
|
||||
}
|
||||
e := db.GetEngine(ctx)
|
||||
if err := e.Where(conds).
|
||||
Asc("comment.created_unix").
|
||||
Asc("comment.id").
|
||||
Find(&comments); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := CommentList(comments).loadPosters(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find all reviews by ReviewID
|
||||
reviews := make(map[int64]*Review)
|
||||
ids := make([]int64, 0, len(comments))
|
||||
for _, comment := range comments {
|
||||
if comment.ReviewID != 0 {
|
||||
ids = append(ids, comment.ReviewID)
|
||||
}
|
||||
}
|
||||
if err := e.In("id", ids).Find(&reviews); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := 0
|
||||
for _, comment := range comments {
|
||||
if re, ok := reviews[comment.ReviewID]; ok && re != nil {
|
||||
// If the review is pending only the author can see the comments (except if the review is set)
|
||||
if review.ID == 0 && re.Type == ReviewTypePending &&
|
||||
(currentUser == nil || currentUser.ID != re.ReviewerID) {
|
||||
continue
|
||||
}
|
||||
comment.Review = re
|
||||
}
|
||||
comments[n] = comment
|
||||
n++
|
||||
|
||||
if err := comment.LoadResolveDoer(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := comment.LoadReactions(issue.Repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: issue.Repo.Link(),
|
||||
Metas: issue.Repo.ComposeMetas(),
|
||||
}, comment.Content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return comments[:n], nil
|
||||
}
|
||||
|
||||
// FetchCodeCommentsByLine fetches the code comments for a given treePath and line number
|
||||
func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64) ([]*Comment, error) {
|
||||
opts := FindCommentsOptions{
|
||||
Type: CommentTypeCode,
|
||||
IssueID: issue.ID,
|
||||
TreePath: treePath,
|
||||
Line: line,
|
||||
}
|
||||
return findCodeComments(ctx, opts, issue, currentUser, nil)
|
||||
}
|
||||
|
||||
// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
|
||||
func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
|
||||
_, err := db.GetEngine(db.DefaultContext).Table("comment").
|
||||
@@ -1549,3 +1420,8 @@ func FixCommentTypeLabelWithOutsideLabels() (int64, error) {
|
||||
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// HasOriginalAuthor returns if a comment was migrated and has an original author.
|
||||
func (c *Comment) HasOriginalAuthor() bool {
|
||||
return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
|
||||
}
|
||||
|
||||
129
models/issues/comment_code.go
Normal file
129
models/issues/comment_code.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
|
||||
type CodeComments map[string]map[int64][]*Comment
|
||||
|
||||
// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
|
||||
func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User) (CodeComments, error) {
|
||||
return fetchCodeCommentsByReview(ctx, issue, currentUser, nil)
|
||||
}
|
||||
|
||||
func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review) (CodeComments, error) {
|
||||
pathToLineToComment := make(CodeComments)
|
||||
if review == nil {
|
||||
review = &Review{ID: 0}
|
||||
}
|
||||
opts := FindCommentsOptions{
|
||||
Type: CommentTypeCode,
|
||||
IssueID: issue.ID,
|
||||
ReviewID: review.ID,
|
||||
}
|
||||
|
||||
comments, err := findCodeComments(ctx, opts, issue, currentUser, review)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if pathToLineToComment[comment.TreePath] == nil {
|
||||
pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
|
||||
}
|
||||
pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
|
||||
}
|
||||
return pathToLineToComment, nil
|
||||
}
|
||||
|
||||
func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review) ([]*Comment, error) {
|
||||
var comments []*Comment
|
||||
if review == nil {
|
||||
review = &Review{ID: 0}
|
||||
}
|
||||
conds := opts.ToConds()
|
||||
if review.ID == 0 {
|
||||
conds = conds.And(builder.Eq{"invalidated": false})
|
||||
}
|
||||
e := db.GetEngine(ctx)
|
||||
if err := e.Where(conds).
|
||||
Asc("comment.created_unix").
|
||||
Asc("comment.id").
|
||||
Find(&comments); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := CommentList(comments).loadPosters(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find all reviews by ReviewID
|
||||
reviews := make(map[int64]*Review)
|
||||
ids := make([]int64, 0, len(comments))
|
||||
for _, comment := range comments {
|
||||
if comment.ReviewID != 0 {
|
||||
ids = append(ids, comment.ReviewID)
|
||||
}
|
||||
}
|
||||
if err := e.In("id", ids).Find(&reviews); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := 0
|
||||
for _, comment := range comments {
|
||||
if re, ok := reviews[comment.ReviewID]; ok && re != nil {
|
||||
// If the review is pending only the author can see the comments (except if the review is set)
|
||||
if review.ID == 0 && re.Type == ReviewTypePending &&
|
||||
(currentUser == nil || currentUser.ID != re.ReviewerID) {
|
||||
continue
|
||||
}
|
||||
comment.Review = re
|
||||
}
|
||||
comments[n] = comment
|
||||
n++
|
||||
|
||||
if err := comment.LoadResolveDoer(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := comment.LoadReactions(issue.Repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: issue.Repo.Link(),
|
||||
Metas: issue.Repo.ComposeMetas(),
|
||||
}, comment.Content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return comments[:n], nil
|
||||
}
|
||||
|
||||
// FetchCodeCommentsByLine fetches the code comments for a given treePath and line number
|
||||
func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64) ([]*Comment, error) {
|
||||
opts := FindCommentsOptions{
|
||||
Type: CommentTypeCode,
|
||||
IssueID: issue.ID,
|
||||
TreePath: treePath,
|
||||
Line: line,
|
||||
}
|
||||
return findCodeComments(ctx, opts, issue, currentUser, nil)
|
||||
}
|
||||
@@ -2466,3 +2466,8 @@ func DeleteOrphanedIssues() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasOriginalAuthor returns if an issue was migrated and has an original author.
|
||||
func (issue *Issue) HasOriginalAuthor() bool {
|
||||
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
|
||||
}
|
||||
|
||||
@@ -755,7 +755,7 @@ func CountOrphanedLabels() (int64, error) {
|
||||
norepo, err := db.GetEngine(db.DefaultContext).Table("label").
|
||||
Where(builder.And(
|
||||
builder.Gt{"repo_id": 0},
|
||||
builder.NotIn("repo_id", builder.Select("id").From("repository")),
|
||||
builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
|
||||
)).
|
||||
Count()
|
||||
if err != nil {
|
||||
@@ -765,7 +765,7 @@ func CountOrphanedLabels() (int64, error) {
|
||||
noorg, err := db.GetEngine(db.DefaultContext).Table("label").
|
||||
Where(builder.And(
|
||||
builder.Gt{"org_id": 0},
|
||||
builder.NotIn("org_id", builder.Select("id").From("user")),
|
||||
builder.NotIn("org_id", builder.Select("id").From("`user`")),
|
||||
)).
|
||||
Count()
|
||||
if err != nil {
|
||||
@@ -786,7 +786,7 @@ func DeleteOrphanedLabels() error {
|
||||
if _, err := db.GetEngine(db.DefaultContext).
|
||||
Where(builder.And(
|
||||
builder.Gt{"repo_id": 0},
|
||||
builder.NotIn("repo_id", builder.Select("id").From("repository")),
|
||||
builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
|
||||
)).
|
||||
Delete(Label{}); err != nil {
|
||||
return err
|
||||
@@ -796,7 +796,7 @@ func DeleteOrphanedLabels() error {
|
||||
if _, err := db.GetEngine(db.DefaultContext).
|
||||
Where(builder.And(
|
||||
builder.Gt{"org_id": 0},
|
||||
builder.NotIn("org_id", builder.Select("id").From("user")),
|
||||
builder.NotIn("org_id", builder.Select("id").From("`user`")),
|
||||
)).
|
||||
Delete(Label{}); err != nil {
|
||||
return err
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -133,6 +134,27 @@ const (
|
||||
PullRequestStatusAncestor
|
||||
)
|
||||
|
||||
func (status PullRequestStatus) String() string {
|
||||
switch status {
|
||||
case PullRequestStatusConflict:
|
||||
return "CONFLICT"
|
||||
case PullRequestStatusChecking:
|
||||
return "CHECKING"
|
||||
case PullRequestStatusMergeable:
|
||||
return "MERGEABLE"
|
||||
case PullRequestStatusManuallyMerged:
|
||||
return "MANUALLY_MERGED"
|
||||
case PullRequestStatusError:
|
||||
return "ERROR"
|
||||
case PullRequestStatusEmpty:
|
||||
return "EMPTY"
|
||||
case PullRequestStatusAncestor:
|
||||
return "ANCESTOR"
|
||||
default:
|
||||
return strconv.Itoa(int(status))
|
||||
}
|
||||
}
|
||||
|
||||
// PullRequestFlow the flow of pull request
|
||||
type PullRequestFlow int
|
||||
|
||||
@@ -204,6 +226,42 @@ func DeletePullsByBaseRepoID(ctx context.Context, repoID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ColorFormat writes a colored string to identify this struct
|
||||
func (pr *PullRequest) ColorFormat(s fmt.State) {
|
||||
if pr == nil {
|
||||
log.ColorFprintf(s, "PR[%d]%s#%d[%s...%s:%s]",
|
||||
log.NewColoredIDValue(0),
|
||||
log.NewColoredValue("<nil>/<nil>"),
|
||||
log.NewColoredIDValue(0),
|
||||
log.NewColoredValue("<nil>"),
|
||||
log.NewColoredValue("<nil>/<nil>"),
|
||||
log.NewColoredValue("<nil>"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
log.ColorFprintf(s, "PR[%d]", log.NewColoredIDValue(pr.ID))
|
||||
if pr.BaseRepo != nil {
|
||||
log.ColorFprintf(s, "%s#%d[%s...", log.NewColoredValue(pr.BaseRepo.FullName()),
|
||||
log.NewColoredIDValue(pr.Index), log.NewColoredValue(pr.BaseBranch))
|
||||
} else {
|
||||
log.ColorFprintf(s, "Repo[%d]#%d[%s...", log.NewColoredIDValue(pr.BaseRepoID),
|
||||
log.NewColoredIDValue(pr.Index), log.NewColoredValue(pr.BaseBranch))
|
||||
}
|
||||
if pr.HeadRepoID == pr.BaseRepoID {
|
||||
log.ColorFprintf(s, "%s]", log.NewColoredValue(pr.HeadBranch))
|
||||
} else if pr.HeadRepo != nil {
|
||||
log.ColorFprintf(s, "%s:%s]", log.NewColoredValue(pr.HeadRepo.FullName()), log.NewColoredValue(pr.HeadBranch))
|
||||
} else {
|
||||
log.ColorFprintf(s, "Repo[%d]:%s]", log.NewColoredIDValue(pr.HeadRepoID), log.NewColoredValue(pr.HeadBranch))
|
||||
}
|
||||
}
|
||||
|
||||
// String represents the pr as a simple string
|
||||
func (pr *PullRequest) String() string {
|
||||
return log.ColorFormatAsString(pr)
|
||||
}
|
||||
|
||||
// MustHeadUserName returns the HeadRepo's username if failed return blank
|
||||
func (pr *PullRequest) MustHeadUserName() string {
|
||||
if err := pr.LoadHeadRepo(); err != nil {
|
||||
@@ -255,7 +313,7 @@ func (pr *PullRequest) LoadHeadRepoCtx(ctx context.Context) (err error) {
|
||||
|
||||
pr.HeadRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.HeadRepoID)
|
||||
if err != nil && !repo_model.IsErrRepoNotExist(err) { // Head repo maybe deleted, but it should still work
|
||||
return fmt.Errorf("getRepositoryByID(head): %w", err)
|
||||
return fmt.Errorf("pr[%d].LoadHeadRepo[%d]: %w", pr.ID, pr.HeadRepoID, err)
|
||||
}
|
||||
pr.isHeadRepoLoaded = true
|
||||
}
|
||||
@@ -290,7 +348,7 @@ func (pr *PullRequest) LoadBaseRepoCtx(ctx context.Context) (err error) {
|
||||
|
||||
pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("repo_model.GetRepositoryByID(base): %w", err)
|
||||
return fmt.Errorf("pr[%d].LoadBaseRepo[%d]: %w", pr.ID, pr.BaseRepoID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"xorm.io/xorm"
|
||||
@@ -53,13 +52,16 @@ func listPullRequestStatement(baseRepoID int64, opts *PullRequestsOptions) (*xor
|
||||
|
||||
// GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged
|
||||
// by given head information (repo and branch).
|
||||
func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string) ([]*PullRequest, error) {
|
||||
// arg `includeClosed` controls whether the SQL returns closed PRs
|
||||
func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string, includeClosed bool) ([]*PullRequest, error) {
|
||||
prs := make([]*PullRequest, 0, 2)
|
||||
return prs, db.GetEngine(db.DefaultContext).
|
||||
Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?",
|
||||
repoID, branch, false, false, PullRequestFlowGithub).
|
||||
sess := db.GetEngine(db.DefaultContext).
|
||||
Join("INNER", "issue", "issue.id = pull_request.issue_id").
|
||||
Find(&prs)
|
||||
Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND flow = ?", repoID, branch, false, PullRequestFlowGithub)
|
||||
if !includeClosed {
|
||||
sess.Where("issue.is_closed = ?", false)
|
||||
}
|
||||
return prs, sess.Find(&prs)
|
||||
}
|
||||
|
||||
// CanMaintainerWriteToBranch check whether user is a maintainer and could write to the branch
|
||||
@@ -72,7 +74,7 @@ func CanMaintainerWriteToBranch(p access_model.Permission, branch string, user *
|
||||
return false
|
||||
}
|
||||
|
||||
prs, err := GetUnmergedPullRequestsByHeadInfo(p.Units[0].RepoID, branch)
|
||||
prs, err := GetUnmergedPullRequestsByHeadInfo(p.Units[0].RepoID, branch, false)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -162,7 +164,7 @@ func (prs PullRequestList) loadAttributes(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Load issues.
|
||||
issueIDs := prs.getIssueIDs()
|
||||
issueIDs := prs.GetIssueIDs()
|
||||
issues := make([]*Issue, 0, len(issueIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("id > 0").
|
||||
@@ -181,7 +183,8 @@ func (prs PullRequestList) loadAttributes(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (prs PullRequestList) getIssueIDs() []int64 {
|
||||
// GetIssueIDs returns all issue ids
|
||||
func (prs PullRequestList) GetIssueIDs() []int64 {
|
||||
issueIDs := make([]int64, 0, len(prs))
|
||||
for i := range prs {
|
||||
issueIDs = append(issueIDs, prs[i].IssueID)
|
||||
@@ -193,24 +196,3 @@ func (prs PullRequestList) getIssueIDs() []int64 {
|
||||
func (prs PullRequestList) LoadAttributes() error {
|
||||
return prs.loadAttributes(db.DefaultContext)
|
||||
}
|
||||
|
||||
// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change
|
||||
func (prs PullRequestList) InvalidateCodeComments(ctx context.Context, doer *user_model.User, repo *git.Repository, branch string) error {
|
||||
if len(prs) == 0 {
|
||||
return nil
|
||||
}
|
||||
issueIDs := prs.getIssueIDs()
|
||||
var codeComments []*Comment
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("type = ? and invalidated = ?", CommentTypeCode, false).
|
||||
In("issue_id", issueIDs).
|
||||
Find(&codeComments); err != nil {
|
||||
return fmt.Errorf("find code comments: %w", err)
|
||||
}
|
||||
for _, comment := range codeComments {
|
||||
if err := comment.CheckInvalidation(repo, doer, branch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
|
||||
func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(1, "branch2")
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(1, "branch2", false)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, prs, 1)
|
||||
for _, pr := range prs {
|
||||
|
||||
@@ -742,17 +742,9 @@ func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Commen
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if official {
|
||||
// recalculate the latest official review for reviewer
|
||||
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
|
||||
if err != nil && !IsErrReviewNotExist(err) {
|
||||
if err := restoreLatestOfficialReview(ctx, issue.ID, reviewer.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if review != nil {
|
||||
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{
|
||||
@@ -770,6 +762,22 @@ func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Commen
|
||||
return comment, committer.Commit()
|
||||
}
|
||||
|
||||
// Recalculate the latest official review for reviewer
|
||||
func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64) error {
|
||||
review, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID)
|
||||
if err != nil && !IsErrReviewNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if review != nil {
|
||||
if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTeamReviewRequest add a review request from one team
|
||||
func AddTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) {
|
||||
ctx, committer, err := db.TxContext()
|
||||
@@ -970,7 +978,7 @@ func DeleteReview(r *Review) error {
|
||||
ReviewID: r.ID,
|
||||
}
|
||||
|
||||
if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
|
||||
if _, err := sess.Where(opts.ToConds()).Delete(new(Comment)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -980,7 +988,7 @@ func DeleteReview(r *Review) error {
|
||||
ReviewID: r.ID,
|
||||
}
|
||||
|
||||
if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil {
|
||||
if _, err := sess.Where(opts.ToConds()).Delete(new(Comment)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -988,6 +996,12 @@ func DeleteReview(r *Review) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Official {
|
||||
if err := restoreLatestOfficialReview(ctx, r.IssueID, r.ReviewerID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
@@ -998,7 +1012,7 @@ func (r *Review) GetCodeCommentsCount() int {
|
||||
IssueID: r.IssueID,
|
||||
ReviewID: r.ID,
|
||||
}
|
||||
conds := opts.toConds()
|
||||
conds := opts.ToConds()
|
||||
if r.ID == 0 {
|
||||
conds = conds.And(builder.Eq{"invalidated": false})
|
||||
}
|
||||
@@ -1018,7 +1032,7 @@ func (r *Review) HTMLURL() string {
|
||||
ReviewID: r.ID,
|
||||
}
|
||||
comment := new(Comment)
|
||||
has, err := db.GetEngine(db.DefaultContext).Where(opts.toConds()).Get(comment)
|
||||
has, err := db.GetEngine(db.DefaultContext).Where(opts.ToConds()).Get(comment)
|
||||
if err != nil || !has {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -201,3 +201,38 @@ func TestDismissReview(t *testing.T) {
|
||||
assert.False(t, requestReviewExample.Dismissed)
|
||||
assert.True(t, approveReviewExample.Dismissed)
|
||||
}
|
||||
|
||||
func TestDeleteReview(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
review1, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
|
||||
Content: "Official rejection",
|
||||
Type: issues_model.ReviewTypeReject,
|
||||
Official: false,
|
||||
Issue: issue,
|
||||
Reviewer: user,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
review2, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
|
||||
Content: "Official approval",
|
||||
Type: issues_model.ReviewTypeApprove,
|
||||
Official: true,
|
||||
Issue: issue,
|
||||
Reviewer: user,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, issues_model.DeleteReview(review2))
|
||||
|
||||
_, err = issues_model.GetReviewByID(db.DefaultContext, review2.ID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist")
|
||||
|
||||
review1, err = issues_model.GetReviewByID(db.DefaultContext, review1.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, review1.Official)
|
||||
}
|
||||
|
||||
@@ -396,13 +396,14 @@ func (org *Organization) GetOrgUserMaxAuthorizeLevel(uid int64) (perm.AccessMode
|
||||
}
|
||||
|
||||
// GetUsersWhoCanCreateOrgRepo returns users which are able to create repo in organization
|
||||
func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) ([]*user_model.User, error) {
|
||||
users := make([]*user_model.User, 0, 10)
|
||||
func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*user_model.User, error) {
|
||||
// Use a map, in order to de-duplicate users.
|
||||
users := make(map[int64]*user_model.User)
|
||||
return users, db.GetEngine(ctx).
|
||||
Join("INNER", "`team_user`", "`team_user`.uid=`user`.id").
|
||||
Join("INNER", "`team`", "`team`.id=`team_user`.team_id").
|
||||
Where(builder.Eq{"team.can_create_org_repo": true}.Or(builder.Eq{"team.authorize": perm.AccessModeOwner})).
|
||||
And("team_user.org_id = ?", orgID).Asc("`user`.name").Find(&users)
|
||||
And("team_user.org_id = ?", orgID).Find(&users)
|
||||
}
|
||||
|
||||
// SearchOrganizationsOptions options to filter organizations
|
||||
|
||||
@@ -92,11 +92,12 @@ func TestUser_GetTeams(t *testing.T) {
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
teams, err := org.LoadTeams()
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, teams, 4) {
|
||||
if assert.Len(t, teams, 5) {
|
||||
assert.Equal(t, int64(1), teams[0].ID)
|
||||
assert.Equal(t, int64(2), teams[1].ID)
|
||||
assert.Equal(t, int64(12), teams[2].ID)
|
||||
assert.Equal(t, int64(7), teams[3].ID)
|
||||
assert.Equal(t, int64(14), teams[3].ID)
|
||||
assert.Equal(t, int64(7), teams[4].ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +294,7 @@ func TestUser_GetUserTeamIDs(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, teamIDs)
|
||||
}
|
||||
testSuccess(2, []int64{1, 2})
|
||||
testSuccess(2, []int64{1, 2, 14})
|
||||
testSuccess(4, []int64{2})
|
||||
testSuccess(unittest.NonexistentID, []int64{})
|
||||
}
|
||||
@@ -448,7 +449,7 @@ func TestGetUsersWhoCanCreateOrgRepo(t *testing.T) {
|
||||
users, err = organization.GetUsersWhoCanCreateOrgRepo(db.DefaultContext, 7)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, users, 1)
|
||||
assert.EqualValues(t, 5, users[0].ID)
|
||||
assert.NotNil(t, users[5])
|
||||
}
|
||||
|
||||
func TestUser_RemoveOrgRepo(t *testing.T) {
|
||||
|
||||
@@ -26,6 +26,7 @@ type BlobSearchOptions struct {
|
||||
Digest string
|
||||
Tag string
|
||||
IsManifest bool
|
||||
Repository string
|
||||
}
|
||||
|
||||
func (opts *BlobSearchOptions) toConds() builder.Cond {
|
||||
@@ -54,6 +55,15 @@ func (opts *BlobSearchOptions) toConds() builder.Cond {
|
||||
|
||||
cond = cond.And(builder.In("package_file.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property")))
|
||||
}
|
||||
if opts.Repository != "" {
|
||||
var propsCond builder.Cond = builder.Eq{
|
||||
"package_property.ref_type": packages.PropertyTypePackage,
|
||||
"package_property.name": container_module.PropertyRepository,
|
||||
"package_property.value": opts.Repository,
|
||||
}
|
||||
|
||||
cond = cond.And(builder.In("package.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property")))
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"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/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
@@ -498,8 +499,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
|
||||
// Only show a repo that either has a topic or description.
|
||||
subQueryCond := builder.NewCond()
|
||||
|
||||
// Topic checking. Topics is non-null.
|
||||
subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"}))
|
||||
// Topic checking. Topics are present.
|
||||
if setting.Database.UsePostgreSQL { // postgres stores the topics as json and not as text
|
||||
subQueryCond = subQueryCond.Or(builder.And(builder.NotNull{"topics"}, builder.Neq{"(topics)::text": "[]"}))
|
||||
} else {
|
||||
subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"}))
|
||||
}
|
||||
|
||||
// Description checking. Description not empty.
|
||||
subQueryCond = subQueryCond.Or(builder.Neq{"description": ""})
|
||||
|
||||
@@ -185,7 +185,7 @@ func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName s
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize
|
||||
// UpdateRepoSize updates the repository size, calculating it using getDirectorySize
|
||||
func UpdateRepoSize(ctx context.Context, repoID, size int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(repoID).Cols("size").NoAutoTime().Update(&Repository{
|
||||
Size: size,
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
setting_module "code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"strk.kbt.io/projects/go/libravatar"
|
||||
@@ -89,17 +89,17 @@ func GetSettingNoCache(key string) (*Setting, error) {
|
||||
if len(v) == 0 {
|
||||
return nil, ErrSettingIsNotExist{key}
|
||||
}
|
||||
return v[key], nil
|
||||
return v[strings.ToLower(key)], nil
|
||||
}
|
||||
|
||||
// GetSetting returns the setting value via the key
|
||||
func GetSetting(key string) (*Setting, error) {
|
||||
return cache.Get(genSettingCacheKey(key), func() (*Setting, error) {
|
||||
func GetSetting(key string) (string, error) {
|
||||
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
|
||||
res, err := GetSettingNoCache(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
return res.SettingValue, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,7 +107,8 @@ func GetSetting(key string) (*Setting, error) {
|
||||
// none existing keys and errors are ignored and result in false
|
||||
func GetSettingBool(key string) bool {
|
||||
s, _ := GetSetting(key)
|
||||
return s.GetValueBool()
|
||||
v, _ := strconv.ParseBool(s)
|
||||
return v
|
||||
}
|
||||
|
||||
// GetSettings returns specific settings
|
||||
@@ -131,7 +132,7 @@ func GetSettings(keys []string) (map[string]*Setting, error) {
|
||||
type AllSettings map[string]*Setting
|
||||
|
||||
func (settings AllSettings) Get(key string) Setting {
|
||||
if v, ok := settings[key]; ok {
|
||||
if v, ok := settings[strings.ToLower(key)]; ok {
|
||||
return *v
|
||||
}
|
||||
return Setting{}
|
||||
@@ -184,14 +185,17 @@ func SetSettingNoVersion(key, value string) error {
|
||||
|
||||
// SetSetting updates a users' setting for a specific key
|
||||
func SetSetting(setting *Setting) error {
|
||||
_, err := cache.Set(genSettingCacheKey(setting.SettingKey), func() (*Setting, error) {
|
||||
return setting, upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version)
|
||||
})
|
||||
if err != nil {
|
||||
if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
setting.Version++
|
||||
|
||||
cc := cache.GetCache()
|
||||
if cc != nil {
|
||||
return cc.Put(genSettingCacheKey(setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -243,7 +247,7 @@ func Init() error {
|
||||
var disableGravatar bool
|
||||
disableGravatarSetting, err := GetSettingNoCache(KeyPictureDisableGravatar)
|
||||
if IsErrSettingIsNotExist(err) {
|
||||
disableGravatar = setting.GetDefaultDisableGravatar()
|
||||
disableGravatar = setting_module.GetDefaultDisableGravatar()
|
||||
disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)}
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -254,7 +258,7 @@ func Init() error {
|
||||
var enableFederatedAvatar bool
|
||||
enableFederatedAvatarSetting, err := GetSettingNoCache(KeyPictureEnableFederatedAvatar)
|
||||
if IsErrSettingIsNotExist(err) {
|
||||
enableFederatedAvatar = setting.GetDefaultEnableFederatedAvatar(disableGravatar)
|
||||
enableFederatedAvatar = setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)
|
||||
enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)}
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -262,20 +266,30 @@ func Init() error {
|
||||
enableFederatedAvatar = disableGravatarSetting.GetValueBool()
|
||||
}
|
||||
|
||||
if setting.OfflineMode {
|
||||
if setting_module.OfflineMode {
|
||||
disableGravatar = true
|
||||
enableFederatedAvatar = false
|
||||
}
|
||||
|
||||
if disableGravatar || !enableFederatedAvatar {
|
||||
var err error
|
||||
GravatarSourceURL, err = url.Parse(setting.GravatarSource)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting.GravatarSource, err)
|
||||
if !GetSettingBool(KeyPictureDisableGravatar) {
|
||||
if err := SetSettingNoVersion(KeyPictureDisableGravatar, "true"); err != nil {
|
||||
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureDisableGravatar, err)
|
||||
}
|
||||
}
|
||||
if GetSettingBool(KeyPictureEnableFederatedAvatar) {
|
||||
if err := SetSettingNoVersion(KeyPictureEnableFederatedAvatar, "false"); err != nil {
|
||||
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enableFederatedAvatarSetting.GetValueBool() {
|
||||
if enableFederatedAvatar || !disableGravatar {
|
||||
var err error
|
||||
GravatarSourceURL, err = url.Parse(setting_module.GravatarSource)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err)
|
||||
}
|
||||
}
|
||||
|
||||
if GravatarSourceURL != nil && enableFederatedAvatarSetting.GetValueBool() {
|
||||
LibravatarService = libravatar.New()
|
||||
if GravatarSourceURL.Scheme == "https" {
|
||||
LibravatarService.SetUseHTTPS(true)
|
||||
|
||||
@@ -34,10 +34,14 @@ func TestSettings(t *testing.T) {
|
||||
assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue)
|
||||
|
||||
// updated setting
|
||||
updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: newSetting.Version}
|
||||
updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: settings[strings.ToLower(keyName)].Version}
|
||||
err = system.SetSetting(updatedSetting)
|
||||
assert.NoError(t, err)
|
||||
|
||||
value, err := system.GetSetting(keyName)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, updatedSetting.SettingValue, value)
|
||||
|
||||
// get all settings
|
||||
settings, err = system.GetAllSettings()
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -68,9 +68,7 @@ func (u *User) AvatarLinkWithSize(size int) string {
|
||||
useLocalAvatar := false
|
||||
autoGenerateAvatar := false
|
||||
|
||||
disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
|
||||
|
||||
disableGravatar := disableGravatarSetting.GetValueBool()
|
||||
disableGravatar := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
|
||||
|
||||
switch {
|
||||
case u.UseCustomAvatar:
|
||||
|
||||
49
models/user/must_change_password.go
Normal file
49
models/user/must_change_password.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func SetMustChangePassword(ctx context.Context, all, mustChangePassword bool, include, exclude []string) (int64, error) {
|
||||
sliceTrimSpaceDropEmpty := func(input []string) []string {
|
||||
output := make([]string, 0, len(input))
|
||||
for _, in := range input {
|
||||
in = strings.ToLower(strings.TrimSpace(in))
|
||||
if in == "" {
|
||||
continue
|
||||
}
|
||||
output = append(output, in)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
var cond builder.Cond
|
||||
|
||||
// Only include the users where something changes to get an accurate count
|
||||
cond = builder.Neq{"must_change_password": mustChangePassword}
|
||||
|
||||
if !all {
|
||||
include = sliceTrimSpaceDropEmpty(include)
|
||||
if len(include) == 0 {
|
||||
return 0, fmt.Errorf("no users to include provided")
|
||||
}
|
||||
|
||||
cond = cond.And(builder.In("lower_name", include))
|
||||
}
|
||||
|
||||
exclude = sliceTrimSpaceDropEmpty(exclude)
|
||||
if len(exclude) > 0 {
|
||||
cond = cond.And(builder.NotIn("lower_name", exclude))
|
||||
}
|
||||
|
||||
return db.GetEngine(ctx).Where(cond).MustCols("must_change_password").Update(&User{MustChangePassword: mustChangePassword})
|
||||
}
|
||||
@@ -54,13 +54,13 @@ func genSettingCacheKey(userID int64, key string) string {
|
||||
}
|
||||
|
||||
// GetSetting returns the setting value via the key
|
||||
func GetSetting(uid int64, key string) (*Setting, error) {
|
||||
return cache.Get(genSettingCacheKey(uid, key), func() (*Setting, error) {
|
||||
func GetSetting(uid int64, key string) (string, error) {
|
||||
return cache.GetString(genSettingCacheKey(uid, key), func() (string, error) {
|
||||
res, err := GetSettingNoCache(uid, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
return res.SettingValue, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func SetUserSetting(userID int64, key, value string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := cache.Set(genSettingCacheKey(userID, key), func() (string, error) {
|
||||
_, err := cache.GetString(genSettingCacheKey(userID, key), func() (string, error) {
|
||||
return value, upsertUserSettingValue(userID, key, value)
|
||||
})
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
@@ -22,6 +20,7 @@ import (
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/auth/openid"
|
||||
"code.gitea.io/gitea/modules/auth/password/hash"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -30,10 +29,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@@ -48,21 +43,6 @@ const (
|
||||
UserTypeOrganization
|
||||
)
|
||||
|
||||
const (
|
||||
algoBcrypt = "bcrypt"
|
||||
algoScrypt = "scrypt"
|
||||
algoArgon2 = "argon2"
|
||||
algoPbkdf2 = "pbkdf2"
|
||||
)
|
||||
|
||||
// AvailableHashAlgorithms represents the available password hashing algorithms
|
||||
var AvailableHashAlgorithms = []string{
|
||||
algoPbkdf2,
|
||||
algoArgon2,
|
||||
algoScrypt,
|
||||
algoBcrypt,
|
||||
}
|
||||
|
||||
const (
|
||||
// EmailNotificationsEnabled indicates that the user would like to receive all email notifications except your own
|
||||
EmailNotificationsEnabled = "enabled"
|
||||
@@ -368,42 +348,6 @@ func (u *User) NewGitSig() *git.Signature {
|
||||
}
|
||||
}
|
||||
|
||||
func hashPassword(passwd, salt, algo string) (string, error) {
|
||||
var tempPasswd []byte
|
||||
var saltBytes []byte
|
||||
|
||||
// There are two formats for the Salt value:
|
||||
// * The new format is a (32+)-byte hex-encoded string
|
||||
// * The old format was a 10-byte binary format
|
||||
// We have to tolerate both here but Authenticate should
|
||||
// regenerate the Salt following a successful validation.
|
||||
if len(salt) == 10 {
|
||||
saltBytes = []byte(salt)
|
||||
} else {
|
||||
var err error
|
||||
saltBytes, err = hex.DecodeString(salt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
switch algo {
|
||||
case algoBcrypt:
|
||||
tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
|
||||
return string(tempPasswd), nil
|
||||
case algoScrypt:
|
||||
tempPasswd, _ = scrypt.Key([]byte(passwd), saltBytes, 65536, 16, 2, 50)
|
||||
case algoArgon2:
|
||||
tempPasswd = argon2.IDKey([]byte(passwd), saltBytes, 2, 65536, 8, 50)
|
||||
case algoPbkdf2:
|
||||
fallthrough
|
||||
default:
|
||||
tempPasswd = pbkdf2.Key([]byte(passwd), saltBytes, 10000, 50, sha256.New)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", tempPasswd), nil
|
||||
}
|
||||
|
||||
// SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
|
||||
// change passwd, salt and passwd_hash_algo fields
|
||||
func (u *User) SetPassword(passwd string) (err error) {
|
||||
@@ -417,7 +361,7 @@ func (u *User) SetPassword(passwd string) (err error) {
|
||||
if u.Salt, err = GetUserSalt(); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Passwd, err = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo); err != nil {
|
||||
if u.Passwd, err = hash.Parse(setting.PasswordHashAlgo).Hash(passwd, u.Salt); err != nil {
|
||||
return err
|
||||
}
|
||||
u.PasswdHashAlgo = setting.PasswordHashAlgo
|
||||
@@ -425,20 +369,9 @@ func (u *User) SetPassword(passwd string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePassword checks if given password matches the one belongs to the user.
|
||||
// ValidatePassword checks if the given password matches the one belonging to the user.
|
||||
func (u *User) ValidatePassword(passwd string) bool {
|
||||
tempHash, err := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if u.PasswdHashAlgo != algoBcrypt && subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(tempHash)) == 1 {
|
||||
return true
|
||||
}
|
||||
if u.PasswdHashAlgo == algoBcrypt && bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(passwd)) == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return hash.Parse(u.PasswdHashAlgo).VerifyPassword(passwd, u.Passwd, u.Salt)
|
||||
}
|
||||
|
||||
// IsPasswordSet checks if the password is set or left empty
|
||||
@@ -1227,7 +1160,10 @@ func GetUserByOpenID(uri string) (*User, error) {
|
||||
// GetAdminUser returns the first administrator
|
||||
func GetAdminUser() (*User, error) {
|
||||
var admin User
|
||||
has, err := db.GetEngine(db.DefaultContext).Where("is_admin=?", true).Get(&admin)
|
||||
has, err := db.GetEngine(db.DefaultContext).
|
||||
Where("is_admin=?", true).
|
||||
Asc("id"). // Reliably get the admin with the lowest ID.
|
||||
Get(&admin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/password/hash"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -162,7 +163,7 @@ func TestEmailNotificationPreferences(t *testing.T) {
|
||||
func TestHashPasswordDeterministic(t *testing.T) {
|
||||
b := make([]byte, 16)
|
||||
u := &user_model.User{}
|
||||
algos := []string{"argon2", "pbkdf2", "scrypt", "bcrypt"}
|
||||
algos := hash.RecommendedHashAlgorithms
|
||||
for j := 0; j < len(algos); j++ {
|
||||
u.PasswdHashAlgo = algos[j]
|
||||
for i := 0; i < 50; i++ {
|
||||
|
||||
77
modules/auth/password/hash/argon2.go
Normal file
77
modules/auth/password/hash/argon2.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package hash
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("argon2", NewArgon2Hasher)
|
||||
}
|
||||
|
||||
// Argon2Hasher implements PasswordHasher
|
||||
// and uses the Argon2 key derivation function, hybrant variant
|
||||
type Argon2Hasher struct {
|
||||
time uint32
|
||||
memory uint32
|
||||
threads uint8
|
||||
keyLen uint32
|
||||
}
|
||||
|
||||
// HashWithSaltBytes a provided password and salt
|
||||
func (hasher *Argon2Hasher) HashWithSaltBytes(password string, salt []byte) string {
|
||||
if hasher == nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(argon2.IDKey([]byte(password), salt, hasher.time, hasher.memory, hasher.threads, hasher.keyLen))
|
||||
}
|
||||
|
||||
// NewArgon2Hasher is a factory method to create an Argon2Hasher
|
||||
// The provided config should be either empty or of the form:
|
||||
// "<time>$<memory>$<threads>$<keyLen>", where <x> is the string representation
|
||||
// of an integer
|
||||
func NewArgon2Hasher(config string) *Argon2Hasher {
|
||||
// This default configuration uses the following parameters:
|
||||
// time=2, memory=64*1024, threads=8, keyLen=50.
|
||||
// It will make two passes through the memory, using 64MiB in total.
|
||||
hasher := &Argon2Hasher{
|
||||
time: 2,
|
||||
memory: 1 << 16,
|
||||
threads: 8,
|
||||
keyLen: 50,
|
||||
}
|
||||
|
||||
if config == "" {
|
||||
return hasher
|
||||
}
|
||||
|
||||
vals := strings.SplitN(config, "$", 4)
|
||||
if len(vals) != 4 {
|
||||
log.Error("invalid argon2 hash spec %s", config)
|
||||
return nil
|
||||
}
|
||||
|
||||
parsed, err := parseUIntParam(vals[0], "time", "argon2", config, nil)
|
||||
hasher.time = uint32(parsed)
|
||||
|
||||
parsed, err = parseUIntParam(vals[1], "memory", "argon2", config, err)
|
||||
hasher.memory = uint32(parsed)
|
||||
|
||||
parsed, err = parseUIntParam(vals[2], "threads", "argon2", config, err)
|
||||
hasher.threads = uint8(parsed)
|
||||
|
||||
parsed, err = parseUIntParam(vals[3], "keyLen", "argon2", config, err)
|
||||
hasher.keyLen = uint32(parsed)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return hasher
|
||||
}
|
||||
51
modules/auth/password/hash/bcrypt.go
Normal file
51
modules/auth/password/hash/bcrypt.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package hash
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("bcrypt", NewBcryptHasher)
|
||||
}
|
||||
|
||||
// BcryptHasher implements PasswordHasher
|
||||
// and uses the bcrypt password hash function.
|
||||
type BcryptHasher struct {
|
||||
cost int
|
||||
}
|
||||
|
||||
// HashWithSaltBytes a provided password and salt
|
||||
func (hasher *BcryptHasher) HashWithSaltBytes(password string, salt []byte) string {
|
||||
if hasher == nil {
|
||||
return ""
|
||||
}
|
||||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), hasher.cost)
|
||||
return string(hashedPassword)
|
||||
}
|
||||
|
||||
func (hasher *BcryptHasher) VerifyPassword(password, hashedPassword, salt string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
|
||||
}
|
||||
|
||||
// NewBcryptHasher is a factory method to create an BcryptHasher
|
||||
// The provided config should be either empty or the string representation of the "<cost>"
|
||||
// as an integer
|
||||
func NewBcryptHasher(config string) *BcryptHasher {
|
||||
hasher := &BcryptHasher{
|
||||
cost: 10, // cost=10. i.e. 2^10 rounds of key expansion.
|
||||
}
|
||||
|
||||
if config == "" {
|
||||
return hasher
|
||||
}
|
||||
var err error
|
||||
hasher.cost, err = parseIntParam(config, "cost", "bcrypt", config, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return hasher
|
||||
}
|
||||
28
modules/auth/password/hash/common.go
Normal file
28
modules/auth/password/hash/common.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package hash
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
func parseIntParam(value, param, algorithmName, config string, previousErr error) (int, error) {
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config)
|
||||
return 0, err
|
||||
}
|
||||
return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed
|
||||
}
|
||||
|
||||
func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) {
|
||||
parsed, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config)
|
||||
return 0, err
|
||||
}
|
||||
return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed
|
||||
}
|
||||
147
modules/auth/password/hash/hash.go
Normal file
147
modules/auth/password/hash/hash.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// This package takes care of hashing passwords, verifying passwords, defining
|
||||
// available password algorithms, defining recommended password algorithms and
|
||||
// choosing the default password algorithm.
|
||||
|
||||
// PasswordSaltHasher will hash a provided password with the provided saltBytes
|
||||
type PasswordSaltHasher interface {
|
||||
HashWithSaltBytes(password string, saltBytes []byte) string
|
||||
}
|
||||
|
||||
// PasswordHasher will hash a provided password with the salt
|
||||
type PasswordHasher interface {
|
||||
Hash(password, salt string) (string, error)
|
||||
}
|
||||
|
||||
// PasswordVerifier will ensure that a providedPassword matches the hashPassword when hashed with the salt
|
||||
type PasswordVerifier interface {
|
||||
VerifyPassword(providedPassword, hashedPassword, salt string) bool
|
||||
}
|
||||
|
||||
// PasswordHashAlgorithms are named PasswordSaltHashers with a default verifier and hash function
|
||||
type PasswordHashAlgorithm struct {
|
||||
PasswordSaltHasher
|
||||
Name string
|
||||
}
|
||||
|
||||
// Hash the provided password with the salt and return the hash
|
||||
func (algorithm *PasswordHashAlgorithm) Hash(password, salt string) (string, error) {
|
||||
var saltBytes []byte
|
||||
|
||||
// There are two formats for the salt value:
|
||||
// * The new format is a (32+)-byte hex-encoded string
|
||||
// * The old format was a 10-byte binary format
|
||||
// We have to tolerate both here.
|
||||
if len(salt) == 10 {
|
||||
saltBytes = []byte(salt)
|
||||
} else {
|
||||
var err error
|
||||
saltBytes, err = hex.DecodeString(salt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return algorithm.HashWithSaltBytes(password, saltBytes), nil
|
||||
}
|
||||
|
||||
// Verify the provided password matches the hashPassword when hashed with the salt
|
||||
func (algorithm *PasswordHashAlgorithm) VerifyPassword(providedPassword, hashedPassword, salt string) bool {
|
||||
// The bcrypt package has its own specialized compare function that takes into
|
||||
// account the stored password's bcrypt parameters.
|
||||
if verifier, ok := algorithm.PasswordSaltHasher.(PasswordVerifier); ok {
|
||||
return verifier.VerifyPassword(providedPassword, hashedPassword, salt)
|
||||
}
|
||||
|
||||
// Compute the hash of the password.
|
||||
providedPasswordHash, err := algorithm.Hash(providedPassword, salt)
|
||||
if err != nil {
|
||||
log.Error("passwordhash: %v.Hash(): %v", algorithm.Name, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare it against the hashed password in constant-time.
|
||||
return subtle.ConstantTimeCompare([]byte(hashedPassword), []byte(providedPasswordHash)) == 1
|
||||
}
|
||||
|
||||
var (
|
||||
lastNonDefaultAlgorithm atomic.Value
|
||||
availableHasherFactories = map[string]func(string) PasswordSaltHasher{}
|
||||
)
|
||||
|
||||
// Register registers a PasswordSaltHasher with the availableHasherFactories
|
||||
// This is not thread safe.
|
||||
func Register[T PasswordSaltHasher](name string, newFn func(config string) T) {
|
||||
if _, has := availableHasherFactories[name]; has {
|
||||
panic(fmt.Errorf("duplicate registration of password salt hasher: %s", name))
|
||||
}
|
||||
|
||||
availableHasherFactories[name] = func(config string) PasswordSaltHasher {
|
||||
n := newFn(config)
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
// In early versions of gitea the password hash algorithm field could be empty
|
||||
// At that point the default was `pbkdf2` without configuration values
|
||||
// Please note this is not the same as the DefaultAlgorithm
|
||||
const defaultEmptyHashAlgorithmName = "pbkdf2"
|
||||
|
||||
func Parse(algorithm string) *PasswordHashAlgorithm {
|
||||
if algorithm == "" {
|
||||
algorithm = defaultEmptyHashAlgorithmName
|
||||
}
|
||||
|
||||
if DefaultHashAlgorithm != nil && algorithm == DefaultHashAlgorithm.Name {
|
||||
return DefaultHashAlgorithm
|
||||
}
|
||||
|
||||
ptr := lastNonDefaultAlgorithm.Load()
|
||||
if ptr != nil {
|
||||
hashAlgorithm, ok := ptr.(*PasswordHashAlgorithm)
|
||||
if ok && hashAlgorithm.Name == algorithm {
|
||||
return hashAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
vals := strings.SplitN(algorithm, "$", 2)
|
||||
var name string
|
||||
var config string
|
||||
if len(vals) == 0 {
|
||||
return nil
|
||||
}
|
||||
name = vals[0]
|
||||
if len(vals) > 1 {
|
||||
config = vals[1]
|
||||
}
|
||||
newFn, has := availableHasherFactories[name]
|
||||
if !has {
|
||||
return nil
|
||||
}
|
||||
ph := newFn(config)
|
||||
if ph == nil {
|
||||
return nil
|
||||
}
|
||||
hashAlgorithm := &PasswordHashAlgorithm{
|
||||
PasswordSaltHasher: ph,
|
||||
Name: algorithm,
|
||||
}
|
||||
|
||||
lastNonDefaultAlgorithm.Store(hashAlgorithm)
|
||||
|
||||
return hashAlgorithm
|
||||
}
|
||||
186
modules/auth/password/hash/hash_test.go
Normal file
186
modules/auth/password/hash/hash_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package hash
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type testSaltHasher string
|
||||
|
||||
func (t testSaltHasher) HashWithSaltBytes(password string, salt []byte) string {
|
||||
return password + "$" + string(salt) + "$" + string(t)
|
||||
}
|
||||
|
||||
func Test_registerHasher(t *testing.T) {
|
||||
Register("Test_registerHasher", func(config string) testSaltHasher {
|
||||
return testSaltHasher(config)
|
||||
})
|
||||
|
||||
assert.Panics(t, func() {
|
||||
Register("Test_registerHasher", func(config string) testSaltHasher {
|
||||
return testSaltHasher(config)
|
||||
})
|
||||
})
|
||||
|
||||
assert.Equal(t, "password$salt$",
|
||||
Parse("Test_registerHasher").PasswordSaltHasher.HashWithSaltBytes("password", []byte("salt")))
|
||||
|
||||
assert.Equal(t, "password$salt$config",
|
||||
Parse("Test_registerHasher$config").PasswordSaltHasher.HashWithSaltBytes("password", []byte("salt")))
|
||||
|
||||
delete(availableHasherFactories, "Test_registerHasher")
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
hashAlgorithmsToTest := []string{}
|
||||
for plainHashAlgorithmNames := range availableHasherFactories {
|
||||
hashAlgorithmsToTest = append(hashAlgorithmsToTest, plainHashAlgorithmNames)
|
||||
}
|
||||
for _, aliased := range aliasAlgorithmNames {
|
||||
if strings.Contains(aliased, "$") {
|
||||
hashAlgorithmsToTest = append(hashAlgorithmsToTest, aliased)
|
||||
}
|
||||
}
|
||||
for _, algorithmName := range hashAlgorithmsToTest {
|
||||
t.Run(algorithmName, func(t *testing.T) {
|
||||
algo := Parse(algorithmName)
|
||||
assert.NotNil(t, algo, "Algorithm %s resulted in an empty algorithm", algorithmName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashing(t *testing.T) {
|
||||
hashAlgorithmsToTest := []string{}
|
||||
for plainHashAlgorithmNames := range availableHasherFactories {
|
||||
hashAlgorithmsToTest = append(hashAlgorithmsToTest, plainHashAlgorithmNames)
|
||||
}
|
||||
for _, aliased := range aliasAlgorithmNames {
|
||||
if strings.Contains(aliased, "$") {
|
||||
hashAlgorithmsToTest = append(hashAlgorithmsToTest, aliased)
|
||||
}
|
||||
}
|
||||
|
||||
runTests := func(password, salt string, shouldPass bool) {
|
||||
for _, algorithmName := range hashAlgorithmsToTest {
|
||||
t.Run(algorithmName, func(t *testing.T) {
|
||||
output, err := Parse(algorithmName).Hash(password, salt)
|
||||
if shouldPass {
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, output, "output for %s was empty", algorithmName)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, Parse(algorithmName).VerifyPassword(password, output, salt), shouldPass)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test with new salt format.
|
||||
runTests(strings.Repeat("a", 16), hex.EncodeToString([]byte{0x01, 0x02, 0x03}), true)
|
||||
|
||||
// Test with legacy salt format.
|
||||
runTests(strings.Repeat("a", 16), strings.Repeat("b", 10), true)
|
||||
|
||||
// Test with invalid salt.
|
||||
runTests(strings.Repeat("a", 16), "a", false)
|
||||
}
|
||||
|
||||
// vectors were generated using the current codebase.
|
||||
var vectors = []struct {
|
||||
algorithms []string
|
||||
password string
|
||||
salt string
|
||||
output string
|
||||
shouldfail bool
|
||||
}{
|
||||
{
|
||||
algorithms: []string{"bcrypt", "bcrypt$10"},
|
||||
password: "abcdef",
|
||||
salt: strings.Repeat("a", 10),
|
||||
output: "$2a$10$fjtm8BsQ2crym01/piJroenO3oSVUBhSLKaGdTYJ4tG0ePVCrU0G2",
|
||||
shouldfail: false,
|
||||
},
|
||||
{
|
||||
algorithms: []string{"scrypt", "scrypt$65536$16$2$50"},
|
||||
password: "abcdef",
|
||||
salt: strings.Repeat("a", 10),
|
||||
output: "3b571d0c07c62d42b7bad3dbf18fb0cd67d4d8cd4ad4c6928e1090e5b2a4a84437c6fd2627d897c0e7e65025ca62b67a0002",
|
||||
shouldfail: false,
|
||||
},
|
||||
{
|
||||
algorithms: []string{"argon2", "argon2$2$65536$8$50"},
|
||||
password: "abcdef",
|
||||
salt: strings.Repeat("a", 10),
|
||||
output: "551f089f570f989975b6f7c6a8ff3cf89bc486dd7bbe87ed4d80ad4362f8ee599ec8dda78dac196301b98456402bcda775dc",
|
||||
shouldfail: false,
|
||||
},
|
||||
{
|
||||
algorithms: []string{"pbkdf2", "pbkdf2$10000$50"},
|
||||
password: "abcdef",
|
||||
salt: strings.Repeat("a", 10),
|
||||
output: "ab48d5471b7e6ed42d10001db88c852ff7303c788e49da5c3c7b63d5adf96360303724b74b679223a3dea8a242d10abb1913",
|
||||
shouldfail: false,
|
||||
},
|
||||
{
|
||||
algorithms: []string{"bcrypt", "bcrypt$10"},
|
||||
password: "abcdef",
|
||||
salt: hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
|
||||
output: "$2a$10$qhgm32w9ZpqLygugWJsLjey8xRGcaq9iXAfmCeNBXxddgyoaOC3Gq",
|
||||
shouldfail: false,
|
||||
},
|
||||
{
|
||||
algorithms: []string{"scrypt", "scrypt$65536$16$2$50"},
|
||||
password: "abcdef",
|
||||
salt: hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
|
||||
output: "25fe5f66b43fa4eb7b6717905317cd2223cf841092dc8e0a1e8c75720ad4846cb5d9387303e14bc3c69faa3b1c51ef4b7de1",
|
||||
shouldfail: false,
|
||||
},
|
||||
{
|
||||
algorithms: []string{"argon2", "argon2$2$65536$8$50"},
|
||||
password: "abcdef",
|
||||
salt: hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
|
||||
output: "9c287db63a91d18bb1414b703216da4fc431387c1ae7c8acdb280222f11f0929831055dbfd5126a3b48566692e83ec750d2a",
|
||||
shouldfail: false,
|
||||
},
|
||||
{
|
||||
algorithms: []string{"pbkdf2", "pbkdf2$10000$50"},
|
||||
password: "abcdef",
|
||||
salt: hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
|
||||
output: "45d6cdc843d65cf0eda7b90ab41435762a282f7df013477a1c5b212ba81dbdca2edf1ecc4b5cb05956bb9e0c37ab29315d78",
|
||||
shouldfail: false,
|
||||
},
|
||||
{
|
||||
algorithms: []string{"pbkdf2$320000$50"},
|
||||
password: "abcdef",
|
||||
salt: hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}),
|
||||
output: "84e233114499e8721da80e85568e5b7b5900b3e49a30845fcda9d1e1756da4547d70f8740ac2b4a5d82f88cebcd27f21bfe2",
|
||||
shouldfail: false,
|
||||
},
|
||||
{
|
||||
algorithms: []string{"pbkdf2", "pbkdf2$10000$50"},
|
||||
password: "abcdef",
|
||||
salt: "",
|
||||
output: "",
|
||||
shouldfail: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Ensure that the current code will correctly verify against the test vectors.
|
||||
func TestVectors(t *testing.T) {
|
||||
for i, vector := range vectors {
|
||||
for _, algorithm := range vector.algorithms {
|
||||
t.Run(strconv.Itoa(i)+": "+algorithm, func(t *testing.T) {
|
||||
pa := Parse(algorithm)
|
||||
assert.Equal(t, !vector.shouldfail, pa.VerifyPassword(vector.password, vector.output, vector.salt))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
62
modules/auth/password/hash/pbkdf2.go
Normal file
62
modules/auth/password/hash/pbkdf2.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package hash
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("pbkdf2", NewPBKDF2Hasher)
|
||||
}
|
||||
|
||||
// PBKDF2Hasher implements PasswordHasher
|
||||
// and uses the PBKDF2 key derivation function.
|
||||
type PBKDF2Hasher struct {
|
||||
iter, keyLen int
|
||||
}
|
||||
|
||||
// HashWithSaltBytes a provided password and salt
|
||||
func (hasher *PBKDF2Hasher) HashWithSaltBytes(password string, salt []byte) string {
|
||||
if hasher == nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(pbkdf2.Key([]byte(password), salt, hasher.iter, hasher.keyLen, sha256.New))
|
||||
}
|
||||
|
||||
// NewPBKDF2Hasher is a factory method to create an PBKDF2Hasher
|
||||
// config should be either empty or of the form:
|
||||
// "<iter>$<keyLen>", where <x> is the string representation
|
||||
// of an integer
|
||||
func NewPBKDF2Hasher(config string) *PBKDF2Hasher {
|
||||
hasher := &PBKDF2Hasher{
|
||||
iter: 10_000,
|
||||
keyLen: 50,
|
||||
}
|
||||
|
||||
if config == "" {
|
||||
return hasher
|
||||
}
|
||||
|
||||
vals := strings.SplitN(config, "$", 2)
|
||||
if len(vals) != 2 {
|
||||
log.Error("invalid pbkdf2 hash spec %s", config)
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
hasher.iter, err = parseIntParam(vals[0], "iter", "pbkdf2", config, nil)
|
||||
hasher.keyLen, err = parseIntParam(vals[1], "keyLen", "pbkdf2", config, err)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return hasher
|
||||
}
|
||||
64
modules/auth/password/hash/scrypt.go
Normal file
64
modules/auth/password/hash/scrypt.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package hash
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("scrypt", NewScryptHasher)
|
||||
}
|
||||
|
||||
// ScryptHasher implements PasswordHasher
|
||||
// and uses the scrypt key derivation function.
|
||||
type ScryptHasher struct {
|
||||
n, r, p, keyLen int
|
||||
}
|
||||
|
||||
// HashWithSaltBytes a provided password and salt
|
||||
func (hasher *ScryptHasher) HashWithSaltBytes(password string, salt []byte) string {
|
||||
if hasher == nil {
|
||||
return ""
|
||||
}
|
||||
hashedPassword, _ := scrypt.Key([]byte(password), salt, hasher.n, hasher.r, hasher.p, hasher.keyLen)
|
||||
return hex.EncodeToString(hashedPassword)
|
||||
}
|
||||
|
||||
// NewScryptHasher is a factory method to create an ScryptHasher
|
||||
// The provided config should be either empty or of the form:
|
||||
// "<n>$<r>$<p>$<keyLen>", where <x> is the string representation
|
||||
// of an integer
|
||||
func NewScryptHasher(config string) *ScryptHasher {
|
||||
hasher := &ScryptHasher{
|
||||
n: 1 << 16,
|
||||
r: 16,
|
||||
p: 2, // 2 passes through memory - this default config will use 128MiB in total.
|
||||
keyLen: 50,
|
||||
}
|
||||
|
||||
if config == "" {
|
||||
return hasher
|
||||
}
|
||||
|
||||
vals := strings.SplitN(config, "$", 4)
|
||||
if len(vals) != 4 {
|
||||
log.Error("invalid scrypt hash spec %s", config)
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
hasher.n, err = parseIntParam(vals[0], "n", "scrypt", config, nil)
|
||||
hasher.r, err = parseIntParam(vals[1], "r", "scrypt", config, err)
|
||||
hasher.p, err = parseIntParam(vals[2], "p", "scrypt", config, err)
|
||||
hasher.keyLen, err = parseIntParam(vals[3], "keyLen", "scrypt", config, err)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return hasher
|
||||
}
|
||||
44
modules/auth/password/hash/setting.go
Normal file
44
modules/auth/password/hash/setting.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package hash
|
||||
|
||||
const DefaultHashAlgorithmName = "pbkdf2"
|
||||
|
||||
var DefaultHashAlgorithm *PasswordHashAlgorithm
|
||||
|
||||
var aliasAlgorithmNames = map[string]string{
|
||||
"argon2": "argon2$2$65536$8$50",
|
||||
"bcrypt": "bcrypt$10",
|
||||
"scrypt": "scrypt$65536$16$2$50",
|
||||
"pbkdf2": "pbkdf2_v2", // pbkdf2 should default to pbkdf2_v2
|
||||
"pbkdf2_v1": "pbkdf2$10000$50",
|
||||
// The latest PBKDF2 password algorithm is used as the default since it doesn't
|
||||
// use a lot of memory and is safer to use on less powerful devices.
|
||||
"pbkdf2_v2": "pbkdf2$50000$50",
|
||||
// The pbkdf2_hi password algorithm is offered as a stronger alternative to the
|
||||
// slightly improved pbkdf2_v2 algorithm
|
||||
"pbkdf2_hi": "pbkdf2$320000$50",
|
||||
}
|
||||
|
||||
var RecommendedHashAlgorithms = []string{
|
||||
"pbkdf2",
|
||||
"argon2",
|
||||
"bcrypt",
|
||||
"scrypt",
|
||||
"pbkdf2_hi",
|
||||
}
|
||||
|
||||
func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) {
|
||||
if algorithmName == "" {
|
||||
algorithmName = DefaultHashAlgorithmName
|
||||
}
|
||||
alias, has := aliasAlgorithmNames[algorithmName]
|
||||
for has {
|
||||
algorithmName = alias
|
||||
alias, has = aliasAlgorithmNames[algorithmName]
|
||||
}
|
||||
DefaultHashAlgorithm = Parse(algorithmName)
|
||||
|
||||
return algorithmName, DefaultHashAlgorithm
|
||||
}
|
||||
38
modules/auth/password/hash/setting_test.go
Normal file
38
modules/auth/password/hash/setting_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package hash
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckSettingPasswordHashAlgorithm(t *testing.T) {
|
||||
t.Run("pbkdf2 is pbkdf2_v2", func(t *testing.T) {
|
||||
pbkdf2v2Config, pbkdf2v2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2_v2")
|
||||
pbkdf2Config, pbkdf2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2")
|
||||
|
||||
assert.Equal(t, pbkdf2v2Config, pbkdf2Config)
|
||||
assert.Equal(t, pbkdf2v2Algo.Name, pbkdf2Algo.Name)
|
||||
})
|
||||
|
||||
for a, b := range aliasAlgorithmNames {
|
||||
t.Run(a+"="+b, func(t *testing.T) {
|
||||
aConfig, aAlgo := SetDefaultPasswordHashAlgorithm(a)
|
||||
bConfig, bAlgo := SetDefaultPasswordHashAlgorithm(b)
|
||||
|
||||
assert.Equal(t, bConfig, aConfig)
|
||||
assert.Equal(t, aAlgo.Name, bAlgo.Name)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("pbkdf2_v2 is the default when default password hash algorithm is empty", func(t *testing.T) {
|
||||
emptyConfig, emptyAlgo := SetDefaultPasswordHashAlgorithm("")
|
||||
pbkdf2v2Config, pbkdf2v2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2_v2")
|
||||
|
||||
assert.Equal(t, pbkdf2v2Config, emptyConfig)
|
||||
assert.Equal(t, pbkdf2v2Algo.Name, emptyAlgo.Name)
|
||||
})
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
)
|
||||
|
||||
// complexity contains information about a particular kind of password complexity
|
||||
@@ -113,13 +113,13 @@ func Generate(n int) (string, error) {
|
||||
}
|
||||
|
||||
// BuildComplexityError builds the error message when password complexity checks fail
|
||||
func BuildComplexityError(ctx *context.Context) string {
|
||||
func BuildComplexityError(locale translation.Locale) string {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(ctx.Tr("form.password_complexity"))
|
||||
buffer.WriteString(locale.Tr("form.password_complexity"))
|
||||
buffer.WriteString("<ul>")
|
||||
for _, c := range requiredList {
|
||||
buffer.WriteString("<li>")
|
||||
buffer.WriteString(ctx.Tr(c.TrNameOne))
|
||||
buffer.WriteString(locale.Tr(c.TrNameOne))
|
||||
buffer.WriteString("</li>")
|
||||
}
|
||||
buffer.WriteString("</ul>")
|
||||
33
modules/cache/cache.go
vendored
33
modules/cache/cache.go
vendored
@@ -46,39 +46,6 @@ func GetCache() mc.Cache {
|
||||
return conn
|
||||
}
|
||||
|
||||
// Get returns the key value from cache with callback when no key exists in cache
|
||||
func Get[V interface{}](key string, getFunc func() (V, error)) (V, error) {
|
||||
if conn == nil || setting.CacheService.TTL == 0 {
|
||||
return getFunc()
|
||||
}
|
||||
|
||||
cached := conn.Get(key)
|
||||
if value, ok := cached.(V); ok {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
value, err := getFunc()
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
|
||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
}
|
||||
|
||||
// Set updates and returns the key value in the cache with callback. The old value is only removed if the updateFunc() is successful
|
||||
func Set[V interface{}](key string, valueFunc func() (V, error)) (V, error) {
|
||||
if conn == nil || setting.CacheService.TTL == 0 {
|
||||
return valueFunc()
|
||||
}
|
||||
|
||||
value, err := valueFunc()
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
|
||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
}
|
||||
|
||||
// GetString returns the key value from cache with callback when no key exists in cache
|
||||
func GetString(key string, getFunc func() (string, error)) (string, error) {
|
||||
if conn == nil || setting.CacheService.TTL == 0 {
|
||||
|
||||
@@ -45,7 +45,7 @@ func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.
|
||||
return streamer.escaped, err
|
||||
}
|
||||
|
||||
// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
|
||||
// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte. HTML line breaks are not inserted after every newline by this method.
|
||||
func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
|
||||
bufRd := bufio.NewReader(reader)
|
||||
outputStream := &HTMLStreamerWriter{Writer: writer}
|
||||
@@ -66,10 +66,6 @@ func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale transl
|
||||
}
|
||||
break
|
||||
}
|
||||
if err := streamer.SelfClosingTag("br"); err != nil {
|
||||
streamer.escaped.HasError = true
|
||||
return streamer.escaped, err
|
||||
}
|
||||
}
|
||||
return streamer.escaped, err
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ package charset
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -21,12 +20,16 @@ import (
|
||||
var defaultWordRegexp = regexp.MustCompile(`(-?\d*\.\d\w*)|([^\` + "`" + `\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s\x00-\x1f]+)`)
|
||||
|
||||
func NewEscapeStreamer(locale translation.Locale, next HTMLStreamer, allowed ...rune) HTMLStreamer {
|
||||
allowedM := make(map[rune]bool, len(allowed))
|
||||
for _, v := range allowed {
|
||||
allowedM[v] = true
|
||||
}
|
||||
return &escapeStreamer{
|
||||
escaped: &EscapeStatus{},
|
||||
PassthroughHTMLStreamer: *NewPassthroughStreamer(next),
|
||||
locale: locale,
|
||||
ambiguousTables: AmbiguousTablesForLocale(locale),
|
||||
allowed: allowed,
|
||||
allowed: allowedM,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +38,7 @@ type escapeStreamer struct {
|
||||
escaped *EscapeStatus
|
||||
locale translation.Locale
|
||||
ambiguousTables []*AmbiguousTable
|
||||
allowed []rune
|
||||
allowed map[rune]bool
|
||||
}
|
||||
|
||||
func (e *escapeStreamer) EscapeStatus() *EscapeStatus {
|
||||
@@ -257,7 +260,7 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables
|
||||
runeCounts.numBrokenRunes++
|
||||
case r == ' ' || r == '\t' || r == '\n':
|
||||
runeCounts.numBasicRunes++
|
||||
case e.isAllowed(r):
|
||||
case e.allowed[r]:
|
||||
if r > 0x7e || r < 0x20 {
|
||||
types[i] = nonBasicASCIIRuneType
|
||||
runeCounts.numNonConfusingNonBasicRunes++
|
||||
@@ -283,16 +286,3 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables
|
||||
}
|
||||
return types, confusables, runeCounts
|
||||
}
|
||||
|
||||
func (e *escapeStreamer) isAllowed(r rune) bool {
|
||||
if len(e.allowed) == 0 {
|
||||
return false
|
||||
}
|
||||
if len(e.allowed) == 1 {
|
||||
return e.allowed[0] == r
|
||||
}
|
||||
|
||||
return sort.Search(len(e.allowed), func(i int) bool {
|
||||
return e.allowed[i] >= r
|
||||
}) >= 0
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ package context
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
@@ -19,10 +19,11 @@ type Pagination struct {
|
||||
urlParams []string
|
||||
}
|
||||
|
||||
// NewPagination creates a new instance of the Pagination struct
|
||||
func NewPagination(total, page, issueNum, numPages int) *Pagination {
|
||||
// NewPagination creates a new instance of the Pagination struct.
|
||||
// "pagingNum" is "page size" or "limit", "current" is "page"
|
||||
func NewPagination(total, pagingNum, current, numPages int) *Pagination {
|
||||
p := &Pagination{}
|
||||
p.Paginater = paginator.New(total, page, issueNum, numPages)
|
||||
p.Paginater = paginator.New(total, pagingNum, current, numPages)
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
@@ -1087,6 +1087,9 @@ func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplat
|
||||
if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
|
||||
invalidFiles[fullName] = err
|
||||
} else {
|
||||
if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
|
||||
it.Ref = git.BranchPrefix + it.Ref
|
||||
}
|
||||
issueTemplates = append(issueTemplates, it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
||||
},
|
||||
}
|
||||
|
||||
if pr.Issue.ClosedUnix != 0 {
|
||||
apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr()
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
|
||||
if err != nil {
|
||||
log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RepoPath(), err)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,12 +24,12 @@ type BlamePart struct {
|
||||
|
||||
// BlameReader returns part of file blame one by one
|
||||
type BlameReader struct {
|
||||
cmd *exec.Cmd
|
||||
output io.ReadCloser
|
||||
reader *bufio.Reader
|
||||
lastSha *string
|
||||
cancel context.CancelFunc // Cancels the context that this reader runs in
|
||||
finished process.FinishedFunc // Tells the process manager we're finished and it can remove the associated process from the process table
|
||||
cmd *exec.Cmd
|
||||
reader io.ReadCloser
|
||||
lastSha *string
|
||||
cancel context.CancelFunc // Cancels the context that this reader runs in
|
||||
finished process.FinishedFunc // Tells the process manager we're finished and it can remove the associated process from the process table
|
||||
bufferedReader *bufio.Reader
|
||||
}
|
||||
|
||||
var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
|
||||
@@ -38,8 +38,6 @@ var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
|
||||
func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
var blamePart *BlamePart
|
||||
|
||||
reader := r.reader
|
||||
|
||||
if r.lastSha != nil {
|
||||
blamePart = &BlamePart{*r.lastSha, make([]string, 0)}
|
||||
}
|
||||
@@ -49,7 +47,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
var err error
|
||||
|
||||
for err != io.EOF {
|
||||
line, isPrefix, err = reader.ReadLine()
|
||||
line, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return blamePart, err
|
||||
}
|
||||
@@ -71,7 +69,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
r.lastSha = &sha1
|
||||
// need to munch to end of line...
|
||||
for isPrefix {
|
||||
_, isPrefix, err = reader.ReadLine()
|
||||
_, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return blamePart, err
|
||||
}
|
||||
@@ -86,7 +84,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
|
||||
// need to munch to end of line...
|
||||
for isPrefix {
|
||||
_, isPrefix, err = reader.ReadLine()
|
||||
_, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return blamePart, err
|
||||
}
|
||||
@@ -102,9 +100,9 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
func (r *BlameReader) Close() error {
|
||||
defer r.finished() // Only remove the process from the process table when the underlying command is closed
|
||||
r.cancel() // However, first cancel our own context early
|
||||
r.bufferedReader = nil
|
||||
|
||||
_ = r.output.Close()
|
||||
|
||||
_ = r.reader.Close()
|
||||
if err := r.cmd.Wait(); err != nil {
|
||||
return fmt.Errorf("Wait: %w", err)
|
||||
}
|
||||
@@ -126,25 +124,27 @@ func createBlameReader(ctx context.Context, dir string, command ...string) (*Bla
|
||||
cmd.Stderr = os.Stderr
|
||||
process.SetSysProcAttribute(cmd)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
reader, stdout, err := os.Pipe()
|
||||
if err != nil {
|
||||
defer finished()
|
||||
return nil, fmt.Errorf("StdoutPipe: %w", err)
|
||||
}
|
||||
cmd.Stdout = stdout
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
defer finished()
|
||||
_ = stdout.Close()
|
||||
return nil, fmt.Errorf("Start: %w", err)
|
||||
}
|
||||
_ = stdout.Close()
|
||||
|
||||
reader := bufio.NewReader(stdout)
|
||||
bufferedReader := bufio.NewReader(reader)
|
||||
|
||||
return &BlameReader{
|
||||
cmd: cmd,
|
||||
output: stdout,
|
||||
reader: reader,
|
||||
cancel: cancel,
|
||||
finished: finished,
|
||||
cmd: cmd,
|
||||
reader: reader,
|
||||
cancel: cancel,
|
||||
finished: finished,
|
||||
bufferedReader: bufferedReader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ summary Add code of delete user
|
||||
previous be0ba9ea88aff8a658d0495d36accf944b74888d gogs.go
|
||||
filename gogs.go
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
` + `
|
||||
e2aa991e10ffd924a828ec149951f2f20eecead2 6 6 2
|
||||
author Lunny Xiao
|
||||
author-mail <xiaolunwen@gmail.com>
|
||||
@@ -112,9 +112,7 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"ce21ed6c3490cdfad797319cbb1145e2330a8fef",
|
||||
[]string{
|
||||
"// Copyright 2016 The Gitea Authors. All rights reserved.",
|
||||
},
|
||||
[]string{"// Copyright 2016 The Gitea Authors. All rights reserved."},
|
||||
},
|
||||
{
|
||||
"4b92a6c2df28054ad766bc262f308db9f6066596",
|
||||
|
||||
@@ -132,7 +132,7 @@ func CommitChangesWithArgs(repoPath string, args []CmdArg, opts CommitChangesOpt
|
||||
if opts.Author != nil {
|
||||
cmd.AddArguments(CmdArg(fmt.Sprintf("--author='%s <%s>'", opts.Author.Name, opts.Author.Email)))
|
||||
}
|
||||
cmd.AddArguments("-m").AddDynamicArguments(opts.Message)
|
||||
cmd.AddArguments(CmdArg("--message=" + opts.Message))
|
||||
|
||||
_, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
|
||||
// No stderr but exit status 1 means nothing to commit.
|
||||
|
||||
@@ -313,7 +313,7 @@ func CheckGitVersionAtLeast(atLeast string) error {
|
||||
}
|
||||
|
||||
func configSet(key, value string) error {
|
||||
stdout, _, err := NewCommand(DefaultContext, "config", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err != nil && !err.IsExitCode(1) {
|
||||
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
|
||||
}
|
||||
@@ -332,7 +332,7 @@ func configSet(key, value string) error {
|
||||
}
|
||||
|
||||
func configSetNonExist(key, value string) error {
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err == nil {
|
||||
// already exist
|
||||
return nil
|
||||
@@ -350,7 +350,7 @@ func configSetNonExist(key, value string) error {
|
||||
}
|
||||
|
||||
func configAddNonExist(key, value string) error {
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil)
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil)
|
||||
if err == nil {
|
||||
// already exist
|
||||
return nil
|
||||
@@ -367,7 +367,7 @@ func configAddNonExist(key, value string) error {
|
||||
}
|
||||
|
||||
func configUnsetAll(key, value string) error {
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
|
||||
if err == nil {
|
||||
// exist, need to remove
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil)
|
||||
|
||||
@@ -164,10 +164,8 @@ func CloneWithArgs(ctx context.Context, args []CmdArg, from, to string, opts Clo
|
||||
|
||||
envs := os.Environ()
|
||||
u, err := url.Parse(from)
|
||||
if err == nil && (strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https")) {
|
||||
if proxy.Match(u.Host) {
|
||||
envs = append(envs, fmt.Sprintf("https_proxy=%s", proxy.GetProxyURL()))
|
||||
}
|
||||
if err == nil {
|
||||
envs = proxy.EnvWithProxy(u)
|
||||
}
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
@@ -282,11 +282,18 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
|
||||
|
||||
// GetFilesChangedBetween returns a list of all files that have been changed between the given commits
|
||||
func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) {
|
||||
stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(base + ".." + head).RunStdString(&RunOpts{Dir: repo.Path})
|
||||
stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", "-z").AddDynamicArguments(base + ".." + head).RunStdString(&RunOpts{Dir: repo.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strings.Split(stdout, "\n"), err
|
||||
split := strings.Split(stdout, "\000")
|
||||
|
||||
// Because Git will always emit filenames with a terminal NUL ignore the last entry in the split - which will always be empty.
|
||||
if len(split) > 0 {
|
||||
split = split[:len(split)-1]
|
||||
}
|
||||
|
||||
return split, err
|
||||
}
|
||||
|
||||
// GetDiffFromMergeBase generates and return patch data from merge base to head
|
||||
|
||||
@@ -100,6 +100,9 @@ func RefURL(repoURL, ref string) string {
|
||||
return repoURL + "/src/branch/" + refName
|
||||
case strings.HasPrefix(ref, TagPrefix):
|
||||
return repoURL + "/src/tag/" + refName
|
||||
case !IsValidSHAPattern(ref):
|
||||
// assume they mean a branch
|
||||
return repoURL + "/src/branch/" + refName
|
||||
default:
|
||||
return repoURL + "/src/commit/" + refName
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package lfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// DetermineEndpoint determines an endpoint from the clone url or uses the specified LFS url.
|
||||
@@ -96,7 +96,7 @@ func endpointFromLocalPath(path string) *url.URL {
|
||||
return nil
|
||||
}
|
||||
|
||||
path = fmt.Sprintf("file://%s%s", slash, filepath.ToSlash(path))
|
||||
path = "file://" + slash + util.PathEscapeSegments(filepath.ToSlash(path))
|
||||
|
||||
u, _ := url.Parse(path)
|
||||
|
||||
|
||||
@@ -384,6 +384,13 @@ func (cv *ColoredValue) Format(s fmt.State, c rune) {
|
||||
s.Write(*cv.resetBytes)
|
||||
}
|
||||
|
||||
// ColorFormatAsString returns the result of the ColorFormat without the color
|
||||
func ColorFormatAsString(colorVal ColorFormatted) string {
|
||||
s := new(strings.Builder)
|
||||
_, _ = ColorFprintf(&protectedANSIWriter{w: s, mode: removeColor}, "%-v", colorVal)
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// SetColorBytes will allow a user to set the colorBytes of a colored value
|
||||
func (cv *ColoredValue) SetColorBytes(colorBytes []byte) {
|
||||
cv.colorBytes = &colorBytes
|
||||
|
||||
@@ -7,7 +7,7 @@ package log
|
||||
import "unsafe"
|
||||
|
||||
//go:linkname runtime_getProfLabel runtime/pprof.runtime_getProfLabel
|
||||
func runtime_getProfLabel() unsafe.Pointer // nolint
|
||||
func runtime_getProfLabel() unsafe.Pointer //nolint
|
||||
|
||||
type labelMap map[string]string
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
)
|
||||
|
||||
type loggerMap struct {
|
||||
@@ -286,6 +288,15 @@ func (l *LoggerAsWriter) Log(msg string) {
|
||||
}
|
||||
|
||||
func init() {
|
||||
process.Trace = func(start bool, pid process.IDType, description string, parentPID process.IDType, typ string) {
|
||||
if start && parentPID != "" {
|
||||
Log(1, TRACE, "Start %s: %s (from %s) (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(parentPID, FgYellow), NewColoredValue(typ, Reset))
|
||||
} else if start {
|
||||
Log(1, TRACE, "Start %s: %s (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(typ, Reset))
|
||||
} else {
|
||||
Log(1, TRACE, "Done %s: %s", NewColoredValue(pid, FgHiYellow), NewColoredValue(description, Reset))
|
||||
}
|
||||
}
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
prefix = strings.TrimSuffix(filename, "modules/log/log.go")
|
||||
if prefix == filename {
|
||||
|
||||
5
modules/markup/external/external.go
vendored
5
modules/markup/external/external.go
vendored
@@ -5,6 +5,7 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -133,11 +134,13 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
|
||||
if !p.IsInputFile {
|
||||
cmd.Stdin = input
|
||||
}
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = output
|
||||
cmd.Stderr = &stderr
|
||||
process.SetSysProcAttribute(cmd)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("%s render run command %s %v failed: %w", p.Name(), commands[0], args, err)
|
||||
return fmt.Errorf("%s render run command %s %v failed: %w\nStderr: %s", p.Name(), commands[0], args, err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -358,12 +358,19 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
|
||||
}
|
||||
|
||||
func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node) {
|
||||
// Add user-content- to IDs if they don't already have them
|
||||
// Add user-content- to IDs and "#" links if they don't already have them
|
||||
for idx, attr := range node.Attr {
|
||||
if attr.Key == "id" && !(strings.HasPrefix(attr.Val, "user-content-") || blackfridayExtRegex.MatchString(attr.Val)) {
|
||||
val := strings.TrimPrefix(attr.Val, "#")
|
||||
notHasPrefix := !(strings.HasPrefix(val, "user-content-") || blackfridayExtRegex.MatchString(val))
|
||||
|
||||
if attr.Key == "id" && notHasPrefix {
|
||||
node.Attr[idx].Val = "user-content-" + attr.Val
|
||||
}
|
||||
|
||||
if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix {
|
||||
node.Attr[idx].Val = "#user-content-" + val
|
||||
}
|
||||
|
||||
if attr.Key == "class" && attr.Val == "emoji" {
|
||||
textProcs = nil
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ import "time"
|
||||
|
||||
// Commentable can be commented upon
|
||||
type Commentable interface {
|
||||
GetLocalIndex() int64
|
||||
GetForeignIndex() int64
|
||||
Reviewable
|
||||
GetContext() DownloaderContext
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,15 @@ func (issue *Issue) GetExternalName() string { return issue.PosterName }
|
||||
// GetExternalID ExternalUserMigrated interface
|
||||
func (issue *Issue) GetExternalID() int64 { return issue.PosterID }
|
||||
|
||||
func (issue *Issue) GetLocalIndex() int64 { return issue.Number }
|
||||
func (issue *Issue) GetForeignIndex() int64 { return issue.ForeignIndex }
|
||||
func (issue *Issue) GetLocalIndex() int64 { return issue.Number }
|
||||
|
||||
func (issue *Issue) GetForeignIndex() int64 {
|
||||
// see the comment of Reviewable.GetForeignIndex
|
||||
// if there is no ForeignIndex, then use LocalIndex
|
||||
if issue.ForeignIndex == 0 {
|
||||
return issue.Number
|
||||
}
|
||||
return issue.ForeignIndex
|
||||
}
|
||||
|
||||
func (issue *Issue) GetContext() DownloaderContext { return issue.Context }
|
||||
|
||||
@@ -9,6 +9,16 @@ import "time"
|
||||
// Reviewable can be reviewed
|
||||
type Reviewable interface {
|
||||
GetLocalIndex() int64
|
||||
|
||||
// GetForeignIndex presents the foreign index, which could be misused:
|
||||
// For example, if there are 2 Gitea sites: site-A exports a dataset, then site-B imports it:
|
||||
// * if site-A exports files by using its LocalIndex
|
||||
// * from site-A's view, LocalIndex is site-A's IssueIndex while ForeignIndex is site-B's IssueIndex
|
||||
// * but from site-B's view, LocalIndex is site-B's IssueIndex while ForeignIndex is site-A's IssueIndex
|
||||
//
|
||||
// So the exporting/importing must be paired, but the meaning of them looks confusing then:
|
||||
// * either site-A and site-B both use LocalIndex during dumping/restoring
|
||||
// * or site-A and site-B both use ForeignIndex
|
||||
GetForeignIndex() int64
|
||||
}
|
||||
|
||||
@@ -38,7 +48,7 @@ type Review struct {
|
||||
// GetExternalName ExternalUserMigrated interface
|
||||
func (r *Review) GetExternalName() string { return r.ReviewerName }
|
||||
|
||||
// ExternalID ExternalUserMigrated interface
|
||||
// GetExternalID ExternalUserMigrated interface
|
||||
func (r *Review) GetExternalID() int64 { return r.ReviewerID }
|
||||
|
||||
// ReviewComment represents a review comment
|
||||
|
||||
@@ -96,6 +96,7 @@ func (ns *notificationService) NotifyIssueChangeStatus(doer *user_model.User, is
|
||||
_ = ns.issueQueue.Push(issueNotificationOpts{
|
||||
IssueID: issue.ID,
|
||||
NotificationAuthorID: doer.ID,
|
||||
CommentID: actionComment.ID,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/packages/container/helm"
|
||||
"code.gitea.io/gitea/modules/packages/container/oci"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
|
||||
oci "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -66,8 +67,8 @@ type Metadata struct {
|
||||
}
|
||||
|
||||
// ParseImageConfig parses the metadata of an image config
|
||||
func ParseImageConfig(mediaType oci.MediaType, r io.Reader) (*Metadata, error) {
|
||||
if strings.EqualFold(string(mediaType), helm.ConfigMediaType) {
|
||||
func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) {
|
||||
if strings.EqualFold(mt, helm.ConfigMediaType) {
|
||||
return parseHelmConfig(r)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/packages/container/helm"
|
||||
"code.gitea.io/gitea/modules/packages/container/oci"
|
||||
|
||||
oci "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestParseImageConfig(t *testing.T) {
|
||||
|
||||
configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}`
|
||||
|
||||
metadata, err := ParseImageConfig(oci.MediaType(oci.MediaTypeImageManifest), strings.NewReader(configOCI))
|
||||
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, TypeOCI, metadata.Type)
|
||||
@@ -51,7 +51,7 @@ func TestParseImageConfig(t *testing.T) {
|
||||
|
||||
configHelm := `{"description":"` + description + `", "home": "` + projectURL + `", "sources": ["` + repositoryURL + `"], "maintainers":[{"name":"` + author + `"}]}`
|
||||
|
||||
metadata, err = ParseImageConfig(oci.MediaType(helm.ConfigMediaType), strings.NewReader(configHelm))
|
||||
metadata, err = ParseImageConfig(helm.ConfigMediaType, strings.NewReader(configHelm))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, TypeHelm, metadata.Type)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var digestPattern = regexp.MustCompile(`\Asha256:[a-f0-9]{64}\z`)
|
||||
|
||||
type Digest string
|
||||
|
||||
// Validate checks if the digest has a valid SHA256 signature
|
||||
func (d Digest) Validate() bool {
|
||||
return digestPattern.MatchString(string(d))
|
||||
}
|
||||
|
||||
func (d Digest) Hash() string {
|
||||
p := strings.SplitN(string(d), ":", 2)
|
||||
if len(p) != 2 {
|
||||
return ""
|
||||
}
|
||||
return p[1]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json"
|
||||
MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json"
|
||||
MediaTypeDockerManifest = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
MediaTypeDockerManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
|
||||
)
|
||||
|
||||
type MediaType string
|
||||
|
||||
// IsValid tests if the media type is in the OCI or Docker namespace
|
||||
func (m MediaType) IsValid() bool {
|
||||
s := string(m)
|
||||
return strings.HasPrefix(s, "application/vnd.docker.") || strings.HasPrefix(s, "application/vnd.oci.")
|
||||
}
|
||||
|
||||
// IsImageManifest tests if the media type is an image manifest
|
||||
func (m MediaType) IsImageManifest() bool {
|
||||
s := string(m)
|
||||
return strings.EqualFold(s, MediaTypeDockerManifest) || strings.EqualFold(s, MediaTypeImageManifest)
|
||||
}
|
||||
|
||||
// IsImageIndex tests if the media type is an image index
|
||||
func (m MediaType) IsImageIndex() bool {
|
||||
s := string(m)
|
||||
return strings.EqualFold(s, MediaTypeDockerManifestList) || strings.EqualFold(s, MediaTypeImageIndex)
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// https://github.com/opencontainers/image-spec/tree/main/specs-go/v1
|
||||
|
||||
// ImageConfig defines the execution parameters which should be used as a base when running a container using an image.
|
||||
type ImageConfig struct {
|
||||
// User defines the username or UID which the process in the container should run as.
|
||||
User string `json:"User,omitempty"`
|
||||
|
||||
// ExposedPorts a set of ports to expose from a container running this image.
|
||||
ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"`
|
||||
|
||||
// Env is a list of environment variables to be used in a container.
|
||||
Env []string `json:"Env,omitempty"`
|
||||
|
||||
// Entrypoint defines a list of arguments to use as the command to execute when the container starts.
|
||||
Entrypoint []string `json:"Entrypoint,omitempty"`
|
||||
|
||||
// Cmd defines the default arguments to the entrypoint of the container.
|
||||
Cmd []string `json:"Cmd,omitempty"`
|
||||
|
||||
// Volumes is a set of directories describing where the process is likely write data specific to a container instance.
|
||||
Volumes map[string]struct{} `json:"Volumes,omitempty"`
|
||||
|
||||
// WorkingDir sets the current working directory of the entrypoint process in the container.
|
||||
WorkingDir string `json:"WorkingDir,omitempty"`
|
||||
|
||||
// Labels contains arbitrary metadata for the container.
|
||||
Labels map[string]string `json:"Labels,omitempty"`
|
||||
|
||||
// StopSignal contains the system call signal that will be sent to the container to exit.
|
||||
StopSignal string `json:"StopSignal,omitempty"`
|
||||
}
|
||||
|
||||
// RootFS describes a layer content addresses
|
||||
type RootFS struct {
|
||||
// Type is the type of the rootfs.
|
||||
Type string `json:"type"`
|
||||
|
||||
// DiffIDs is an array of layer content hashes, in order from bottom-most to top-most.
|
||||
DiffIDs []string `json:"diff_ids"`
|
||||
}
|
||||
|
||||
// History describes the history of a layer.
|
||||
type History struct {
|
||||
// Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6.
|
||||
Created *time.Time `json:"created,omitempty"`
|
||||
|
||||
// CreatedBy is the command which created the layer.
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
|
||||
// Author is the author of the build point.
|
||||
Author string `json:"author,omitempty"`
|
||||
|
||||
// Comment is a custom message set when creating the layer.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
|
||||
// EmptyLayer is used to mark if the history item created a filesystem diff.
|
||||
EmptyLayer bool `json:"empty_layer,omitempty"`
|
||||
}
|
||||
|
||||
// Image is the JSON structure which describes some basic information about the image.
|
||||
// This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON.
|
||||
type Image struct {
|
||||
// Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6.
|
||||
Created *time.Time `json:"created,omitempty"`
|
||||
|
||||
// Author defines the name and/or email address of the person or entity which created and is responsible for maintaining the image.
|
||||
Author string `json:"author,omitempty"`
|
||||
|
||||
// Architecture is the CPU architecture which the binaries in this image are built to run on.
|
||||
Architecture string `json:"architecture"`
|
||||
|
||||
// Variant is the variant of the specified CPU architecture which image binaries are intended to run on.
|
||||
Variant string `json:"variant,omitempty"`
|
||||
|
||||
// OS is the name of the operating system which the image is built to run on.
|
||||
OS string `json:"os"`
|
||||
|
||||
// OSVersion is an optional field specifying the operating system
|
||||
// version, for example on Windows `10.0.14393.1066`.
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
|
||||
// OSFeatures is an optional field specifying an array of strings,
|
||||
// each listing a required OS feature (for example on Windows `win32k`).
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
|
||||
// Config defines the execution parameters which should be used as a base when running a container using the image.
|
||||
Config ImageConfig `json:"config,omitempty"`
|
||||
|
||||
// RootFS references the layer content addresses used by the image.
|
||||
RootFS RootFS `json:"rootfs"`
|
||||
|
||||
// History describes the history of each layer.
|
||||
History []History `json:"history,omitempty"`
|
||||
}
|
||||
|
||||
// Descriptor describes the disposition of targeted content.
|
||||
// This structure provides `application/vnd.oci.descriptor.v1+json` mediatype
|
||||
// when marshalled to JSON.
|
||||
type Descriptor struct {
|
||||
// MediaType is the media type of the object this schema refers to.
|
||||
MediaType MediaType `json:"mediaType,omitempty"`
|
||||
|
||||
// Digest is the digest of the targeted content.
|
||||
Digest Digest `json:"digest"`
|
||||
|
||||
// Size specifies the size in bytes of the blob.
|
||||
Size int64 `json:"size"`
|
||||
|
||||
// URLs specifies a list of URLs from which this object MAY be downloaded
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
|
||||
// Annotations contains arbitrary metadata relating to the targeted content.
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
|
||||
// Data is an embedding of the targeted content. This is encoded as a base64
|
||||
// string when marshalled to JSON (automatically, by encoding/json). If
|
||||
// present, Data can be used directly to avoid fetching the targeted content.
|
||||
Data []byte `json:"data,omitempty"`
|
||||
|
||||
// Platform describes the platform which the image in the manifest runs on.
|
||||
//
|
||||
// This should only be used when referring to a manifest.
|
||||
Platform *Platform `json:"platform,omitempty"`
|
||||
}
|
||||
|
||||
// Platform describes the platform which the image in the manifest runs on.
|
||||
type Platform struct {
|
||||
// Architecture field specifies the CPU architecture, for example
|
||||
// `amd64` or `ppc64`.
|
||||
Architecture string `json:"architecture"`
|
||||
|
||||
// OS specifies the operating system, for example `linux` or `windows`.
|
||||
OS string `json:"os"`
|
||||
|
||||
// OSVersion is an optional field specifying the operating system
|
||||
// version, for example on Windows `10.0.14393.1066`.
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
|
||||
// OSFeatures is an optional field specifying an array of strings,
|
||||
// each listing a required OS feature (for example on Windows `win32k`).
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
|
||||
// Variant is an optional field specifying a variant of the CPU, for
|
||||
// example `v7` to specify ARMv7 when architecture is `arm`.
|
||||
Variant string `json:"variant,omitempty"`
|
||||
}
|
||||
|
||||
type SchemaMediaBase struct {
|
||||
// SchemaVersion is the image manifest schema that this image follows
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
|
||||
// MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.manifest.v1+json`
|
||||
MediaType MediaType `json:"mediaType,omitempty"`
|
||||
}
|
||||
|
||||
// Manifest provides `application/vnd.oci.image.manifest.v1+json` mediatype structure when marshalled to JSON.
|
||||
type Manifest struct {
|
||||
SchemaMediaBase
|
||||
|
||||
// Config references a configuration object for a container, by digest.
|
||||
// The referenced configuration object is a JSON blob that the runtime uses to set up the container.
|
||||
Config Descriptor `json:"config"`
|
||||
|
||||
// Layers is an indexed list of layers referenced by the manifest.
|
||||
Layers []Descriptor `json:"layers"`
|
||||
|
||||
// Annotations contains arbitrary metadata for the image manifest.
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
// Index references manifests for various platforms.
|
||||
// This structure provides `application/vnd.oci.image.index.v1+json` mediatype when marshalled to JSON.
|
||||
type Index struct {
|
||||
SchemaMediaBase
|
||||
|
||||
// Manifests references platform specific manifests.
|
||||
Manifests []Descriptor `json:"manifests"`
|
||||
|
||||
// Annotations contains arbitrary metadata for the image index.
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
|
||||
|
||||
type Reference string
|
||||
|
||||
func (r Reference) Validate() bool {
|
||||
return referencePattern.MatchString(string(r))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user