forked from mirrors/gitea
Compare commits
182 Commits
main
...
release/v1
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 | ||
|
53db977e7e | ||
|
4fdd4fb2c4 | ||
|
900e158064 | ||
|
e9bc2c77c3 | ||
|
9b4da56963 | ||
|
5583eaa904 | ||
|
2a5e7f8f92 | ||
|
d2777444d9 | ||
|
198342efe4 | ||
|
f7258aa42b | ||
|
9a0a4086e2 | ||
|
145e11bc39 | ||
|
72524adf3f | ||
|
2d4083f03c | ||
|
56bded9d8d | ||
|
e88218f4be | ||
|
4297aced93 | ||
|
dd2343d01f | ||
|
9e49270676 | ||
|
194b780cd7 | ||
|
1409b348c6 | ||
|
c36a1bc766 | ||
|
079ef56824 | ||
|
b54c064f89 | ||
|
c0ca9c612b | ||
|
e39bb2d05a | ||
|
ac54331549 | ||
|
35fc9ad984 | ||
|
6e4ba04843 | ||
|
09794b4259 | ||
|
757b49ec5e | ||
|
9819a47717 | ||
|
c7770fa502 | ||
|
da956b863b | ||
|
888384a631 | ||
|
cddceb9dca | ||
|
b56d269cf8 | ||
|
ff4e292b3f | ||
|
9ba4ef93ff | ||
|
9bccc60cf5 | ||
|
16772ffde3 | ||
|
c844c4ff88 | ||
|
f4ec03a4e5 | ||
|
b2369830bb | ||
|
ef08998bf6 | ||
|
7a004ad7eb | ||
|
af8b2250c4 | ||
|
8917af8701 | ||
|
0d25292fbc | ||
|
ac409fcfba | ||
|
df512f77b7 | ||
|
e4bf9cad1e | ||
|
169eeee101 | ||
|
3aacc9b4ac | ||
|
87d05d376d | ||
|
b9dcf991b9 | ||
|
a2a42cd5de | ||
|
805a14cc91 | ||
|
69a54545a8 | ||
|
e054f80fe0 | ||
|
89d52922d0 | ||
|
3a0d000b94 | ||
|
fd4e7447e7 | ||
|
7a8e34b255 | ||
|
e4a10f8c78 | ||
|
6dba648e5d | ||
|
4d39fd8aae | ||
|
4869f9c3c8 | ||
|
79275d9db4 |
@ -5,6 +5,6 @@ tmp_dir = ".air"
|
||||
cmd = "make backend"
|
||||
bin = "gitea"
|
||||
include_ext = ["go", "tmpl"]
|
||||
exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata"]
|
||||
include_dir = ["cmd", "models", "modules", "options", "routers", "services", "templates"]
|
||||
exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"]
|
||||
include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
|
||||
exclude_regex = ["_test.go$", "_gen.go$"]
|
||||
|
45
.drone.yml
45
.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}"
|
||||
@ -941,7 +959,8 @@ steps:
|
||||
image: plugins/hugo:latest
|
||||
pull: always
|
||||
commands:
|
||||
- apk add --no-cache make bash curl
|
||||
# https://github.com/drone-plugins/drone-hugo/issues/36
|
||||
- apk upgrade --no-cache libcurl && apk add --no-cache make bash curl
|
||||
- cd docs
|
||||
- make trans-copy clean build
|
||||
|
||||
|
@ -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
|
||||
|
361
CHANGELOG.md
361
CHANGELOG.md
@ -4,6 +4,367 @@ 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
|
||||
* Remove ReverseProxy authentication from the API (#22219) (#22251)
|
||||
* Support Go Vulnerability Management (#21139)
|
||||
* Forbid HTML string tooltips (#20935)
|
||||
* BREAKING
|
||||
* Rework mailer settings (#18982)
|
||||
* Remove U2F support (#20141)
|
||||
* Refactor `i18n` to `locale` (#20153)
|
||||
* Enable contenthash in filename for dynamic assets (#20813)
|
||||
* FEATURES
|
||||
* Add color previews in markdown (#21474)
|
||||
* Allow package version sorting (#21453)
|
||||
* Add support for Chocolatey/NuGet v2 API (#21393)
|
||||
* Add API endpoint to get changed files of a PR (#21177)
|
||||
* Add filetree on left of diff view (#21012)
|
||||
* Support Issue forms and PR forms (#20987)
|
||||
* Add support for Vagrant packages (#20930)
|
||||
* Add support for `npm unpublish` (#20688)
|
||||
* Add badge capabilities to users (#20607)
|
||||
* Add issue filter for Author (#20578)
|
||||
* Add KaTeX rendering to Markdown. (#20571)
|
||||
* Add support for Pub packages (#20560)
|
||||
* Support localized README (#20508)
|
||||
* Add support mCaptcha as captcha provider (#20458)
|
||||
* Add team member invite by email (#20307)
|
||||
* Added email notification option to receive all own messages (#20179)
|
||||
* Switch Unicode Escaping to a VSCode-like system (#19990)
|
||||
* Add user/organization code search (#19977)
|
||||
* Only show relevant repositories on explore page (#19361)
|
||||
* User keypairs and HTTP signatures for ActivityPub federation using go-ap (#19133)
|
||||
* Add sitemap support (#18407)
|
||||
* Allow creation of OAuth2 applications for orgs (#18084)
|
||||
* Add system setting table with cache and also add cache supports for user setting (#18058)
|
||||
* Add pages to view watched repos and subscribed issues/PRs (#17156)
|
||||
* Support Proxy protocol (#12527)
|
||||
* Implement sync push mirror on commit (#19411)
|
||||
* API
|
||||
* Allow empty assignees on pull request edit (#22150) (#22214)
|
||||
* Make external issue tracker regexp configurable via API (#21338)
|
||||
* Add name field for org api (#21270)
|
||||
* Show teams with no members if user is admin (#21204)
|
||||
* Add latest commit's SHA to content response (#20398)
|
||||
* Add allow_rebase_update, default_delete_branch_after_merge to repository api response (#20079)
|
||||
* Add new endpoints for push mirrors management (#19841)
|
||||
* ENHANCEMENTS
|
||||
* Add setting to disable the git apply step in test patch (#22130) (#22170)
|
||||
* Multiple improvements for comment edit diff (#21990) (#22007)
|
||||
* Fix button in branch list, avoid unexpected page jump before restore branch actually done (#21562) (#21928)
|
||||
* Fix flex layout for repo list icons (#21896) (#21920)
|
||||
* Fix vertical align of committer avatar rendered by email address (#21884) (#21918)
|
||||
* Fix setting HTTP headers after write (#21833) (#21877)
|
||||
* Color and Style enhancements (#21784, #21799) (#21868)
|
||||
* Ignore line anchor links with leading zeroes (#21728) (#21776)
|
||||
* Quick fixes monaco-editor error: "vs.editor.nullLanguage" (#21734) (#21738)
|
||||
* Use CSS color-scheme instead of invert (#21616) (#21623)
|
||||
* Respect user's locale when rendering the date range in the repo activity page (#21410)
|
||||
* Change `commits-table` column width (#21564)
|
||||
* Refactor git command arguments and make all arguments to be safe to be used (#21535)
|
||||
* CSS color enhancements (#21534)
|
||||
* Add link to user profile in markdown mention only if user exists (#21533, #21554)
|
||||
* Add option to skip index dirs (#21501)
|
||||
* Diff file tree tweaks (#21446)
|
||||
* Localize all timestamps (#21440)
|
||||
* Add `code` highlighting in issue titles (#21432)
|
||||
* Use Name instead of DisplayName in LFS Lock (#21415)
|
||||
* Consolidate more CSS colors into variables (#21402)
|
||||
* Redirect to new repository owner (#21398)
|
||||
* Use ISO date format instead of hard-coded English date format for date range in repo activity page (#21396)
|
||||
* Use weighted algorithm for string matching when finding files in repo (#21370)
|
||||
* Show private data in feeds (#21369)
|
||||
* Refactor parseTreeEntries, speed up tree list (#21368)
|
||||
* Add GET and DELETE endpoints for Docker blob uploads (#21367)
|
||||
* Add nicer error handling on template compile errors (#21350)
|
||||
* Add `stat` to `ToCommit` function for speed (#21337)
|
||||
* Support instance-wide OAuth2 applications (#21335)
|
||||
* Record OAuth client type at registration (#21316)
|
||||
* Add new CSS variables --color-accent and --color-small-accent (#21305)
|
||||
* Improve error descriptions for unauthorized_client (#21292)
|
||||
* Case-insensitive "find files in repo" (#21269)
|
||||
* Consolidate more CSS rules, fix inline code on arc-green (#21260)
|
||||
* Log real ip of requests from ssh (#21216)
|
||||
* Save files in local storage as group readable (#21198)
|
||||
* Enable fluid page layout on medium size viewports (#21178)
|
||||
* File header tweaks (#21175)
|
||||
* Added missing headers on user packages page (#21172)
|
||||
* Display image digest for container packages (#21170)
|
||||
* Skip dirty check for team forms (#21154)
|
||||
* Keep path when creating a new branch (#21153)
|
||||
* Remove fomantic image module (#21145)
|
||||
* Make labels clickable in the comments section. (#21137)
|
||||
* Sort branches and tags by date descending (#21136)
|
||||
* Better repo API unit checks (#21130)
|
||||
* Improve commit status icons (#21124)
|
||||
* Limit length of repo description and repo url input fields (#21119)
|
||||
* Show .editorconfig errors in frontend (#21088)
|
||||
* Allow poster to choose reviewers (#21084)
|
||||
* Remove black labels and CSS cleanup (#21003)
|
||||
* Make e-mail sanity check more precise (#20991)
|
||||
* Use native inputs in whitespace dropdown (#20980)
|
||||
* Enhance package date display (#20928)
|
||||
* Display total blob size of a package version (#20927)
|
||||
* Show language name on hover (#20923)
|
||||
* Show instructions for all generic package files (#20917)
|
||||
* Refactor AssertExistsAndLoadBean to use generics (#20797)
|
||||
* Move the official website link at the footer of gitea (#20777)
|
||||
* Add support for full name in reverse proxy auth (#20776)
|
||||
* Remove useless JS operation for relative time tooltips (#20756)
|
||||
* Replace some icons with SVG (#20741)
|
||||
* Change commit status icons to SVG (#20736)
|
||||
* Improve single repo action for issue and pull requests (#20730)
|
||||
* Allow multiple files in generic packages (#20661)
|
||||
* Add option to create new issue from /issues page (#20650)
|
||||
* Background color of private list-items updated (#20630)
|
||||
* Added search input field to issue filter (#20623)
|
||||
* Increase default item listing size `ISSUE_PAGING_NUM` to 20 (#20547)
|
||||
* Modify milestone search keywords to be case insensitive again (#20513)
|
||||
* Show hint to link package to repo when viewing empty repo package list (#20504)
|
||||
* Add Tar ZSTD support (#20493)
|
||||
* Make code review checkboxes clickable (#20481)
|
||||
* Add "X-Gitea-Object-Type" header for GET `/raw/` & `/media/` API (#20438)
|
||||
* Display project in issue list (#20434)
|
||||
* Prepend commit message to template content when opening a new PR (#20429)
|
||||
* Replace fomantic popup module with tippy.js (#20428)
|
||||
* Allow to specify colors for text in markup (#20363)
|
||||
* Allow access to the Public Organization Member lists with minimal permissions (#20330)
|
||||
* Use default values when provided values are empty (#20318)
|
||||
* Vertical align navbar avatar at middle (#20302)
|
||||
* Delete cancel button in repo creation page (#21381)
|
||||
* Include login_name in adminCreateUser response (#20283)
|
||||
* fix: icon margin in user/settings/repos (#20281)
|
||||
* Remove blue text on migrate page (#20273)
|
||||
* Modify milestone search keywords to be case insensitive (#20266)
|
||||
* Move some files into models' sub packages (#20262)
|
||||
* Add tooltip to repo icons in explore page (#20241)
|
||||
* Remove deprecated licenses (#20222)
|
||||
* Webhook for Wiki changes (#20219)
|
||||
* Share HTML template renderers and create a watcher framework (#20218)
|
||||
* Allow enable LDAP source and disable user sync via CLI (#20206)
|
||||
* Adds a checkbox to select all issues/PRs (#20177)
|
||||
* Refactor `i18n` to `locale` (#20153)
|
||||
* Disable status checks in template if none found (#20088)
|
||||
* Allow manager logging to set SQL (#20064)
|
||||
* Add order by for assignee no sort issue (#20053)
|
||||
* Take a stab at porting existing components to Vue3 (#20044)
|
||||
* Add doctor command to write commit-graphs (#20007)
|
||||
* Add support for authentication based on reverse proxy email (#19949)
|
||||
* Enable spellcheck for EasyMDE, use contenteditable mode (#19776)
|
||||
* Allow specifying SECRET_KEY_URI, similar to INTERNAL_TOKEN_URI (#19663)
|
||||
* Rework mailer settings (#18982)
|
||||
* Add option to purge users (#18064)
|
||||
* Add author search input (#21246)
|
||||
* Make rss/atom identifier globally unique (#21550)
|
||||
* BUGFIXES
|
||||
* Auth interface return error when verify failure (#22119) (#22259)
|
||||
* Use complete SHA to create and query commit status (#22244) (#22257)
|
||||
* Update bleve and zapx to fix unaligned atomic (#22031) (#22218)
|
||||
* Prevent panic in doctor command when running default checks (#21791) (#21807)
|
||||
* Load GitRepo in API before deleting issue (#21720) (#21796)
|
||||
* Ignore line anchor links with leading zeroes (#21728) (#21776)
|
||||
* Set last login when activating account (#21731) (#21755)
|
||||
* Fix UI language switching bug (#21597) (#21749)
|
||||
* Quick fixes monaco-editor error: "vs.editor.nullLanguage" (#21734) (#21738)
|
||||
* Allow local package identifiers for PyPI packages (#21690) (#21727)
|
||||
* Deal with markdown template without metadata (#21639) (#21654)
|
||||
* Fix opaque background on mermaid diagrams (#21642) (#21652)
|
||||
* Fix repository adoption on Windows (#21646) (#21650)
|
||||
* Sync git hooks when config file path changed (#21619) (#21626)
|
||||
* Fix 500 on PR files API (#21602) (#21607)
|
||||
* Fix `Timestamp.IsZero` (#21593) (#21603)
|
||||
* Fix viewing user subscriptions (#21482)
|
||||
* Fix mermaid-related bugs (#21431)
|
||||
* Fix branch dropdown shifting on page load (#21428)
|
||||
* Fix default theme-auto selector when nologin (#21346)
|
||||
* Fix and improve incorrect error messages (#21342)
|
||||
* Fix formatted link for PR review notifications to matrix (#21319)
|
||||
* Center-aligning content of WebAuthN page (#21127)
|
||||
* Remove follow from commits by file (#20765)
|
||||
* Fix commit status popup (#20737)
|
||||
* Fix init mail render logic (#20704)
|
||||
* Use correct page size for link header pagination (#20546)
|
||||
* Preserve unix socket file (#20499)
|
||||
* Use tippy.js for context popup (#20393)
|
||||
* Add missing parameter for error in log message (#20144)
|
||||
* Do not allow organisation owners add themselves as collaborator (#20043)
|
||||
* Rework file highlight rendering and fix yaml copy-paste (#19967)
|
||||
* Improve code diff highlight, fix incorrect rendered diff result (#19958)
|
||||
* TESTING
|
||||
* Improve OAuth integration tests (#21390)
|
||||
* Add playwright tests (#20123)
|
||||
* BUILD
|
||||
* Switch to building with go1.19 (#20695)
|
||||
* Update JS dependencies, adjust eslint (#20659)
|
||||
* Add more linters to improve code readability (#19989)
|
||||
|
||||
## [1.17.4](https://github.com/go-gitea/gitea/releases/tag/1.17.4) - 2022-12-21
|
||||
|
||||
* SECURITY
|
||||
* Do not allow Ghost access to limited visible user/org (#21849) (#21875)
|
||||
* Fix package access for admins and inactive users (#21580) (#21592)
|
||||
* ENHANCEMENTS
|
||||
* Fix button in branch list, avoid unexpected page jump before restore branch actually done (#21562) (#21927)
|
||||
* Fix vertical align of committer avatar rendered by email address (#21884) (#21919)
|
||||
* Fix setting HTTP headers after write (#21833) (#21874)
|
||||
* Ignore line anchor links with leading zeroes (#21728) (#21777)
|
||||
* Enable Monaco automaticLayout (#21516)
|
||||
* BUGFIXES
|
||||
* Do not list active repositories as unadopted (#22034) (#22167)
|
||||
* Correctly handle moved files in apply patch (#22118) (#22136)
|
||||
* Fix condition for is_internal (#22095) (#22131)
|
||||
* Fix permission check on issue/pull lock (#22114)
|
||||
* Fix sorting admin user list by last login (#22081) (#22106)
|
||||
* Workaround for container registry push/pull errors (#21862) (#22069)
|
||||
* Fix issue/PR numbers (#22037) (#22045)
|
||||
* Handle empty author names (#21902) (#22028)
|
||||
* Fix ListBranches to handle empty case (#21921) (#22025)
|
||||
* Fix enabling partial clones on 1.17 (#21809)
|
||||
* Prevent panic in doctor command when running default checks (#21791) (#21808)
|
||||
* Upgrade golang.org/x/crypto (#21792) (#21794)
|
||||
* Init git module before database migration (#21764) (#21766)
|
||||
* Set last login when activating account (#21731) (#21754)
|
||||
* Add HEAD fix to gitea doctor (#21352) (#21751)
|
||||
* Fix UI language switching bug (#21597) (#21748)
|
||||
* Remove semver compatible flag and change pypi to an array of test cases (#21708) (#21729)
|
||||
* Allow local package identifiers for PyPI packages (#21690) (#21726)
|
||||
* Fix repository adoption on Windows (#21646) (#21651)
|
||||
* Sync git hooks when config file path changed (#21619) (#21625)
|
||||
* Added check for disabled Packages (#21540) (#21614)
|
||||
* Fix `Timestamp.IsZero` (#21593) (#21604)
|
||||
* Fix issues count bug (#21600)
|
||||
* Support binary deploy in npm packages (#21589)
|
||||
* Update milestone counters when issue is deleted (#21459) (#21586)
|
||||
* SessionUser protection against nil pointer dereference (#21581)
|
||||
* Case-insensitive NuGet symbol file GUID (#21409) (#21575)
|
||||
* Suppress `ExternalLoginUserNotExist` error (#21504) (#21572)
|
||||
* Prevent Authorization header for presigned LFS urls (#21531) (#21569)
|
||||
* Update binding to fix bugs (#21560)
|
||||
* Fix generating compare link (#21519) (#21530)
|
||||
* Ignore error when retrieving changed PR review files (#21487) (#21524)
|
||||
* Fix incorrect notification commit url (#21479) (#21483)
|
||||
* Display total commit count in hook message (#21400) (#21481)
|
||||
* Enforce grouped NuGet search results (#21442) (#21480)
|
||||
* Return 404 when user is not found on avatar (#21476) (#21477)
|
||||
* Normalize NuGet package version on upload (#22186) (#22201)
|
||||
* MISC
|
||||
* Check for zero time instant in TimeStamp.IsZero() (#22171) (#22173)
|
||||
* Fix warn in database structs sync (#22111)
|
||||
* Allow for resolution of NPM registry paths that match upstream (#21568) (#21723)
|
||||
|
||||
## [1.17.3](https://github.com/go-gitea/gitea/releases/tag/v1.17.3) - 2022-10-15
|
||||
|
||||
* SECURITY
|
||||
|
@ -31,6 +31,7 @@ EXPOSE 2222 3000
|
||||
RUN apk --no-cache add \
|
||||
bash \
|
||||
ca-certificates \
|
||||
dumb-init \
|
||||
gettext \
|
||||
git \
|
||||
curl \
|
||||
@ -68,6 +69,6 @@ ENV HOME "/var/lib/gitea/git"
|
||||
VOLUME ["/var/lib/gitea", "/etc/gitea"]
|
||||
WORKDIR /var/lib/gitea
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD []
|
||||
|
||||
|
19
Makefile
19
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
|
||||
@ -358,7 +358,7 @@ watch-frontend: node-check node_modules
|
||||
|
||||
.PHONY: watch-backend
|
||||
watch-backend: go-check
|
||||
$(GO) run $(AIR_PACKAGE) -c .air.toml
|
||||
GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
|
||||
|
||||
.PHONY: test
|
||||
test: test-frontend test-backend
|
||||
@ -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")
|
||||
|
411
cmd/admin.go
411
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: "",
|
||||
@ -413,9 +275,9 @@ var (
|
||||
Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "addr",
|
||||
Name: "host",
|
||||
Value: "",
|
||||
Usage: "SMTP Addr",
|
||||
Usage: "SMTP Host",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port",
|
||||
@ -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
|
||||
|
||||
@ -955,8 +574,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
|
||||
}
|
||||
conf.Auth = c.String("auth-type")
|
||||
}
|
||||
if c.IsSet("addr") {
|
||||
conf.Addr = c.String("addr")
|
||||
if c.IsSet("host") {
|
||||
conf.Host = c.String("host")
|
||||
}
|
||||
if c.IsSet("port") {
|
||||
conf.Port = c.Int("port")
|
||||
|
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)
|
||||
|
@ -996,6 +996,9 @@ ROUTER = console
|
||||
;;
|
||||
;; Add co-authored-by and co-committed-by trailers if committer does not match author
|
||||
;ADD_CO_COMMITTER_TRAILERS = true
|
||||
;;
|
||||
;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply
|
||||
;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = true
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@ -1550,7 +1553,7 @@ ROUTER = console
|
||||
;; Prefix displayed before subject in mail
|
||||
;SUBJECT_PREFIX =
|
||||
;;
|
||||
;; Mail server protocol. One of "smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy".
|
||||
;; Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy".
|
||||
;; - sendmail: use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems.
|
||||
;; - dummy: send email messages to the log as a testing phase.
|
||||
;; If your provider does not explicitly say which protocol it uses but does provide a port,
|
||||
|
@ -101,6 +101,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||
- `DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY`: **true**: In default merge messages only include approvers who are officially allowed to review.
|
||||
- `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`: **false**: In default squash-merge messages include the commit message of all commits comprising the pull request.
|
||||
- `ADD_CO_COMMITTER_TRAILERS`: **true**: Add co-authored-by and co-committed-by trailers to merge commit messages if committer does not match author.
|
||||
- `TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY`: **true**: PR patches are tested using a three-way merge method to discover if there are conflicts. If this setting is set to **true**, conflicting patches will be retested using `git apply` - This was the previous behaviour in 1.18 (and earlier) but is somewhat inefficient. Please report if you find that this setting is required.
|
||||
|
||||
### Repository - Issue (`repository.issue`)
|
||||
|
||||
@ -522,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):
|
||||
@ -672,7 +687,7 @@ and
|
||||
[Gitea 1.17 configuration document](https://github.com/go-gitea/gitea/blob/release/v1.17/docs/content/doc/advanced/config-cheat-sheet.en-us.md)
|
||||
|
||||
- `ENABLED`: **false**: Enable to use a mail service.
|
||||
- `PROTOCOL`: **\<empty\>**: Mail server protocol. One of "smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._
|
||||
- `PROTOCOL`: **\<empty\>**: Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._
|
||||
- SMTP family, if your provider does not explicitly say which protocol it uses but does provide a port, you can set SMTP_PORT instead and this will be inferred.
|
||||
- **sendmail** Use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems.
|
||||
- **dummy** Send email messages to the log as a testing phase.
|
||||
@ -734,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" >}})
|
||||
|
35
go.mod
35
go.mod
@ -15,8 +15,8 @@ require (
|
||||
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/alecthomas/chroma/v2 v2.3.0
|
||||
github.com/blevesearch/bleve/v2 v2.3.4
|
||||
github.com/alecthomas/chroma/v2 v2.4.0
|
||||
github.com/blevesearch/bleve/v2 v2.3.5
|
||||
github.com/buildkite/terminal-to-html/v3 v3.7.0
|
||||
github.com/caddyserver/certmagic v0.17.2
|
||||
github.com/chi-middleware/proxy v1.1.1
|
||||
@ -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
|
||||
@ -94,11 +96,11 @@ require (
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
go.jolheiser.com/hcaptcha v0.0.4
|
||||
go.jolheiser.com/pwn v0.0.3
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
|
||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc
|
||||
golang.org/x/crypto v0.2.1-0.20221112162523-6fad3dfc1891
|
||||
golang.org/x/net v0.2.0
|
||||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
|
||||
golang.org/x/text v0.3.8
|
||||
golang.org/x/sys v0.2.0
|
||||
golang.org/x/text v0.4.0
|
||||
golang.org/x/tools v0.1.12
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
@ -129,21 +131,21 @@ require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/speakeasy v0.1.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.3.3 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.0.3 // indirect
|
||||
github.com/blevesearch/geo v0.1.14 // indirect
|
||||
github.com/blevesearch/bleve_index_api v1.0.4 // indirect
|
||||
github.com/blevesearch/geo v0.1.15 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/gtreap v0.1.1 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.2 // indirect
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.3 // indirect
|
||||
github.com/blevesearch/segment v0.9.0 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.1 // indirect
|
||||
github.com/blevesearch/vellum v1.0.8 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.3.5 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.3.5 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.3.5 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.3.5 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.3.5 // indirect
|
||||
github.com/blevesearch/vellum v1.0.9 // indirect
|
||||
github.com/blevesearch/zapx/v11 v11.3.6 // indirect
|
||||
github.com/blevesearch/zapx/v12 v12.3.6 // indirect
|
||||
github.com/blevesearch/zapx/v13 v13.3.6 // indirect
|
||||
github.com/blevesearch/zapx/v14 v14.3.6 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.3.6 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||
@ -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,6 +305,8 @@ 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-2
|
||||
|
||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||
|
||||
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
||||
|
76
go.sum
76
go.sum
@ -149,7 +149,6 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
|
||||
github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
|
||||
github.com/RoaringBitmap/roaring v1.2.1 h1:58/LJlg/81wfEHd5L9qsHduznOIhyv4qb1yWcSvVq9A=
|
||||
github.com/RoaringBitmap/roaring v1.2.1/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
@ -160,9 +159,10 @@ github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/alecthomas/assert/v2 v2.2.0 h1:f6L/b7KE2bfA+9O4FL3CM/xJccDEwPVYd5fALBiuwvw=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.3.0 h1:83xfxrnjv8eK+Cf8qZDzNo3PPF9IbTWHs7z28GY6D0U=
|
||||
github.com/alecthomas/chroma/v2 v2.3.0/go.mod h1:mZxeWZlxP2Dy+/8cBob2PYd8O2DwNAzave5AY7A2eQw=
|
||||
github.com/alecthomas/chroma/v2 v2.4.0 h1:Loe2ZjT5x3q1bcWwemqyqEi8p11/IV/ncFCeLYDpWC4=
|
||||
github.com/alecthomas/chroma/v2 v2.4.0/go.mod h1:6kHzqF5O6FUSJzBXW7fXELjb+e+7OXW4UpoPqMO7IBQ=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
||||
@ -225,52 +225,47 @@ github.com/bits-and-blooms/bitset v1.3.3/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6fdVJ+R1Bk8LM=
|
||||
github.com/blevesearch/bleve/v2 v2.3.4 h1:SSb7/cwGzo85LWX1jchIsXM8ZiNNMX3shT5lROM63ew=
|
||||
github.com/blevesearch/bleve/v2 v2.3.4/go.mod h1:Ot0zYum8XQRfPcwhae8bZmNyYubynsoMjVvl1jPqL30=
|
||||
github.com/blevesearch/bleve/v2 v2.3.5 h1:1wuR7eB8Fk9UaCaBUfnQt5V7zIpi4VDok9ExN7Rl+/8=
|
||||
github.com/blevesearch/bleve/v2 v2.3.5/go.mod h1:FneKGHMRrCLrp4X9+iy3wlBqgM2ALucg7bp8jUuAi/s=
|
||||
github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
|
||||
github.com/blevesearch/bleve_index_api v1.0.3 h1:DDSWaPXOZZJ2BB73ZTWjKxydAugjwywcqU+91AAqcAg=
|
||||
github.com/blevesearch/bleve_index_api v1.0.3/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4=
|
||||
github.com/blevesearch/geo v0.1.13/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc=
|
||||
github.com/blevesearch/geo v0.1.14 h1:TTDpJN6l9ck/cUYbXSn4aCElNls0Whe44rcQKsB7EfU=
|
||||
github.com/blevesearch/geo v0.1.14/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc=
|
||||
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A=
|
||||
github.com/blevesearch/bleve_index_api v1.0.4 h1:mtlzsyJjMIlDngqqB1mq8kPryUMIuEVVbRbJHOWEexU=
|
||||
github.com/blevesearch/bleve_index_api v1.0.4/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms=
|
||||
github.com/blevesearch/geo v0.1.15 h1:0NybEduqE5fduFRYiUKF0uqybAIFKXYjkBdXKYn7oA4=
|
||||
github.com/blevesearch/geo v0.1.15/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ=
|
||||
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||
github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.2 h1:TAte9VZLWda5WAVlZTTZ+GCzEHqGJb4iB2aiZSA6Iv8=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.2/go.mod h1:rvoQXZGq8drq7vXbNeyiRzdEOwZkjkiYGf1822i6CRA=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.3 h1:2UzpR2dR5DvSZk8tVJkcQ7D5xhoK/UBelYw8ttBHrRQ=
|
||||
github.com/blevesearch/scorch_segment_api/v2 v2.1.3/go.mod h1:eZrfp1y+lUh+DzFjUcTBUSnKGuunyFIpBIvqYVzJfvc=
|
||||
github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac=
|
||||
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
|
||||
github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.1 h1:1SYRwyoFLwG3sj0ed89RLtM15amfX2pXlYbFOnF8zNU=
|
||||
github.com/blevesearch/upsidedown_store_api v1.0.1/go.mod h1:MQDVGpHZrpe3Uy26zJBf/a8h0FZY6xJbthIMm8myH2Q=
|
||||
github.com/blevesearch/vellum v1.0.3/go.mod h1:2u5ax02KeDuNWu4/C+hVQMD6uLN4txH1JbtpaDNLJRo=
|
||||
github.com/blevesearch/vellum v1.0.4/go.mod h1:cMhywHI0de50f7Nj42YgvyD6bFJ2WkNRvNBlNMrEVgY=
|
||||
github.com/blevesearch/vellum v1.0.8 h1:iMGh4lfxza4BnWO/UJTMPlI3HsK9YawjPv+TteVa9ck=
|
||||
github.com/blevesearch/vellum v1.0.8/go.mod h1:+cpRi/tqq49xUYSQN2P7A5zNSNrS+MscLeeaZ3J46UA=
|
||||
github.com/blevesearch/vellum v1.0.9 h1:PL+NWVk3dDGPCV0hoDu9XLLJgqU4E5s/dOeEJByQ2uQ=
|
||||
github.com/blevesearch/vellum v1.0.9/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
|
||||
github.com/blevesearch/zapx/v11 v11.2.0/go.mod h1:gN/a0alGw1FZt/YGTo1G6Z6XpDkeOfujX5exY9sCQQM=
|
||||
github.com/blevesearch/zapx/v11 v11.3.5 h1:eBQWQ7huA+mzm0sAGnZDwgGGli7S45EO+N+ObFWssbI=
|
||||
github.com/blevesearch/zapx/v11 v11.3.5/go.mod h1:5UdIa/HRMdeRCiLQOyFESsnqBGiip7vQmYReA9toevU=
|
||||
github.com/blevesearch/zapx/v11 v11.3.6 h1:50jET4HUJ6eCqGxdhUt+mjybMvEX2MWyqLGtCx3yUgc=
|
||||
github.com/blevesearch/zapx/v11 v11.3.6/go.mod h1:B0CzJRj/pS7hJIroflRtFsa9mRHpMSucSgre0FVINns=
|
||||
github.com/blevesearch/zapx/v12 v12.2.0/go.mod h1:fdjwvCwWWwJW/EYTYGtAp3gBA0geCYGLcVTtJEZnY6A=
|
||||
github.com/blevesearch/zapx/v12 v12.3.5 h1:5pX2hU+R1aZihT7ac1dNWh1n4wqkIM9pZzWp0ANED9s=
|
||||
github.com/blevesearch/zapx/v12 v12.3.5/go.mod h1:ANcthYRZQycpbRut/6ArF5gP5HxQyJqiFcuJCBju/ss=
|
||||
github.com/blevesearch/zapx/v12 v12.3.6 h1:G304NHBLgQeZ+IHK/XRCM0nhHqAts8MEvHI6LhoDNM4=
|
||||
github.com/blevesearch/zapx/v12 v12.3.6/go.mod h1:iYi7tIKpauwU5os5wTxJITixr5Km21Hl365otMwdaP0=
|
||||
github.com/blevesearch/zapx/v13 v13.2.0/go.mod h1:o5rAy/lRS5JpAbITdrOHBS/TugWYbkcYZTz6VfEinAQ=
|
||||
github.com/blevesearch/zapx/v13 v13.3.5 h1:eJ3gbD+Nu8p36/O6lhfdvWQ4pxsGYSuTOBrLLPVWJ74=
|
||||
github.com/blevesearch/zapx/v13 v13.3.5/go.mod h1:FV+dRnScFgKnRDIp08RQL4JhVXt1x2HE3AOzqYa6fjo=
|
||||
github.com/blevesearch/zapx/v13 v13.3.6 h1:vavltQHNdjQezhLZs5nIakf+w/uOa1oqZxB58Jy/3Ig=
|
||||
github.com/blevesearch/zapx/v13 v13.3.6/go.mod h1:X+FsTwCU8qOHtK0d/ArvbOH7qiIgViSQ1GQvcR6LSkI=
|
||||
github.com/blevesearch/zapx/v14 v14.2.0/go.mod h1:GNgZusc1p4ot040cBQMRGEZobvwjCquiEKYh1xLFK9g=
|
||||
github.com/blevesearch/zapx/v14 v14.3.5 h1:hEvVjZaagFCvOUJrlFQ6/Z6Jjy0opM3g7TMEo58TwP4=
|
||||
github.com/blevesearch/zapx/v14 v14.3.5/go.mod h1:954A/eKFb+pg/ncIYWLWCKY+mIjReM9FGTGIO2Wu1cU=
|
||||
github.com/blevesearch/zapx/v14 v14.3.6 h1:b9lub7TvcwUyJxK/cQtnN79abngKxsI7zMZnICU0WhE=
|
||||
github.com/blevesearch/zapx/v14 v14.3.6/go.mod h1:9X8W3XoikagU0rwcTqwZho7p9cC7m7zhPZO94S4wUvM=
|
||||
github.com/blevesearch/zapx/v15 v15.2.0/go.mod h1:MmQceLpWfME4n1WrBFIwplhWmaQbQqLQARpaKUEOs/A=
|
||||
github.com/blevesearch/zapx/v15 v15.3.5 h1:NVD0qq8vRk66ImJn1KloXT5ckqPDUZT7VbVJs9jKlac=
|
||||
github.com/blevesearch/zapx/v15 v15.3.5/go.mod h1:QMUh2hXCaYIWFKPYGavq/Iga2zbHWZ9DZAa9uFbWyvg=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
@ -361,7 +356,6 @@ github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67/go.mod h1:BQwMFl
|
||||
github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401 h1:4KDlx3vjalrHD/EfsjCpV91HNX3JPaIqRtt83zZ7x+Y=
|
||||
github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
|
||||
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
|
||||
github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
@ -830,6 +824,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
@ -1179,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=
|
||||
@ -1487,6 +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-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=
|
||||
@ -1608,8 +1609,8 @@ golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.2.1-0.20221112162523-6fad3dfc1891 h1:WhEPFM1Ck5gaKybeSWvzI7Y/cd8K9K5tJGRxXMACOBA=
|
||||
golang.org/x/crypto v0.2.1-0.20221112162523-6fad3dfc1891/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -1721,8 +1722,8 @@ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
|
||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -1762,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=
|
||||
@ -1876,13 +1878,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1892,8 +1894,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -272,7 +272,7 @@ func (a *Action) GetRefLink() string {
|
||||
return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix))
|
||||
case strings.HasPrefix(a.RefName, git.TagPrefix):
|
||||
return a.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.TagPrefix))
|
||||
case len(a.RefName) == 40 && git.IsValidSHAPattern(a.RefName):
|
||||
case len(a.RefName) == git.SHAFullLength && git.IsValidSHAPattern(a.RefName):
|
||||
return a.GetRepoLink() + "/src/commit/" + a.RefName
|
||||
default:
|
||||
// FIXME: we will just assume it's a branch - this was the old way - at some point we may want to enforce that there is always a ref here.
|
||||
|
@ -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.
|
||||
|
@ -20,8 +20,12 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
|
||||
const DefaultAvatarPixelSize = 28
|
||||
const (
|
||||
// DefaultAvatarClass is the default class of a rendered avatar
|
||||
DefaultAvatarClass = "ui avatar vm"
|
||||
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
|
||||
DefaultAvatarPixelSize = 28
|
||||
)
|
||||
|
||||
// EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records)
|
||||
type EmailHash struct {
|
||||
@ -150,10 +154,10 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
|
||||
return DefaultAvatarLink()
|
||||
}
|
||||
|
||||
enableFederatedAvatar, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar)
|
||||
enableFederatedAvatar := system_model.GetSettingBool(system_model.KeyPictureEnableFederatedAvatar)
|
||||
|
||||
var err error
|
||||
if enableFederatedAvatar != nil && enableFederatedAvatar.GetValueBool() && system_model.LibravatarService != nil {
|
||||
if enableFederatedAvatar && system_model.LibravatarService != nil {
|
||||
emailHash := saveEmailHash(email)
|
||||
if final {
|
||||
// for final link, we can spend more time on slow external query
|
||||
@ -171,8 +175,8 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
|
||||
return urlStr
|
||||
}
|
||||
|
||||
disableGravatar, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
|
||||
if disableGravatar != nil && !disableGravatar.GetValueBool() {
|
||||
disableGravatar := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
|
||||
if !disableGravatar {
|
||||
// copy GravatarSourceURL, because we will modify its Path.
|
||||
avatarURLCopy := *system_model.GravatarSourceURL
|
||||
avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email))
|
||||
|
@ -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 {
|
||||
|
@ -8,6 +8,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// ResourceIndex represents a resource index which could be used as issue/release and others
|
||||
@ -24,11 +27,6 @@ var (
|
||||
ErrGetResourceIndexFailed = errors.New("get resource index failed")
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxDupIndexAttempts max retry times to create index
|
||||
MaxDupIndexAttempts = 3
|
||||
)
|
||||
|
||||
// SyncMaxResourceIndex sync the max index with the resource
|
||||
func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxIndex int64) (err error) {
|
||||
e := GetEngine(ctx)
|
||||
@ -61,8 +59,25 @@ func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxInd
|
||||
return nil
|
||||
}
|
||||
|
||||
func postgresGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) {
|
||||
res, err := GetEngine(ctx).Query(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+
|
||||
"VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1 RETURNING max_index",
|
||||
tableName, tableName), groupID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(res) == 0 {
|
||||
return 0, ErrGetResourceIndexFailed
|
||||
}
|
||||
return strconv.ParseInt(string(res[0]["max_index"]), 10, 64)
|
||||
}
|
||||
|
||||
// GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created
|
||||
func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) {
|
||||
if setting.Database.UsePostgreSQL {
|
||||
return postgresGetNextResourceIndex(ctx, tableName, groupID)
|
||||
}
|
||||
|
||||
e := GetEngine(ctx)
|
||||
|
||||
// try to update the max_index to next value, and acquire the write-lock for the record
|
||||
|
@ -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
|
||||
|
@ -7,8 +7,10 @@ package git
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -49,79 +51,67 @@ func init() {
|
||||
db.RegisterModel(new(CommitStatusIndex))
|
||||
}
|
||||
|
||||
// upsertCommitStatusIndex the function will not return until it acquires the lock or receives an error.
|
||||
func upsertCommitStatusIndex(ctx context.Context, repoID int64, sha string) (err error) {
|
||||
// An atomic UPSERT operation (INSERT/UPDATE) is the only operation
|
||||
// that ensures that the key is actually locked.
|
||||
switch {
|
||||
case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL:
|
||||
_, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
|
||||
"VALUES (?,?,1) ON CONFLICT (repo_id,sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1",
|
||||
repoID, sha)
|
||||
case setting.Database.UseMySQL:
|
||||
_, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
|
||||
"VALUES (?,?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1",
|
||||
repoID, sha)
|
||||
case setting.Database.UseMSSQL:
|
||||
// https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
|
||||
_, err = db.Exec(ctx, "MERGE `commit_status_index` WITH (HOLDLOCK) as target "+
|
||||
"USING (SELECT ? AS repo_id, ? AS sha) AS src "+
|
||||
"ON src.repo_id = target.repo_id AND src.sha = target.sha "+
|
||||
"WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 "+
|
||||
"WHEN NOT MATCHED THEN INSERT (repo_id, sha, max_index) "+
|
||||
"VALUES (src.repo_id, src.sha, 1);",
|
||||
repoID, sha)
|
||||
default:
|
||||
return fmt.Errorf("database type not supported")
|
||||
func postgresGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
|
||||
res, err := db.GetEngine(ctx).Query("INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
|
||||
"VALUES (?,?,1) ON CONFLICT (repo_id, sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1 RETURNING max_index",
|
||||
repoID, sha)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return err
|
||||
if len(res) == 0 {
|
||||
return 0, db.ErrGetResourceIndexFailed
|
||||
}
|
||||
return strconv.ParseInt(string(res[0]["max_index"]), 10, 64)
|
||||
}
|
||||
|
||||
// GetNextCommitStatusIndex retried 3 times to generate a resource index
|
||||
func GetNextCommitStatusIndex(repoID int64, sha string) (int64, error) {
|
||||
for i := 0; i < db.MaxDupIndexAttempts; i++ {
|
||||
idx, err := getNextCommitStatusIndex(repoID, sha)
|
||||
if err == db.ErrResouceOutdated {
|
||||
continue
|
||||
}
|
||||
func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
|
||||
if setting.Database.UsePostgreSQL {
|
||||
return postgresGetCommitStatusIndex(ctx, repoID, sha)
|
||||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
// try to update the max_index to next value, and acquire the write-lock for the record
|
||||
res, err := e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if affected == 0 {
|
||||
// this slow path is only for the first time of creating a resource index
|
||||
_, errIns := e.Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) VALUES (?, ?, 0)", repoID, sha)
|
||||
res, err = e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return idx, nil
|
||||
}
|
||||
return 0, db.ErrGetResourceIndexFailed
|
||||
}
|
||||
|
||||
// getNextCommitStatusIndex return the next index
|
||||
func getNextCommitStatusIndex(repoID int64, sha string) (int64, error) {
|
||||
ctx, commiter, err := db.TxContext()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer commiter.Close()
|
||||
|
||||
var preIdx int64
|
||||
_, err = db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?", repoID, sha).Get(&preIdx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
affected, err = res.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// if the update still can not update any records, the record must not exist and there must be some errors (insert error)
|
||||
if affected == 0 {
|
||||
if errIns == nil {
|
||||
return 0, errors.New("impossible error when GetNextCommitStatusIndex, insert and update both succeeded but no record is updated")
|
||||
}
|
||||
return 0, errIns
|
||||
}
|
||||
}
|
||||
|
||||
if err := upsertCommitStatusIndex(ctx, repoID, sha); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var curIdx int64
|
||||
has, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ? AND max_index=?", repoID, sha, preIdx+1).Get(&curIdx)
|
||||
// now, the new index is in database (protected by the transaction and write-lock)
|
||||
var newIdx int64
|
||||
has, err := e.SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id=? AND sha=?", repoID, sha).Get(&newIdx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !has {
|
||||
return 0, db.ErrResouceOutdated
|
||||
return 0, errors.New("impossible error when GetNextCommitStatusIndex, upsert succeeded but no record can be selected")
|
||||
}
|
||||
if err := commiter.Commit(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return curIdx, nil
|
||||
return newIdx, nil
|
||||
}
|
||||
|
||||
func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) {
|
||||
@ -291,10 +281,8 @@ func NewCommitStatus(opts NewCommitStatusOptions) error {
|
||||
return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA)
|
||||
}
|
||||
|
||||
// Get the next Status Index
|
||||
idx, err := GetNextCommitStatusIndex(opts.Repo.ID, opts.SHA)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate commit status index failed: %w", err)
|
||||
if _, err := git.NewIDFromString(opts.SHA); err != nil {
|
||||
return fmt.Errorf("NewCommitStatus[%s, %s]: invalid sha: %w", repoPath, opts.SHA, err)
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
@ -303,6 +291,12 @@ func NewCommitStatus(opts NewCommitStatusOptions) error {
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
// Get the next Status Index
|
||||
idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate commit status index failed: %w", err)
|
||||
}
|
||||
|
||||
opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description)
|
||||
opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
|
||||
opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
|
||||
@ -316,7 +310,7 @@ func NewCommitStatus(opts NewCommitStatusOptions) error {
|
||||
|
||||
// Insert new CommitStatus
|
||||
if _, err = db.GetEngine(ctx).Insert(opts.CommitStatus); err != nil {
|
||||
return fmt.Errorf("Insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err)
|
||||
return fmt.Errorf("insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err)
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
|
@ -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)
|
||||
}
|
@ -1010,12 +1010,7 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
|
||||
}
|
||||
}
|
||||
|
||||
if opts.IsPull {
|
||||
_, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID)
|
||||
} else {
|
||||
_, err = e.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", opts.Issue.RepoID)
|
||||
}
|
||||
if err != nil {
|
||||
if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -2471,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)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@ -47,33 +48,42 @@ func (t *TrackedTime) LoadAttributes() (err error) {
|
||||
}
|
||||
|
||||
func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) {
|
||||
// Load the issue
|
||||
if t.Issue == nil {
|
||||
t.Issue, err = GetIssueByID(ctx, t.IssueID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = t.Issue.LoadRepo(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if t.User == nil {
|
||||
t.User, err = user_model.GetUserByIDCtx(ctx, t.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadAttributes load Issue, User
|
||||
func (tl TrackedTimeList) LoadAttributes() (err error) {
|
||||
for _, t := range tl {
|
||||
if err = t.LoadAttributes(); err != nil {
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
// Now load the repo for the issue (which we may have just loaded)
|
||||
if t.Issue != nil {
|
||||
err = t.Issue.LoadRepo(ctx)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Load the user
|
||||
if t.User == nil {
|
||||
t.User, err = user_model.GetUserByIDCtx(ctx, t.UserID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
t.User = user_model.NewGhostUser()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAttributes load Issue, User
|
||||
func (tl TrackedTimeList) LoadAttributes() error {
|
||||
for _, t := range tl {
|
||||
if err := t.LoadAttributes(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
@ -513,6 +514,13 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
|
||||
return nil
|
||||
}
|
||||
|
||||
// Some migration tasks depend on the git command
|
||||
if git.DefaultContext == nil {
|
||||
if err = git.InitSimple(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate
|
||||
for i, m := range migrations[v-minDBVersion:] {
|
||||
log.Info("Migration[%d]: %s", v+int64(i), m.Description())
|
||||
|
@ -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
|
||||
@ -458,8 +459,9 @@ func CountOrgs(opts FindOrgOptions) (int64, error) {
|
||||
|
||||
// HasOrgOrUserVisible tells if the given user can see the given org or user
|
||||
func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
|
||||
// Not SignedUser
|
||||
if user == nil {
|
||||
// If user is nil, it's an anonymous user/request.
|
||||
// The Ghost user is handled like an anonymous user.
|
||||
if user == nil || user.IsGhost() {
|
||||
return orgOrUser.Visibility == structs.VisibleTypePublic
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -305,7 +305,7 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
|
||||
|
||||
sess := db.GetEngine(ctx).
|
||||
Table("package_version").
|
||||
Join("LEFT", "package_version pv2", "package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))").
|
||||
Join("LEFT", "package_version pv2", "package_version.package_id = pv2.package_id AND pv2.is_internal = ? AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))", false).
|
||||
Join("INNER", "package", "package.id = package_version.package_id").
|
||||
Where(cond)
|
||||
|
||||
|
@ -444,7 +444,7 @@ func CheckRepoStats(ctx context.Context) error {
|
||||
},
|
||||
// Repository.NumIssues
|
||||
{
|
||||
statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", false, false),
|
||||
statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_pull=?)", false),
|
||||
repoStatsCorrectNumIssues,
|
||||
"repository count 'num_issues'",
|
||||
},
|
||||
@ -456,7 +456,7 @@ func CheckRepoStats(ctx context.Context) error {
|
||||
},
|
||||
// Repository.NumPulls
|
||||
{
|
||||
statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", false, true),
|
||||
statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_pull=?)", true),
|
||||
repoStatsCorrectNumPulls,
|
||||
"repository count 'num_pulls'",
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -12,7 +12,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
setting_module "code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"strk.kbt.io/projects/go/libravatar"
|
||||
@ -35,6 +36,10 @@ func (s *Setting) TableName() string {
|
||||
}
|
||||
|
||||
func (s *Setting) GetValueBool() bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
b, _ := strconv.ParseBool(s.SettingValue)
|
||||
return b
|
||||
}
|
||||
@ -75,8 +80,8 @@ func IsErrDataExpired(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetSetting returns specific setting
|
||||
func GetSetting(key string) (*Setting, error) {
|
||||
// GetSettingNoCache returns specific setting without using the cache
|
||||
func GetSettingNoCache(key string) (*Setting, error) {
|
||||
v, err := GetSettings([]string{key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -84,7 +89,26 @@ func GetSetting(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) (string, error) {
|
||||
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
|
||||
res, err := GetSettingNoCache(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.SettingValue, nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetSettingBool return bool value of setting,
|
||||
// none existing keys and errors are ignored and result in false
|
||||
func GetSettingBool(key string) bool {
|
||||
s, _ := GetSetting(key)
|
||||
v, _ := strconv.ParseBool(s)
|
||||
return v
|
||||
}
|
||||
|
||||
// GetSettings returns specific settings
|
||||
@ -108,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{}
|
||||
@ -139,12 +163,13 @@ func GetAllSettings() (AllSettings, error) {
|
||||
|
||||
// DeleteSetting deletes a specific setting for a user
|
||||
func DeleteSetting(setting *Setting) error {
|
||||
cache.Remove(genSettingCacheKey(setting.SettingKey))
|
||||
_, err := db.GetEngine(db.DefaultContext).Delete(setting)
|
||||
return err
|
||||
}
|
||||
|
||||
func SetSettingNoVersion(key, value string) error {
|
||||
s, err := GetSetting(key)
|
||||
s, err := GetSettingNoCache(key)
|
||||
if IsErrSettingIsNotExist(err) {
|
||||
return SetSetting(&Setting{
|
||||
SettingKey: key,
|
||||
@ -163,7 +188,14 @@ func SetSetting(setting *Setting) error {
|
||||
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
|
||||
}
|
||||
|
||||
@ -213,9 +245,9 @@ var (
|
||||
|
||||
func Init() error {
|
||||
var disableGravatar bool
|
||||
disableGravatarSetting, err := GetSetting(KeyPictureDisableGravatar)
|
||||
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
|
||||
@ -224,9 +256,9 @@ func Init() error {
|
||||
}
|
||||
|
||||
var enableFederatedAvatar bool
|
||||
enableFederatedAvatarSetting, err := GetSetting(KeyPictureEnableFederatedAvatar)
|
||||
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
|
||||
@ -234,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)
|
||||
|
@ -9,3 +9,8 @@ const (
|
||||
KeyPictureDisableGravatar = "picture.disable_gravatar"
|
||||
KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar"
|
||||
)
|
||||
|
||||
// genSettingCacheKey returns the cache key for some configuration
|
||||
func genSettingCacheKey(key string) string {
|
||||
return "system.setting." + key
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -89,6 +89,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
|
||||
&user_model.UserBadge{UserID: u.ID},
|
||||
&pull_model.AutoMerge{DoerID: u.ID},
|
||||
&pull_model.ReviewState{UserID: u.ID},
|
||||
&user_model.Redirect{RedirectUserID: u.ID},
|
||||
); err != nil {
|
||||
return fmt.Errorf("deleteBeans: %w", err)
|
||||
}
|
||||
|
@ -68,11 +68,7 @@ func (u *User) AvatarLinkWithSize(size int) string {
|
||||
useLocalAvatar := false
|
||||
autoGenerateAvatar := false
|
||||
|
||||
var disableGravatar bool
|
||||
disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar)
|
||||
if disableGravatarSetting != nil {
|
||||
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})
|
||||
}
|
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@ -47,9 +48,25 @@ func IsErrUserSettingIsNotExist(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetSetting returns specific setting
|
||||
func GetSetting(uid int64, key string) (*Setting, error) {
|
||||
v, err := GetUserSettings(uid, []string{key})
|
||||
// genSettingCacheKey returns the cache key for some configuration
|
||||
func genSettingCacheKey(userID int64, key string) string {
|
||||
return fmt.Sprintf("user_%d.setting.%s", userID, key)
|
||||
}
|
||||
|
||||
// GetSetting returns the setting value via the key
|
||||
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 "", err
|
||||
}
|
||||
return res.SettingValue, nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetSettingNoCache returns specific setting without using the cache
|
||||
func GetSettingNoCache(uid int64, key string) (*Setting, error) {
|
||||
v, err := GetSettings(uid, []string{key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -59,8 +76,8 @@ func GetSetting(uid int64, key string) (*Setting, error) {
|
||||
return v[key], nil
|
||||
}
|
||||
|
||||
// GetUserSettings returns specific settings from user
|
||||
func GetUserSettings(uid int64, keys []string) (map[string]*Setting, error) {
|
||||
// GetSettings returns specific settings from user
|
||||
func GetSettings(uid int64, keys []string) (map[string]*Setting, error) {
|
||||
settings := make([]*Setting, 0, len(keys))
|
||||
if err := db.GetEngine(db.DefaultContext).
|
||||
Where("user_id=?", uid).
|
||||
@ -105,6 +122,7 @@ func GetUserSetting(userID int64, key string, def ...string) (string, error) {
|
||||
if err := validateUserSettingKey(key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
setting := &Setting{UserID: userID, SettingKey: key}
|
||||
has, err := db.GetEngine(db.DefaultContext).Get(setting)
|
||||
if err != nil {
|
||||
@ -124,7 +142,10 @@ func DeleteUserSetting(userID int64, key string) error {
|
||||
if err := validateUserSettingKey(key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache.Remove(genSettingCacheKey(userID, key))
|
||||
_, err := db.GetEngine(db.DefaultContext).Delete(&Setting{UserID: userID, SettingKey: key})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -133,7 +154,12 @@ func SetUserSetting(userID int64, key, value string) error {
|
||||
if err := validateUserSettingKey(key); err != nil {
|
||||
return err
|
||||
}
|
||||
return upsertUserSettingValue(userID, key, value)
|
||||
|
||||
_, err := cache.GetString(genSettingCacheKey(userID, key), func() (string, error) {
|
||||
return value, upsertUserSettingValue(userID, key, value)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func upsertUserSettingValue(userID int64, key, value string) error {
|
||||
|
@ -27,7 +27,7 @@ func TestSettings(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// get specific setting
|
||||
settings, err := user_model.GetUserSettings(99, []string{keyName})
|
||||
settings, err := user_model.GetSettings(99, []string{keyName})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, settings, 1)
|
||||
assert.EqualValues(t, newSetting.SettingValue, settings[keyName].SettingValue)
|
||||
|
@ -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++ {
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
// GetKeyPair function returns a user's private and public keys
|
||||
func GetKeyPair(user *user_model.User) (pub, priv string, err error) {
|
||||
var settings map[string]*user_model.Setting
|
||||
settings, err = user_model.GetUserSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem})
|
||||
settings, err = user_model.GetSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem})
|
||||
if err != nil {
|
||||
return
|
||||
} else if len(settings) == 0 {
|
||||
|
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>")
|
98
modules/cache/cache.go
vendored
98
modules/cache/cache.go
vendored
@ -51,27 +51,26 @@ func GetString(key string, getFunc func() (string, error)) (string, error) {
|
||||
if conn == nil || setting.CacheService.TTL == 0 {
|
||||
return getFunc()
|
||||
}
|
||||
if !conn.IsExist(key) {
|
||||
var (
|
||||
value string
|
||||
err error
|
||||
)
|
||||
if value, err = getFunc(); err != nil {
|
||||
|
||||
cached := conn.Get(key)
|
||||
|
||||
if cached == nil {
|
||||
value, err := getFunc()
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
err = conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
}
|
||||
value := conn.Get(key)
|
||||
if v, ok := value.(string); ok {
|
||||
return v, nil
|
||||
|
||||
if value, ok := cached.(string); ok {
|
||||
return value, nil
|
||||
}
|
||||
if v, ok := value.(fmt.Stringer); ok {
|
||||
return v.String(), nil
|
||||
|
||||
if stringer, ok := cached.(fmt.Stringer); ok {
|
||||
return stringer.String(), nil
|
||||
}
|
||||
return fmt.Sprintf("%s", conn.Get(key)), nil
|
||||
|
||||
return fmt.Sprintf("%s", cached), nil
|
||||
}
|
||||
|
||||
// GetInt returns key value from cache with callback when no key exists in cache
|
||||
@ -79,30 +78,33 @@ func GetInt(key string, getFunc func() (int, error)) (int, error) {
|
||||
if conn == nil || setting.CacheService.TTL == 0 {
|
||||
return getFunc()
|
||||
}
|
||||
if !conn.IsExist(key) {
|
||||
var (
|
||||
value int
|
||||
err error
|
||||
)
|
||||
if value, err = getFunc(); err != nil {
|
||||
|
||||
cached := conn.Get(key)
|
||||
|
||||
if cached == nil {
|
||||
value, err := getFunc()
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
err = conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
}
|
||||
switch value := conn.Get(key).(type) {
|
||||
|
||||
switch v := cached.(type) {
|
||||
case int:
|
||||
return value, nil
|
||||
return v, nil
|
||||
case string:
|
||||
v, err := strconv.Atoi(value)
|
||||
value, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v, nil
|
||||
return value, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("Unsupported cached value type: %v", value)
|
||||
value, err := getFunc()
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,30 +113,34 @@ func GetInt64(key string, getFunc func() (int64, error)) (int64, error) {
|
||||
if conn == nil || setting.CacheService.TTL == 0 {
|
||||
return getFunc()
|
||||
}
|
||||
if !conn.IsExist(key) {
|
||||
var (
|
||||
value int64
|
||||
err error
|
||||
)
|
||||
if value, err = getFunc(); err != nil {
|
||||
|
||||
cached := conn.Get(key)
|
||||
|
||||
if cached == nil {
|
||||
value, err := getFunc()
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
err = conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
}
|
||||
switch value := conn.Get(key).(type) {
|
||||
|
||||
switch v := conn.Get(key).(type) {
|
||||
case int64:
|
||||
return value, nil
|
||||
return v, nil
|
||||
case string:
|
||||
v, err := strconv.ParseInt(value, 10, 64)
|
||||
value, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v, nil
|
||||
return value, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("Unsupported cached value type: %v", value)
|
||||
value, err := getFunc()
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
|
||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,12 @@ func AmbiguousTablesForLocale(locale translation.Locale) []*AmbiguousTable {
|
||||
key = key[:idx]
|
||||
}
|
||||
}
|
||||
if table == nil && (locale.Language() == "zh-CN" || locale.Language() == "zh_CN") {
|
||||
table = AmbiguousCharacters["zh-hans"]
|
||||
}
|
||||
if table == nil && strings.HasPrefix(locale.Language(), "zh") {
|
||||
table = AmbiguousCharacters["zh-hant"]
|
||||
}
|
||||
if table == nil {
|
||||
table = AmbiguousCharacters["_default"]
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
package charset
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
@ -32,7 +33,7 @@ func EscapeControlHTML(text string, locale translation.Locale, allowed ...rune)
|
||||
return streamer.escaped, sb.String()
|
||||
}
|
||||
|
||||
// EscapeControlReaders escapes the unicode control sequences in a provider reader and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
|
||||
// EscapeControlReaders escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
|
||||
func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
|
||||
outputStream := &HTMLStreamerWriter{Writer: writer}
|
||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
||||
@ -44,6 +45,31 @@ 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. 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}
|
||||
streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer)
|
||||
|
||||
for {
|
||||
line, rdErr := bufRd.ReadString('\n')
|
||||
if len(line) > 0 {
|
||||
if err := streamer.Text(line); err != nil {
|
||||
streamer.escaped.HasError = true
|
||||
log.Error("Error whilst escaping: %v", err)
|
||||
return streamer.escaped, err
|
||||
}
|
||||
}
|
||||
if rdErr != nil {
|
||||
if rdErr != io.EOF {
|
||||
err = rdErr
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return streamer.escaped, err
|
||||
}
|
||||
|
||||
// EscapeControlString escapes the unicode control sequences in a provided string and returns the findings as an EscapeStatus and the escaped string
|
||||
func EscapeControlString(text string, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output string) {
|
||||
sb := &strings.Builder{}
|
||||
|
@ -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"
|
||||
|
@ -220,7 +220,13 @@ func (ctx *APIContext) CheckForOTP() {
|
||||
func APIAuth(authMethod auth_service.Method) func(*APIContext) {
|
||||
return func(ctx *APIContext) {
|
||||
// Get user from session if logged in.
|
||||
ctx.Doer = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
var err error
|
||||
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Doer != nil {
|
||||
if ctx.Locale.Language() != ctx.Doer.Language {
|
||||
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
||||
@ -388,7 +394,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
||||
} else if len(refName) == 40 {
|
||||
} else if len(refName) == git.SHAFullLength {
|
||||
ctx.Repo.CommitID = refName
|
||||
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
|
||||
if err != nil {
|
||||
|
@ -34,6 +34,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
@ -322,9 +323,9 @@ func (ctx *Context) plainTextInternal(skip, status int, bs []byte) {
|
||||
if statusPrefix == 4 || statusPrefix == 5 {
|
||||
log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs))
|
||||
}
|
||||
ctx.Resp.WriteHeader(status)
|
||||
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
||||
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
ctx.Resp.WriteHeader(status)
|
||||
if _, err := ctx.Resp.Write(bs); err != nil {
|
||||
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
|
||||
}
|
||||
@ -345,34 +346,61 @@ func (ctx *Context) RespHeader() http.Header {
|
||||
return ctx.Resp.Header()
|
||||
}
|
||||
|
||||
type ServeHeaderOptions struct {
|
||||
ContentType string // defaults to "application/octet-stream"
|
||||
ContentTypeCharset string
|
||||
ContentLength *int64
|
||||
Disposition string // defaults to "attachment"
|
||||
Filename string
|
||||
CacheDuration time.Duration // defaults to 5 minutes
|
||||
LastModified time.Time
|
||||
}
|
||||
|
||||
// SetServeHeaders sets necessary content serve headers
|
||||
func (ctx *Context) SetServeHeaders(filename string) {
|
||||
ctx.Resp.Header().Set("Content-Description", "File Transfer")
|
||||
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
|
||||
ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+filename)
|
||||
ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
|
||||
ctx.Resp.Header().Set("Expires", "0")
|
||||
ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
|
||||
ctx.Resp.Header().Set("Pragma", "public")
|
||||
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
|
||||
header := ctx.Resp.Header()
|
||||
|
||||
contentType := typesniffer.ApplicationOctetStream
|
||||
if opts.ContentType != "" {
|
||||
if opts.ContentTypeCharset != "" {
|
||||
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
|
||||
} else {
|
||||
contentType = opts.ContentType
|
||||
}
|
||||
}
|
||||
header.Set("Content-Type", contentType)
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
if opts.ContentLength != nil {
|
||||
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
|
||||
}
|
||||
|
||||
if opts.Filename != "" {
|
||||
disposition := opts.Disposition
|
||||
if disposition == "" {
|
||||
disposition = "attachment"
|
||||
}
|
||||
|
||||
backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
|
||||
header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
|
||||
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
}
|
||||
|
||||
duration := opts.CacheDuration
|
||||
if duration == 0 {
|
||||
duration = 5 * time.Minute
|
||||
}
|
||||
httpcache.AddCacheControlToHeader(header, duration)
|
||||
|
||||
if !opts.LastModified.IsZero() {
|
||||
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
}
|
||||
|
||||
// ServeContent serves content to http request
|
||||
func (ctx *Context) ServeContent(name string, r io.ReadSeeker, modTime time.Time) {
|
||||
ctx.SetServeHeaders(name)
|
||||
http.ServeContent(ctx.Resp, ctx.Req, name, modTime, r)
|
||||
}
|
||||
|
||||
// ServeFile serves given file to response.
|
||||
func (ctx *Context) ServeFile(file string, names ...string) {
|
||||
var name string
|
||||
if len(names) > 0 {
|
||||
name = names[0]
|
||||
} else {
|
||||
name = path.Base(file)
|
||||
}
|
||||
ctx.SetServeHeaders(name)
|
||||
http.ServeFile(ctx.Resp, ctx.Req, file)
|
||||
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
|
||||
ctx.SetServeHeaders(opts)
|
||||
http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
|
||||
}
|
||||
|
||||
// UploadStream returns the request body or the first form file
|
||||
@ -635,7 +663,13 @@ func getCsrfOpts() CsrfOptions {
|
||||
// Auth converts auth.Auth as a middleware
|
||||
func Auth(authMethod auth.Method) func(*Context) {
|
||||
return func(ctx *Context) {
|
||||
ctx.Doer = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
var err error
|
||||
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
if err != nil {
|
||||
log.Error("Failed to verify user %v: %v", ctx.Req.RemoteAddr, err)
|
||||
ctx.Error(http.StatusUnauthorized, "Verify")
|
||||
return
|
||||
}
|
||||
if ctx.Doer != nil {
|
||||
if ctx.Locale.Language() != ctx.Doer.Language {
|
||||
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -816,7 +816,7 @@ func getRefName(ctx *Context, pathType RepoRefType) string {
|
||||
}
|
||||
// For legacy and API support only full commit sha
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 && len(parts[0]) == 40 {
|
||||
if len(parts) > 0 && len(parts[0]) == git.SHAFullLength {
|
||||
ctx.Repo.TreePath = strings.Join(parts[1:], "/")
|
||||
return parts[0]
|
||||
}
|
||||
@ -852,7 +852,7 @@ func getRefName(ctx *Context, pathType RepoRefType) string {
|
||||
return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsTagExist)
|
||||
case RepoRefCommit:
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= 40 {
|
||||
if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= git.SHAFullLength {
|
||||
ctx.Repo.TreePath = strings.Join(parts[1:], "/")
|
||||
return parts[0]
|
||||
}
|
||||
@ -961,7 +961,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
||||
return
|
||||
}
|
||||
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
||||
} else if len(refName) >= 7 && len(refName) <= 40 {
|
||||
} else if len(refName) >= 7 && len(refName) <= git.SHAFullLength {
|
||||
ctx.Repo.IsViewCommit = true
|
||||
ctx.Repo.CommitID = refName
|
||||
|
||||
@ -971,7 +971,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
||||
return
|
||||
}
|
||||
// If short commit ID add canonical link header
|
||||
if len(refName) < 40 {
|
||||
if len(refName) < git.SHAFullLength {
|
||||
ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"",
|
||||
util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))))
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -110,12 +110,11 @@ func ToAPIIssueList(il issues_model.IssueList) []*api.Issue {
|
||||
// ToTrackedTime converts TrackedTime to API format
|
||||
func ToTrackedTime(t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
|
||||
apiT = &api.TrackedTime{
|
||||
ID: t.ID,
|
||||
IssueID: t.IssueID,
|
||||
UserID: t.UserID,
|
||||
UserName: t.User.Name,
|
||||
Time: t.Time,
|
||||
Created: t.Created,
|
||||
ID: t.ID,
|
||||
IssueID: t.IssueID,
|
||||
UserID: t.UserID,
|
||||
Time: t.Time,
|
||||
Created: t.Created,
|
||||
}
|
||||
if t.Issue != nil {
|
||||
apiT.Issue = ToAPIIssue(t.Issue)
|
||||
|
@ -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)
|
||||
|
@ -205,6 +205,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
|
||||
// find stopwatches without existing issue
|
||||
genericOrphanCheck("Orphaned Stopwatches without existing Issue",
|
||||
"stopwatch", "issue", "stopwatch.issue_id=`issue`.id"),
|
||||
// find redirects without existing user.
|
||||
genericOrphanCheck("Orphaned Redirects without existing redirect user",
|
||||
"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
|
||||
)
|
||||
|
||||
for _, c := range consistencyChecks {
|
||||
|
@ -19,11 +19,9 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool)
|
||||
numReposUpdated := 0
|
||||
err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
|
||||
numRepos++
|
||||
runOpts := &git.RunOpts{Dir: repo.RepoPath()}
|
||||
_, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
|
||||
|
||||
_, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(runOpts)
|
||||
|
||||
head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(runOpts)
|
||||
head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
|
||||
|
||||
// what we expect: default branch is valid, and HEAD points to it
|
||||
if headErr == nil && defaultBranchErr == nil && head == repo.DefaultBranch {
|
||||
@ -49,7 +47,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool)
|
||||
}
|
||||
|
||||
// otherwise, let's try fixing HEAD
|
||||
err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", repo.DefaultBranch).Run(runOpts)
|
||||
err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", git.BranchPrefix+repo.DefaultBranch).Run(&git.RunOpts{Dir: repo.RepoPath()})
|
||||
if err != nil {
|
||||
logger.Warn("Failed to fix HEAD for %s/%s: %v", repo.OwnerName, repo.Name, err)
|
||||
return nil
|
||||
@ -65,7 +63,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool)
|
||||
logger.Info("Out of %d repos, HEADs for %d are now fixed and HEADS for %d are still broken", numRepos, numReposUpdated, numDefaultBranchesBroken+numHeadsBroken-numReposUpdated)
|
||||
} else {
|
||||
if numHeadsBroken == 0 && numDefaultBranchesBroken == 0 {
|
||||
logger.Info("All %d repos have their HEADs in the correct state")
|
||||
logger.Info("All %d repos have their HEADs in the correct state", numRepos)
|
||||
} else {
|
||||
if numHeadsBroken == 0 && numDefaultBranchesBroken != 0 {
|
||||
logger.Critical("Default branches are broken for %d/%d repos", numDefaultBranchesBroken, numRepos)
|
||||
|
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",
|
||||
|
@ -202,8 +202,11 @@ func (c *Command) Run(opts *RunOpts) error {
|
||||
if opts == nil {
|
||||
opts = &RunOpts{}
|
||||
}
|
||||
if opts.Timeout <= 0 {
|
||||
opts.Timeout = defaultCommandExecutionTimeout
|
||||
|
||||
// We must not change the provided options
|
||||
timeout := opts.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultCommandExecutionTimeout
|
||||
}
|
||||
|
||||
if len(opts.Dir) == 0 {
|
||||
@ -238,7 +241,7 @@ func (c *Command) Run(opts *RunOpts) error {
|
||||
if opts.UseContextTimeout {
|
||||
ctx, cancel, finished = process.GetManager().AddContext(c.parentContext, desc)
|
||||
} else {
|
||||
ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, opts.Timeout, desc)
|
||||
ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, timeout, desc)
|
||||
}
|
||||
defer finished()
|
||||
|
||||
@ -339,9 +342,20 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
|
||||
}
|
||||
stdoutBuf := &bytes.Buffer{}
|
||||
stderrBuf := &bytes.Buffer{}
|
||||
opts.Stdout = stdoutBuf
|
||||
opts.Stderr = stderrBuf
|
||||
err := c.Run(opts)
|
||||
|
||||
// We must not change the provided options as it could break future calls - therefore make a copy.
|
||||
newOpts := &RunOpts{
|
||||
Env: opts.Env,
|
||||
Timeout: opts.Timeout,
|
||||
UseContextTimeout: opts.UseContextTimeout,
|
||||
Dir: opts.Dir,
|
||||
Stdout: stdoutBuf,
|
||||
Stderr: stderrBuf,
|
||||
Stdin: opts.Stdin,
|
||||
PipelineFunc: opts.PipelineFunc,
|
||||
}
|
||||
|
||||
err := c.Run(newOpts)
|
||||
stderr = stderrBuf.Bytes()
|
||||
if err != nil {
|
||||
return nil, stderr, &runStdError{err: err, stderr: bytesToString(stderr)}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user