Merge remote-tracking branch 'upstream/forgejo' into forgejo
Some checks failed
testing / backend-checks (push) Successful in 18m5s
testing / frontend-checks (push) Successful in 1m45s
testing / test-unit (push) Successful in 13m47s
testing / test-mysql (push) Successful in 30m20s
testing / test-sqlite (push) Has been cancelled
testing / test-pgsql (push) Has been cancelled
/ release (push) Has been cancelled
Integration tests for the release process / release-simulation (push) Has been cancelled

This commit is contained in:
Anthony Lawn 2024-03-28 22:31:28 -05:00
commit f56408b8a9
549 changed files with 9074 additions and 10126 deletions

View file

@ -8,6 +8,15 @@ delay = 1000
include_ext = ["go", "tmpl"] include_ext = ["go", "tmpl"]
include_file = ["main.go"] include_file = ["main.go"]
include_dir = ["cmd", "models", "modules", "options", "routers", "services"] include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
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"] exclude_dir = [
"models/fixtures",
"models/migrations/fixtures",
"modules/avatar/identicon/testdata",
"modules/avatar/testdata",
"modules/git/tests",
"modules/migration/file_format_testdata",
"routers/private/tests",
"services/gitdiff/testdata",
]
exclude_regex = ["_test.go$", "_gen.go$"] exclude_regex = ["_test.go$", "_gen.go$"]
stop_on_error = true stop_on_error = true

View file

@ -271,13 +271,19 @@ package "code.gitea.io/gitea/modules/sync"
package "code.gitea.io/gitea/modules/testlogger" package "code.gitea.io/gitea/modules/testlogger"
func (*testLoggerWriterCloser).pushT func (*testLoggerWriterCloser).pushT
func (*testLoggerWriterCloser).Write func (*testLoggerWriterCloser).Log
func (*testLoggerWriterCloser).recordError
func (*testLoggerWriterCloser).printMsg
func (*testLoggerWriterCloser).popT func (*testLoggerWriterCloser).popT
func (*testLoggerWriterCloser).Close
func (*testLoggerWriterCloser).Reset func (*testLoggerWriterCloser).Reset
func PrintCurrentTest func PrintCurrentTest
func Printf func Printf
func NewTestLoggerWriter func NewTestLoggerWriter
func (*TestLogEventWriter).Base
func (*TestLogEventWriter).GetLevel
func (*TestLogEventWriter).GetWriterName
func (*TestLogEventWriter).GetWriterType
func (*TestLogEventWriter).Run
package "code.gitea.io/gitea/modules/timeutil" package "code.gitea.io/gitea/modules/timeutil"
func GetExecutableModTime func GetExecutableModTime
@ -323,7 +329,6 @@ package "code.gitea.io/gitea/services/pull"
package "code.gitea.io/gitea/services/repository" package "code.gitea.io/gitea/services/repository"
func IsErrForkAlreadyExist func IsErrForkAlreadyExist
func UpdateRepositoryUnits
package "code.gitea.io/gitea/services/repository/archiver" package "code.gitea.io/gitea/services/repository/archiver"
func ArchiveRepository func ArchiveRepository
@ -336,4 +341,5 @@ package "code.gitea.io/gitea/services/repository/files"
package "code.gitea.io/gitea/services/webhook" package "code.gitea.io/gitea/services/webhook"
func NewNotifier func NewNotifier
func List

View file

@ -4,7 +4,7 @@
"features": { "features": {
// installs nodejs into container // installs nodejs into container
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version":"20" "version": "20"
}, },
"ghcr.io/devcontainers/features/git-lfs:1.1.0": {}, "ghcr.io/devcontainers/features/git-lfs:1.1.0": {},
"ghcr.io/devcontainers-contrib/features/poetry:2": {}, "ghcr.io/devcontainers-contrib/features/poetry:2": {},
@ -24,7 +24,7 @@
"DavidAnson.vscode-markdownlint", "DavidAnson.vscode-markdownlint",
"Vue.volar", "Vue.volar",
"ms-azuretools.vscode-docker", "ms-azuretools.vscode-docker",
"zixuanchen.vitest-explorer", "vitest.explorer",
"qwtel.sqlite-viewer", "qwtel.sqlite-viewer",
"GitHub.vscode-pull-request-github" "GitHub.vscode-pull-request-github"
] ]

View file

@ -62,7 +62,6 @@ cpu.out
/data /data
/indexers /indexers
/log /log
/public/img/avatar
/tests/integration/gitea-integration-* /tests/integration/gitea-integration-*
/tests/integration/indexers-* /tests/integration/indexers-*
/tests/e2e/gitea-e2e-* /tests/e2e/gitea-e2e-*
@ -77,6 +76,7 @@ cpu.out
/public/assets/js /public/assets/js
/public/assets/css /public/assets/css
/public/assets/fonts /public/assets/fonts
/public/assets/img/avatar
/public/assets/img/webpack /public/assets/img/webpack
/vendor /vendor
/web_src/fomantic/node_modules /web_src/fomantic/node_modules

View file

@ -42,10 +42,6 @@ overrides:
worker: true worker: true
rules: rules:
no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top] no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
- files: ["build/generate-images.js"]
rules:
i/no-unresolved: [0]
i/no-extraneous-dependencies: [0]
- files: ["*.config.*"] - files: ["*.config.*"]
rules: rules:
i/no-unused-modules: [0] i/no-unused-modules: [0]
@ -123,7 +119,7 @@ rules:
"@stylistic/js/arrow-spacing": [2, {before: true, after: true}] "@stylistic/js/arrow-spacing": [2, {before: true, after: true}]
"@stylistic/js/block-spacing": [0] "@stylistic/js/block-spacing": [0]
"@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}] "@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}]
"@stylistic/js/comma-dangle": [2, only-multiline] "@stylistic/js/comma-dangle": [2, always-multiline]
"@stylistic/js/comma-spacing": [2, {before: false, after: true}] "@stylistic/js/comma-spacing": [2, {before: false, after: true}]
"@stylistic/js/comma-style": [2, last] "@stylistic/js/comma-style": [2, last]
"@stylistic/js/computed-property-spacing": [2, never] "@stylistic/js/computed-property-spacing": [2, never]
@ -290,7 +286,7 @@ rules:
jquery/no-class: [0] jquery/no-class: [0]
jquery/no-clone: [2] jquery/no-clone: [2]
jquery/no-closest: [0] jquery/no-closest: [0]
jquery/no-css: [0] jquery/no-css: [2]
jquery/no-data: [0] jquery/no-data: [0]
jquery/no-deferred: [2] jquery/no-deferred: [2]
jquery/no-delegate: [2] jquery/no-delegate: [2]
@ -413,7 +409,7 @@ rules:
no-jquery/no-constructor-attributes: [2] no-jquery/no-constructor-attributes: [2]
no-jquery/no-contains: [2] no-jquery/no-contains: [2]
no-jquery/no-context-prop: [2] no-jquery/no-context-prop: [2]
no-jquery/no-css: [0] no-jquery/no-css: [2]
no-jquery/no-data: [0] no-jquery/no-data: [0]
no-jquery/no-deferred: [2] no-jquery/no-deferred: [2]
no-jquery/no-delegate: [2] no-jquery/no-delegate: [2]

View file

@ -34,10 +34,14 @@ jobs:
!startsWith(vars.ROLE, 'forgejo-') && ( !startsWith(vars.ROLE, 'forgejo-') && (
github.event.pull_request.merged github.event.pull_request.merged
&& ( && (
github.event.action == 'closed' (
|| ( github.event.action == 'closed' &&
github.event.action == 'labeled' contains(toJSON(github.event.pull_request.labels), 'backport/v')
&& contains(github.event.label.name, 'backport/') )
||
(
github.event.action == 'labeled' &&
contains(github.event.label.name, 'backport/v')
) )
) )
) )
@ -54,7 +58,7 @@ jobs:
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update -qq apt-get update -qq
apt-get -q install -qq -y jq apt-get -q install -qq -y jq
filtered_labels=$(echo "$LABELS" | jq -c 'map(select(.name | startswith("backport/")))') filtered_labels=$(echo "$LABELS" | jq -c 'map(select(.name | startswith("backport/v")))')
echo "FILTERED_LABELS=${filtered_labels}" >> $GITHUB_ENV echo "FILTERED_LABELS=${filtered_labels}" >> $GITHUB_ENV
env: env:
LABELS: ${{ toJSON(github.event.pull_request.labels) }} LABELS: ${{ toJSON(github.event.pull_request.labels) }}

View file

@ -9,6 +9,8 @@ on:
- docker/** - docker/**
- .forgejo/workflows/build-release.yml - .forgejo/workflows/build-release.yml
- .forgejo/workflows/build-release-integration.yml - .forgejo/workflows/build-release-integration.yml
branches-ignore:
- renovate/**
pull_request: pull_request:
paths: paths:
- Makefile - Makefile

View file

@ -43,7 +43,7 @@ jobs:
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.22"
check-latest: true check-latest: true
- name: version from ref - name: version from ref

View file

@ -17,7 +17,7 @@ jobs:
- uses: https://code.forgejo.org/actions/checkout@v4 - uses: https://code.forgejo.org/actions/checkout@v4
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "~1.21" go-version: "1.22"
check-latest: true check-latest: true
- run: | - run: |
apt-get -qq update apt-get -qq update

View file

@ -64,7 +64,7 @@ jobs:
if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != '' if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != ''
uses: https://code.forgejo.org/actions/setup-go@v4 uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.22"
check-latest: true check-latest: true
- name: update the _release.experimental DNS record - name: update the _release.experimental DNS record
if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != '' if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != ''

View file

@ -0,0 +1,53 @@
name: renovate
on:
push:
branches:
- 'renovate/**' # self-test updates
schedule:
- cron: '*/30 * * * *'
env:
RENOVATE_DRY_RUN: ${{ (github.event_name != 'schedule' && github.ref_name != github.event.repository.default_branch) && 'full' || '' }}
RENOVATE_REPOSITORIES: ${{ github.repository }}
jobs:
renovate:
if: ${{ secrets.RENOVATE_TOKEN != '' }}
runs-on: docker
container:
image: ghcr.io/visualon/renovate:37.272.0
steps:
- uses: https://code.forgejo.org/actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
.tmp/cache/renovate/repository
key: repo-cache-${{ github.run_id }}
restore-keys: |
repo-cache-
- run: renovate
env:
GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
LOG_LEVEL: debug
RENOVATE_BASE_DIR: ${{ github.workspace }}/.tmp
RENOVATE_ENDPOINT: ${{ github.server_url }}
RENOVATE_PLATFORM: gitea
RENOVATE_REPOSITORY_CACHE: 'enabled'
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
RENOVATE_GIT_AUTHOR: 'Renovate Bot <forgejo-renovate-action@forgejo.org>'
GIT_AUTHOR_NAME: 'Renovate Bot'
GIT_AUTHOR_EMAIL: 'forgejo-renovate-action@forgejo.org'
GIT_COMMITTER_NAME: 'Renovate Bot'
GIT_COMMITTER_EMAIL: 'forgejo-renovate-action@forgejo.org'
- name: Save renovate repo cache
if: always() && env.RENOVATE_DRY_RUN == 'true'
uses: https://code.forgejo.org/actions/cache/save@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
.tmp/cache/renovate/repository
key: repo-cache-${{ github.run_id }}

View file

@ -17,7 +17,7 @@ jobs:
- uses: https://code.forgejo.org/actions/checkout@v3 - uses: https://code.forgejo.org/actions/checkout@v3
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.22"
check-latest: true check-latest: true
- run: make deps-backend deps-tools - run: make deps-backend deps-tools
- run: make --always-make -j$(nproc) lint-backend checks-backend # ensure the "go-licenses" make target runs - run: make --always-make -j$(nproc) lint-backend checks-backend # ensure the "go-licenses" make target runs
@ -52,7 +52,7 @@ jobs:
- uses: https://code.forgejo.org/actions/checkout@v3 - uses: https://code.forgejo.org/actions/checkout@v3
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.22"
- run: | - run: |
git config --add safe.directory '*' git config --add safe.directory '*'
adduser --quiet --comment forgejo --disabled-password forgejo adduser --quiet --comment forgejo --disabled-password forgejo
@ -97,7 +97,7 @@ jobs:
- uses: https://code.forgejo.org/actions/checkout@v3 - uses: https://code.forgejo.org/actions/checkout@v3
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.22"
- name: install dependencies & git >= 2.42 - name: install dependencies & git >= 2.42
run: | run: |
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
@ -144,7 +144,7 @@ jobs:
- uses: https://code.forgejo.org/actions/checkout@v3 - uses: https://code.forgejo.org/actions/checkout@v3
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.22"
- name: install dependencies & git >= 2.42 - name: install dependencies & git >= 2.42
run: | run: |
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
@ -181,7 +181,7 @@ jobs:
- uses: https://code.forgejo.org/actions/checkout@v3 - uses: https://code.forgejo.org/actions/checkout@v3
- uses: https://code.forgejo.org/actions/setup-go@v4 - uses: https://code.forgejo.org/actions/setup-go@v4
with: with:
go-version: "1.21" go-version: "1.22"
- name: install dependencies & git >= 2.42 - name: install dependencies & git >= 2.42
run: | run: |
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive

View file

@ -1,7 +1,7 @@
name: 🦋 Bug Report (web interface / frontend) name: 🦋 Bug Report (web interface / frontend)
description: Something doesn't look quite as it should? Report it here! description: Something doesn't look quite as it should? Report it here!
title: "[BUG] " title: "[BUG] "
labels: ["bug", "forgejo/ui"] labels: ["bug/new-report", "forgejo/ui"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -1,7 +1,7 @@
name: 🐛 Bug Report (server / backend) name: 🐛 Bug Report (server / backend)
description: Found something you weren't expecting? Report it here! description: Found something you weren't expecting? Report it here!
title: "[BUG] " title: "[BUG] "
labels: bug labels: bug/new-report
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -1,3 +1,12 @@
---
name: "Pull Request Template"
about: "Template for all Pull Requests"
labels:
- test/needed
---
<!-- <!--
Before submitting a PR, please read the contributing guidelines: Before submitting a PR, please read the contributing guidelines:
https://codeberg.org/forgejo/forgejo/src/branch/forgejo/CONTRIBUTING.md https://codeberg.org/forgejo/forgejo/src/branch/forgejo/CONTRIBUTING.md

2
.gitignore vendored
View file

@ -64,7 +64,7 @@ cpu.out
/data /data
/indexers /indexers
/log /log
/public/img/avatar /public/assets/img/avatar
/tests/integration/gitea-integration-* /tests/integration/gitea-integration-*
/tests/integration/indexers-* /tests/integration/indexers-*
/tests/e2e/gitea-e2e-* /tests/e2e/gitea-e2e-*

View file

@ -42,7 +42,7 @@ vscode:
- DavidAnson.vscode-markdownlint - DavidAnson.vscode-markdownlint
- Vue.volar - Vue.volar
- ms-azuretools.vscode-docker - ms-azuretools.vscode-docker
- zixuanchen.vitest-explorer - vitest.explorer
- qwtel.sqlite-viewer - qwtel.sqlite-viewer
- GitHub.vscode-pull-request-github - GitHub.vscode-pull-request-github

View file

@ -30,7 +30,7 @@ rules:
"@stylistic/block-opening-brace-newline-after": null "@stylistic/block-opening-brace-newline-after": null
"@stylistic/block-opening-brace-newline-before": null "@stylistic/block-opening-brace-newline-before": null
"@stylistic/block-opening-brace-space-after": null "@stylistic/block-opening-brace-space-after": null
"@stylistic/block-opening-brace-space-before": null "@stylistic/block-opening-brace-space-before": always
"@stylistic/color-hex-case": lower "@stylistic/color-hex-case": lower
"@stylistic/declaration-bang-space-after": never "@stylistic/declaration-bang-space-after": never
"@stylistic/declaration-bang-space-before": null "@stylistic/declaration-bang-space-before": null
@ -140,7 +140,7 @@ rules:
function-disallowed-list: null function-disallowed-list: null
function-linear-gradient-no-nonstandard-direction: true function-linear-gradient-no-nonstandard-direction: true
function-name-case: lower function-name-case: lower
function-no-unknown: null function-no-unknown: true
function-url-no-scheme-relative: null function-url-no-scheme-relative: null
function-url-quotes: always function-url-quotes: always
function-url-scheme-allowed-list: null function-url-scheme-allowed-list: null
@ -168,7 +168,7 @@ rules:
no-duplicate-selectors: true no-duplicate-selectors: true
no-empty-source: true no-empty-source: true
no-invalid-double-slash-comments: true no-invalid-double-slash-comments: true
no-invalid-position-at-import-rule: null no-invalid-position-at-import-rule: [true, ignoreAtRules: [tailwind]]
no-irregular-whitespace: true no-irregular-whitespace: true
no-unknown-animations: null no-unknown-animations: null
no-unknown-custom-properties: null no-unknown-custom-properties: null
@ -181,6 +181,7 @@ rules:
rule-empty-line-before: null rule-empty-line-before: null
rule-selector-property-disallowed-list: null rule-selector-property-disallowed-list: null
scale-unlimited/declaration-strict-value: [[/color$/, font-weight], {ignoreValues: /^(inherit|transparent|unset|initial|currentcolor|none)$/, ignoreFunctions: false, disableFix: true, expandShorthand: true}] scale-unlimited/declaration-strict-value: [[/color$/, font-weight], {ignoreValues: /^(inherit|transparent|unset|initial|currentcolor|none)$/, ignoreFunctions: false, disableFix: true, expandShorthand: true}]
selector-anb-no-unmatchable: true
selector-attribute-name-disallowed-list: null selector-attribute-name-disallowed-list: null
selector-attribute-operator-allowed-list: null selector-attribute-operator-allowed-list: null
selector-attribute-operator-disallowed-list: null selector-attribute-operator-disallowed-list: null

View file

@ -1,6 +1,6 @@
FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.21-alpine3.19 as build-env FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.22-alpine3.19 as build-env
ARG GOPROXY ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct} ENV GOPROXY ${GOPROXY:-direct}

View file

@ -1,6 +1,6 @@
FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.21-alpine3.19 as build-env FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.22-alpine3.19 as build-env
ARG GOPROXY ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct} ENV GOPROXY ${GOPROXY:-direct}

View file

@ -44,9 +44,6 @@ DOCKER_TAG ?= latest
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG) DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
ifeq ($(HAS_GO), yes) ifeq ($(HAS_GO), yes)
GOPATH ?= $(shell $(GO) env GOPATH)
export PATH := $(GOPATH)/bin:$(PATH)
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766 CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS) CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
endif endif
@ -148,6 +145,8 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN
GO_DIRS := build cmd models modules routers services tests GO_DIRS := build cmd models modules routers services tests
WEB_DIRS := web_src/js web_src/css WEB_DIRS := web_src/js web_src/css
ESLINT_FILES := web_src/js tools *.config.js tests/e2e
STYLELINT_FILES := web_src/css web_src/js/components/*.vue
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github
GO_SOURCES := $(wildcard *.go) GO_SOURCES := $(wildcard *.go)
@ -396,19 +395,19 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
.PHONY: lint-js .PHONY: lint-js
lint-js: node_modules lint-js: node_modules
npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES)
.PHONY: lint-js-fix .PHONY: lint-js-fix
lint-js-fix: node_modules lint-js-fix: node_modules
npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e --fix npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) --fix
.PHONY: lint-css .PHONY: lint-css
lint-css: node_modules lint-css: node_modules
npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue npx stylelint --color --max-warnings=0 $(STYLELINT_FILES)
.PHONY: lint-css-fix .PHONY: lint-css-fix
lint-css-fix: node_modules lint-css-fix: node_modules
npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue --fix npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
.PHONY: lint-swagger .PHONY: lint-swagger
lint-swagger: node_modules lint-swagger: node_modules
@ -468,7 +467,7 @@ lint-yaml: .venv
.PHONY: watch .PHONY: watch
watch: watch:
@bash build/watch.sh @bash tools/watch.sh
.PHONY: watch-frontend .PHONY: watch-frontend
watch-frontend: node-check node_modules watch-frontend: node-check node_modules
@ -962,7 +961,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
.PHONY: svg .PHONY: svg
svg: node-check | node_modules svg: node-check | node_modules
rm -rf $(SVG_DEST_DIR) rm -rf $(SVG_DEST_DIR)
node build/generate-svg.js node tools/generate-svg.js
.PHONY: svg-check .PHONY: svg-check
svg-check: svg svg-check: svg
@ -997,7 +996,7 @@ generate-gitignore:
.PHONY: generate-images .PHONY: generate-images
generate-images: | node_modules generate-images: | node_modules
npm install --no-save fabric@6.0.0-beta19 imagemin-zopfli@7 npm install --no-save fabric@6.0.0-beta19 imagemin-zopfli@7
node build/generate-images.js $(TAGS) node tools/generate-images.js $(TAGS)
.PHONY: generate-manpage .PHONY: generate-manpage
generate-manpage: generate-manpage:

View file

@ -293,9 +293,32 @@ Forgejo or set your environment appropriately.`, "")
return nil return nil
} }
// runHookUpdate process the update hook: https://git-scm.com/docs/githooks#update
func runHookUpdate(c *cli.Context) error { func runHookUpdate(c *cli.Context) error {
// Update is empty and is kept only for backwards compatibility // Now if we're an internal don't do anything else
return nil if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
ctx, cancel := installSignals()
defer cancel()
// The last three arguments given to the hook are in order: reference name, old commit ID and new commit ID.
args := os.Args[len(os.Args)-3:]
refFullName := git.RefName(args[0])
newCommitID := args[2]
// Only process pull references.
if !refFullName.IsPull() {
return nil
}
// Deletion of the ref means that the new commit ID is only composed of '0'.
if strings.ContainsFunc(newCommitID, func(e rune) bool { return e != '0' }) {
return nil
}
return fail(ctx, fmt.Sprintf("The deletion of %s is skipped as it's an internal reference.", refFullName), "")
} }
func runHookPostReceive(c *cli.Context) error { func runHookPostReceive(c *cli.Context) error {

View file

@ -37,7 +37,7 @@ gitea embedded list [--include-vendored] [patterns...]
- 列出所有模板文件,无论在哪个虚拟目录下:`**.tmpl` - 列出所有模板文件,无论在哪个虚拟目录下:`**.tmpl`
- 列出所有邮件模板文件:`templates/mail/**.tmpl` - 列出所有邮件模板文件:`templates/mail/**.tmpl`
- 列出 `public/img` 目录下的所有文件:`public/img/**` 列出 `public/assets/img` 目录下的所有文件:`public/assets/img/**`
不要忘记为模式使用引号,因为空格、`*` 和其他字符可能对命令行解释器有特殊含义。 不要忘记为模式使用引号,因为空格、`*` 和其他字符可能对命令行解释器有特殊含义。
@ -49,8 +49,8 @@ gitea embedded list [--include-vendored] [patterns...]
```sh ```sh
$ gitea embedded list '**openid**' $ gitea embedded list '**openid**'
public/img/auth/openid_connect.svg public/assets/img/auth/openid_connect.svg
public/img/openid-16x16.png public/assets/img/openid-16x16.png
templates/user/auth/finalize_openid.tmpl templates/user/auth/finalize_openid.tmpl
templates/user/auth/signin_openid.tmpl templates/user/auth/signin_openid.tmpl
templates/user/auth/signup_openid_connect.tmpl templates/user/auth/signup_openid_connect.tmpl

View file

@ -17,6 +17,12 @@ menu:
# Repository indexer # Repository indexer
## Builtin repository code search without indexer
Users could do repository-level code search without setting up a repository indexer.
The builtin code search is based on the `git grep` command, which is fast and efficient for small repositories.
Better code search support could be achieved by setting up the repository indexer.
## Setting up the repository indexer ## Setting up the repository indexer
Gitea can search through the files of the repositories by enabling this function in your [`app.ini`](administration/config-cheat-sheet.md): Gitea can search through the files of the repositories by enabling this function in your [`app.ini`](administration/config-cheat-sheet.md):

View file

@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h
9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided. 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided.
10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event. 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event.
11. Custom event names are recommended to use `ce-` prefix. 11. Custom event names are recommended to use `ce-` prefix.
12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). 12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-mono`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided. 13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided.
### Accessibility / ARIA ### Accessibility / ARIA

View file

@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
11. 推荐使用自定义事件名称前缀`ce-` 11. 推荐使用自定义事件名称前缀`ce-`
12. 建议使用 Tailwind CSS它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-df`Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。 12. 建议使用 Tailwind CSS它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-mono`Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
13. 尽量避免内联脚本和样式建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免请解释无法避免的原因。 13. 尽量避免内联脚本和样式建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免请解释无法避免的原因。
### 可访问性 / ARIA ### 可访问性 / ARIA

View file

@ -214,7 +214,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
### Building and adding SVGs ### Building and adding SVGs
SVG icons are built using the `make svg` target which compiles the icon sources defined in `build/generate-svg.js` into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory. SVG icons are built using the `make svg` target which compiles the icon sources into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory.
### Building the Logo ### Building the Logo

View file

@ -201,7 +201,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
### 构建和添加 SVGs ### 构建和添加 SVGs
SVG 图标是使用 `make svg` 目标构建的,该目标将 `build/generate-svg.js` 中定义的图标源编译到输出目录 `public/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。 SVG 图标是使用 `make svg` 命令构建的,该命令将图标资源编译到输出目录 `public/assets/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。
### 构建 Logo ### 构建 Logo

View file

@ -87,6 +87,9 @@ _Symbols used in table:_
| Git Blame | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Git Blame | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Visual comparison of image changes | ✓ | ✘ | ✓ | ? | ? | ? | ✘ | ✘ | | Visual comparison of image changes | ✓ | ✘ | ✓ | ? | ? | ? | ✘ | ✘ |
- Gitea has builtin repository-level code search
- Better code search support could be achieved by [using a repository indexer](administration/repo-indexer.md)
## Issue Tracker ## Issue Tracker
| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | RhodeCode EE | | Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | RhodeCode EE |

14
go.mod
View file

@ -1,6 +1,6 @@
module code.gitea.io/gitea module code.gitea.io/gitea
go 1.21 go 1.22
require ( require (
code.gitea.io/actions-proto-go v0.4.0 code.gitea.io/actions-proto-go v0.4.0
@ -10,8 +10,8 @@ require (
connectrpc.com/connect v1.15.0 connectrpc.com/connect v1.15.0
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669
gitea.com/go-chi/cache v0.2.0 gitea.com/go-chi/cache v0.2.0
gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384 gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3 gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
@ -29,18 +29,18 @@ require (
github.com/djherbis/nio/v3 v3.0.1 github.com/djherbis/nio/v3 v3.0.1
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.0 github.com/editorconfig/editorconfig-core-go/v2 v2.6.1
github.com/emersion/go-imap v1.2.1 github.com/emersion/go-imap v1.2.1
github.com/emirpasic/gods v1.18.1 github.com/emirpasic/gods v1.18.1
github.com/felixge/fgprof v0.9.3 github.com/felixge/fgprof v0.9.3
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.7.0
github.com/gliderlabs/ssh v0.3.6 github.com/gliderlabs/ssh v0.3.7
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.0.11 github.com/go-chi/chi/v5 v5.0.11
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0 github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.8.6 github.com/go-enry/go-enry/v2 v2.8.7
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e
github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.11.0 github.com/go-git/go-git/v5 v5.11.0
@ -66,7 +66,7 @@ require (
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4
github.com/klauspost/compress v1.17.4 github.com/klauspost/compress v1.17.7
github.com/klauspost/cpuid/v2 v2.2.6 github.com/klauspost/cpuid/v2 v2.2.6
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/markbates/goth v1.78.0 github.com/markbates/goth v1.78.0

38
go.sum
View file

@ -56,13 +56,12 @@ gitea.com/gitea/act v0.259.1 h1:8GG1o/xtUHl3qjn5f0h/2FXrT5ubBn05TJOM5ry+FBw=
gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8= gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8=
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo= gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo=
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc=
gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0=
gitea.com/go-chi/cache v0.2.0 h1:E0npuTfDW6CT1yD8NMDVc1SK6IeRjfmRL2zlEsCEd7w= gitea.com/go-chi/cache v0.2.0 h1:E0npuTfDW6CT1yD8NMDVc1SK6IeRjfmRL2zlEsCEd7w=
gitea.com/go-chi/cache v0.2.0/go.mod h1:iQlVK2aKTZ/rE9UcHyz9pQWGvdP9i1eI2spOpzgCrtE= gitea.com/go-chi/cache v0.2.0/go.mod h1:iQlVK2aKTZ/rE9UcHyz9pQWGvdP9i1eI2spOpzgCrtE=
gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384 h1:klh0LjhH7l4CuJkxlCM//o3rWLvWqxUpFxEtoYg5TNY= gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384/go.mod h1:hQ9SYHKdOX968wJglb/NMQ+UqpOKwW4L+EYdvkWjHSo= gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3 h1:4FuO+MahrkDjdjVIS8ExmY9FEHTZS8TPheEm4uU5xLI= gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 h1:IFDiMBObsP6CZIRaDLd54SR6zPYAffPXiXck5Xslu0Q=
gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3/go.mod h1:fc/pjt5EqNKgqQXYzcas1Z5L5whkZHyOvTA7OzWVJck= gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
@ -185,7 +184,6 @@ github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NY
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ= github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ=
@ -198,13 +196,10 @@ github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUK
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/couchbase/go-couchbase v0.0.0-20201026062457-7b3be89bbd89/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk= github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk=
github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A=
github.com/couchbase/gomemcached v0.1.1/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo=
github.com/couchbase/gomemcached v0.3.0 h1:XkMDdP6w7rtvLijDE0/RhcccX+XvAk5cboyBv1YcI0U= github.com/couchbase/gomemcached v0.3.0 h1:XkMDdP6w7rtvLijDE0/RhcccX+XvAk5cboyBv1YcI0U=
github.com/couchbase/gomemcached v0.3.0/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo= github.com/couchbase/gomemcached v0.3.0/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo=
github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs= github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs=
github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
@ -243,8 +238,8 @@ github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj6
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/editorconfig/editorconfig-core-go/v2 v2.6.0 h1:5O8paxMLmi/5ONoKXzWNYxoSZU7+ITVbGcPga0IrzfE= github.com/editorconfig/editorconfig-core-go/v2 v2.6.1 h1:iPCqofzMO41WVbcS/B5Ym7AwHQg9cyQ7Ie/R2XU5L3A=
github.com/editorconfig/editorconfig-core-go/v2 v2.6.0/go.mod h1:hdTKe+hwa3mMnMn4JUQziT+yc3pF+6EVmK2LPbLZthE= github.com/editorconfig/editorconfig-core-go/v2 v2.6.1/go.mod h1:VY4oyqUnpULFB3SCRpl24GFDIN1PmfiQIvN/G4ScSNg=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
@ -277,8 +272,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 h1:j2TrkUG/NATGi/EQS+MvEoF79CxiRUmT16ErFroNcKI= github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 h1:j2TrkUG/NATGi/EQS+MvEoF79CxiRUmT16ErFroNcKI=
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9/go.mod h1:cJ9Ye0ZNSMN7RzZDBRY3E+8M3Bpf/R1JX22Ir9yX6WI= github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9/go.mod h1:cJ9Ye0ZNSMN7RzZDBRY3E+8M3Bpf/R1JX22Ir9yX6WI=
github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 h1:I2nuhyVI/48VXoRCCZR2hYBgnSXa+EuDJf/VyX06TC0= github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 h1:I2nuhyVI/48VXoRCCZR2hYBgnSXa+EuDJf/VyX06TC0=
@ -295,8 +290,8 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-enry/go-enry/v2 v2.8.6 h1:T6ljs5+qNiUTDqpfK5GUD5EvLNdDbf804u8iC30vw7U= github.com/go-enry/go-enry/v2 v2.8.7 h1:vbab0pcf5Yo1cHQLzbWZ+QomUh3EfEU8EiR5n7W0lnQ=
github.com/go-enry/go-enry/v2 v2.8.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-enry/v2 v2.8.7/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
@ -342,7 +337,6 @@ github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmrid
github.com/go-openapi/validate v0.22.6 h1:+NhuwcEYpWdO5Nm4bmvhGLW0rt1Fcc532Mu3wpypXfo= github.com/go-openapi/validate v0.22.6 h1:+NhuwcEYpWdO5Nm4bmvhGLW0rt1Fcc532Mu3wpypXfo=
github.com/go-openapi/validate v0.22.6/go.mod h1:eaddXSqKeTg5XpSmj1dYyFTK/95n/XHwcOY+BMxKMyM= github.com/go-openapi/validate v0.22.6/go.mod h1:eaddXSqKeTg5XpSmj1dYyFTK/95n/XHwcOY+BMxKMyM=
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4= github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@ -427,7 +421,6 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -557,8 +550,8 @@ github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@ -667,14 +660,12 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@ -869,7 +860,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
@ -969,7 +959,6 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -1037,7 +1026,6 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1176,7 +1164,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -1255,7 +1242,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg= gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=

View file

@ -170,14 +170,16 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
return err return err
} }
// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow. // CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string) error { // It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
// Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'. func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
// Find all runs in the specified repository, reference, and workflow with non-final status
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
RepoID: repoID, RepoID: repoID,
Ref: ref, Ref: ref,
WorkflowID: workflowID, WorkflowID: workflowID,
Status: []Status{StatusRunning, StatusWaiting}, TriggerEvent: event,
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked},
}) })
if err != nil { if err != nil {
return err return err

View file

@ -10,6 +10,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
webhook_module "code.gitea.io/gitea/modules/webhook"
"xorm.io/builder" "xorm.io/builder"
) )
@ -71,6 +72,7 @@ type FindRunOptions struct {
WorkflowID string WorkflowID string
Ref string // the commit/tag/… that caused this workflow Ref string // the commit/tag/… that caused this workflow
TriggerUserID int64 TriggerUserID int64
TriggerEvent webhook_module.HookEventType
Approved bool // not util.OptionalBool, it works only when it's true Approved bool // not util.OptionalBool, it works only when it's true
Status []Status Status []Status
} }
@ -98,6 +100,9 @@ func (opts FindRunOptions) ToConds() builder.Cond {
if opts.Ref != "" { if opts.Ref != "" {
cond = cond.And(builder.Eq{"ref": opts.Ref}) cond = cond.And(builder.Eq{"ref": opts.Ref})
} }
if opts.TriggerEvent != "" {
cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent})
}
return cond return cond
} }

View file

@ -5,6 +5,7 @@ package actions
import ( import (
"context" "context"
"fmt"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -118,3 +119,22 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
return committer.Commit() return committer.Commit()
} }
func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
// If actions disabled when there is schedule task, this will remove the outdated schedule tasks
// There is no other place we can do this because the app.ini will be changed manually
if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
if err := CancelPreviousJobs(
ctx,
repo.ID,
repo.DefaultBranch,
"",
webhook_module.HookEventSchedule,
); err != nil {
return fmt.Errorf("CancelPreviousJobs: %v", err)
}
return nil
}

View file

@ -12,14 +12,11 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder" "xorm.io/builder"
) )
@ -79,53 +76,6 @@ func init() {
db.RegisterModel(new(Notification)) db.RegisterModel(new(Notification))
} }
// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
type FindNotificationOptions struct {
db.ListOptions
UserID int64
RepoID int64
IssueID int64
Status []NotificationStatus
Source []NotificationSource
UpdatedAfterUnix int64
UpdatedBeforeUnix int64
}
// ToCond will convert each condition into a xorm-Cond
func (opts FindNotificationOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.UserID != 0 {
cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
}
if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
}
if opts.IssueID != 0 {
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
}
if len(opts.Status) > 0 {
if len(opts.Status) == 1 {
cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
} else {
cond = cond.And(builder.In("notification.status", opts.Status))
}
}
if len(opts.Source) > 0 {
cond = cond.And(builder.In("notification.source", opts.Source))
}
if opts.UpdatedAfterUnix != 0 {
cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
}
if opts.UpdatedBeforeUnix != 0 {
cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
}
return cond
}
func (opts FindNotificationOptions) ToOrders() string {
return "notification.updated_unix DESC"
}
// CreateRepoTransferNotification creates notification for the user a repository was transferred to // CreateRepoTransferNotification creates notification for the user a repository was transferred to
func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error { func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
@ -159,118 +109,6 @@ func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_mo
}) })
} }
// CreateOrUpdateIssueNotifications creates an issue notification
// for each watcher, or updates it if already exists
// receiverID > 0 just send to receiver, else send to all watcher
func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil {
return err
}
return committer.Commit()
}
func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
// init
var toNotify container.Set[int64]
notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
IssueID: issueID,
})
if err != nil {
return err
}
issue, err := issues_model.GetIssueByID(ctx, issueID)
if err != nil {
return err
}
if receiverID > 0 {
toNotify = make(container.Set[int64], 1)
toNotify.Add(receiverID)
} else {
toNotify = make(container.Set[int64], 32)
issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
if err != nil {
return err
}
toNotify.AddMultiple(issueWatches...)
if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
if err != nil {
return err
}
toNotify.AddMultiple(repoWatches...)
}
issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
if err != nil {
return err
}
toNotify.AddMultiple(issueParticipants...)
// dont notify user who cause notification
delete(toNotify, notificationAuthorID)
// explicit unwatch on issue
issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
if err != nil {
return err
}
for _, id := range issueUnWatches {
toNotify.Remove(id)
}
// Remove users who have the notification author blocked.
blockedAuthorIDs, err := user_model.ListBlockedByUsersID(ctx, notificationAuthorID)
if err != nil {
return err
}
for _, id := range blockedAuthorIDs {
toNotify.Remove(id)
}
}
err = issue.LoadRepo(ctx)
if err != nil {
return err
}
// notify
for userID := range toNotify {
issue.Repo.Units = nil
user, err := user_model.GetUserByID(ctx, userID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
continue
}
return err
}
if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
continue
}
if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
continue
}
if notificationExists(notifications, issue.ID, userID) {
if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
return err
}
continue
}
if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
return err
}
}
return nil
}
func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error { func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error {
notification := &Notification{ notification := &Notification{
UserID: userID, UserID: userID,
@ -458,309 +296,6 @@ func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.Tim
return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res) return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res)
} }
// NotificationList contains a list of notifications
type NotificationList []*Notification
// LoadAttributes load Repo Issue User and Comment if not loaded
func (nl NotificationList) LoadAttributes(ctx context.Context) error {
if _, _, err := nl.LoadRepos(ctx); err != nil {
return err
}
if _, err := nl.LoadIssues(ctx); err != nil {
return err
}
if _, err := nl.LoadUsers(ctx); err != nil {
return err
}
if _, err := nl.LoadComments(ctx); err != nil {
return err
}
return nil
}
func (nl NotificationList) getPendingRepoIDs() []int64 {
ids := make(container.Set[int64], len(nl))
for _, notification := range nl {
if notification.Repository != nil {
continue
}
ids.Add(notification.RepoID)
}
return ids.Values()
}
// LoadRepos loads repositories from database
func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
if len(nl) == 0 {
return repo_model.RepositoryList{}, []int{}, nil
}
repoIDs := nl.getPendingRepoIDs()
repos := make(map[int64]*repo_model.Repository, len(repoIDs))
left := len(repoIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", repoIDs[:limit]).
Rows(new(repo_model.Repository))
if err != nil {
return nil, nil, err
}
for rows.Next() {
var repo repo_model.Repository
err = rows.Scan(&repo)
if err != nil {
rows.Close()
return nil, nil, err
}
repos[repo.ID] = &repo
}
_ = rows.Close()
left -= limit
repoIDs = repoIDs[limit:]
}
failed := []int{}
reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
for i, notification := range nl {
if notification.Repository == nil {
notification.Repository = repos[notification.RepoID]
}
if notification.Repository == nil {
log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
failed = append(failed, i)
continue
}
var found bool
for _, r := range reposList {
if r.ID == notification.RepoID {
found = true
break
}
}
if !found {
reposList = append(reposList, notification.Repository)
}
}
return reposList, failed, nil
}
func (nl NotificationList) getPendingIssueIDs() []int64 {
ids := make(container.Set[int64], len(nl))
for _, notification := range nl {
if notification.Issue != nil {
continue
}
ids.Add(notification.IssueID)
}
return ids.Values()
}
// LoadIssues loads issues from database
func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
if len(nl) == 0 {
return []int{}, nil
}
issueIDs := nl.getPendingIssueIDs()
issues := make(map[int64]*issues_model.Issue, len(issueIDs))
left := len(issueIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", issueIDs[:limit]).
Rows(new(issues_model.Issue))
if err != nil {
return nil, err
}
for rows.Next() {
var issue issues_model.Issue
err = rows.Scan(&issue)
if err != nil {
rows.Close()
return nil, err
}
issues[issue.ID] = &issue
}
_ = rows.Close()
left -= limit
issueIDs = issueIDs[limit:]
}
failures := []int{}
for i, notification := range nl {
if notification.Issue == nil {
notification.Issue = issues[notification.IssueID]
if notification.Issue == nil {
if notification.IssueID != 0 {
log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
failures = append(failures, i)
}
continue
}
notification.Issue.Repo = notification.Repository
}
}
return failures, nil
}
// Without returns the notification list without the failures
func (nl NotificationList) Without(failures []int) NotificationList {
if len(failures) == 0 {
return nl
}
remaining := make([]*Notification, 0, len(nl))
last := -1
var i int
for _, i = range failures {
remaining = append(remaining, nl[last+1:i]...)
last = i
}
if len(nl) > i {
remaining = append(remaining, nl[i+1:]...)
}
return remaining
}
func (nl NotificationList) getPendingCommentIDs() []int64 {
ids := make(container.Set[int64], len(nl))
for _, notification := range nl {
if notification.CommentID == 0 || notification.Comment != nil {
continue
}
ids.Add(notification.CommentID)
}
return ids.Values()
}
func (nl NotificationList) getUserIDs() []int64 {
ids := make(container.Set[int64], len(nl))
for _, notification := range nl {
if notification.UserID == 0 || notification.User != nil {
continue
}
ids.Add(notification.UserID)
}
return ids.Values()
}
// LoadUsers loads users from database
func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
if len(nl) == 0 {
return []int{}, nil
}
userIDs := nl.getUserIDs()
users := make(map[int64]*user_model.User, len(userIDs))
left := len(userIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", userIDs[:limit]).
Rows(new(user_model.User))
if err != nil {
return nil, err
}
for rows.Next() {
var user user_model.User
err = rows.Scan(&user)
if err != nil {
rows.Close()
return nil, err
}
users[user.ID] = &user
}
_ = rows.Close()
left -= limit
userIDs = userIDs[limit:]
}
failures := []int{}
for i, notification := range nl {
if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
notification.User = users[notification.UserID]
if notification.User == nil {
log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
failures = append(failures, i)
continue
}
}
}
return failures, nil
}
// LoadComments loads comments from database
func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
if len(nl) == 0 {
return []int{}, nil
}
commentIDs := nl.getPendingCommentIDs()
comments := make(map[int64]*issues_model.Comment, len(commentIDs))
left := len(commentIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", commentIDs[:limit]).
Rows(new(issues_model.Comment))
if err != nil {
return nil, err
}
for rows.Next() {
var comment issues_model.Comment
err = rows.Scan(&comment)
if err != nil {
rows.Close()
return nil, err
}
comments[comment.ID] = &comment
}
_ = rows.Close()
left -= limit
commentIDs = commentIDs[limit:]
}
failures := []int{}
for i, notification := range nl {
if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
notification.Comment = comments[notification.CommentID]
if notification.Comment == nil {
log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
failures = append(failures, i)
continue
}
notification.Comment.Issue = notification.Issue
}
}
return failures, nil
}
// SetIssueReadBy sets issue to be read by given user. // SetIssueReadBy sets issue to be read by given user.
func SetIssueReadBy(ctx context.Context, issueID, userID int64) error { func SetIssueReadBy(ctx context.Context, issueID, userID int64) error {
if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil { if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil {
@ -841,3 +376,31 @@ func UpdateNotificationStatuses(ctx context.Context, user *user_model.User, curr
Update(n) Update(n)
return err return err
} }
// LoadIssuePullRequests loads all issues' pull requests if possible
func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error {
issues := make(map[int64]*issues_model.Issue, len(nl))
for _, notification := range nl {
if notification.Issue != nil && notification.Issue.IsPull && notification.Issue.PullRequest == nil {
issues[notification.Issue.ID] = notification.Issue
}
}
if len(issues) == 0 {
return nil
}
pulls, err := issues_model.GetPullRequestByIssueIDs(ctx, util.KeysOfMap(issues))
if err != nil {
return err
}
for _, pull := range pulls {
if issue := issues[pull.IssueID]; issue != nil {
issue.PullRequest = pull
issue.PullRequest.Issue = issue
}
}
return nil
}

View file

@ -0,0 +1,480 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activities
import (
"context"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"xorm.io/builder"
)
// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
type FindNotificationOptions struct {
db.ListOptions
UserID int64
RepoID int64
IssueID int64
Status []NotificationStatus
Source []NotificationSource
UpdatedAfterUnix int64
UpdatedBeforeUnix int64
}
// ToCond will convert each condition into a xorm-Cond
func (opts FindNotificationOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.UserID != 0 {
cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
}
if opts.RepoID != 0 {
cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
}
if opts.IssueID != 0 {
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
}
if len(opts.Status) > 0 {
if len(opts.Status) == 1 {
cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
} else {
cond = cond.And(builder.In("notification.status", opts.Status))
}
}
if len(opts.Source) > 0 {
cond = cond.And(builder.In("notification.source", opts.Source))
}
if opts.UpdatedAfterUnix != 0 {
cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
}
if opts.UpdatedBeforeUnix != 0 {
cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
}
return cond
}
func (opts FindNotificationOptions) ToOrders() string {
return "notification.updated_unix DESC"
}
// CreateOrUpdateIssueNotifications creates an issue notification
// for each watcher, or updates it if already exists
// receiverID > 0 just send to receiver, else send to all watcher
func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil {
return err
}
return committer.Commit()
}
func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
// init
var toNotify container.Set[int64]
notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
IssueID: issueID,
})
if err != nil {
return err
}
issue, err := issues_model.GetIssueByID(ctx, issueID)
if err != nil {
return err
}
if receiverID > 0 {
toNotify = make(container.Set[int64], 1)
toNotify.Add(receiverID)
} else {
toNotify = make(container.Set[int64], 32)
issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
if err != nil {
return err
}
toNotify.AddMultiple(issueWatches...)
if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
if err != nil {
return err
}
toNotify.AddMultiple(repoWatches...)
}
issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
if err != nil {
return err
}
toNotify.AddMultiple(issueParticipants...)
// dont notify user who cause notification
delete(toNotify, notificationAuthorID)
// explicit unwatch on issue
issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
if err != nil {
return err
}
for _, id := range issueUnWatches {
toNotify.Remove(id)
}
// Remove users who have the notification author blocked.
blockedAuthorIDs, err := user_model.ListBlockedByUsersID(ctx, notificationAuthorID)
if err != nil {
return err
}
for _, id := range blockedAuthorIDs {
toNotify.Remove(id)
}
}
err = issue.LoadRepo(ctx)
if err != nil {
return err
}
// notify
for userID := range toNotify {
issue.Repo.Units = nil
user, err := user_model.GetUserByID(ctx, userID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
continue
}
return err
}
if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
continue
}
if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
continue
}
if notificationExists(notifications, issue.ID, userID) {
if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
return err
}
continue
}
if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
return err
}
}
return nil
}
// NotificationList contains a list of notifications
type NotificationList []*Notification
// LoadAttributes load Repo Issue User and Comment if not loaded
func (nl NotificationList) LoadAttributes(ctx context.Context) error {
if _, _, err := nl.LoadRepos(ctx); err != nil {
return err
}
if _, err := nl.LoadIssues(ctx); err != nil {
return err
}
if _, err := nl.LoadUsers(ctx); err != nil {
return err
}
if _, err := nl.LoadComments(ctx); err != nil {
return err
}
return nil
}
func (nl NotificationList) getPendingRepoIDs() []int64 {
ids := make(container.Set[int64], len(nl))
for _, notification := range nl {
if notification.Repository != nil {
continue
}
ids.Add(notification.RepoID)
}
return ids.Values()
}
// LoadRepos loads repositories from database
func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
if len(nl) == 0 {
return repo_model.RepositoryList{}, []int{}, nil
}
repoIDs := nl.getPendingRepoIDs()
repos := make(map[int64]*repo_model.Repository, len(repoIDs))
left := len(repoIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", repoIDs[:limit]).
Rows(new(repo_model.Repository))
if err != nil {
return nil, nil, err
}
for rows.Next() {
var repo repo_model.Repository
err = rows.Scan(&repo)
if err != nil {
rows.Close()
return nil, nil, err
}
repos[repo.ID] = &repo
}
_ = rows.Close()
left -= limit
repoIDs = repoIDs[limit:]
}
failed := []int{}
reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
for i, notification := range nl {
if notification.Repository == nil {
notification.Repository = repos[notification.RepoID]
}
if notification.Repository == nil {
log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
failed = append(failed, i)
continue
}
var found bool
for _, r := range reposList {
if r.ID == notification.RepoID {
found = true
break
}
}
if !found {
reposList = append(reposList, notification.Repository)
}
}
return reposList, failed, nil
}
func (nl NotificationList) getPendingIssueIDs() []int64 {
ids := make(container.Set[int64], len(nl))
for _, notification := range nl {
if notification.Issue != nil {
continue
}
ids.Add(notification.IssueID)
}
return ids.Values()
}
// LoadIssues loads issues from database
func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
if len(nl) == 0 {
return []int{}, nil
}
issueIDs := nl.getPendingIssueIDs()
issues := make(map[int64]*issues_model.Issue, len(issueIDs))
left := len(issueIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", issueIDs[:limit]).
Rows(new(issues_model.Issue))
if err != nil {
return nil, err
}
for rows.Next() {
var issue issues_model.Issue
err = rows.Scan(&issue)
if err != nil {
rows.Close()
return nil, err
}
issues[issue.ID] = &issue
}
_ = rows.Close()
left -= limit
issueIDs = issueIDs[limit:]
}
failures := []int{}
for i, notification := range nl {
if notification.Issue == nil {
notification.Issue = issues[notification.IssueID]
if notification.Issue == nil {
if notification.IssueID != 0 {
log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
failures = append(failures, i)
}
continue
}
notification.Issue.Repo = notification.Repository
}
}
return failures, nil
}
// Without returns the notification list without the failures
func (nl NotificationList) Without(failures []int) NotificationList {
if len(failures) == 0 {
return nl
}
remaining := make([]*Notification, 0, len(nl))
last := -1
var i int
for _, i = range failures {
remaining = append(remaining, nl[last+1:i]...)
last = i
}
if len(nl) > i {
remaining = append(remaining, nl[i+1:]...)
}
return remaining
}
func (nl NotificationList) getPendingCommentIDs() []int64 {
ids := make(container.Set[int64], len(nl))
for _, notification := range nl {
if notification.CommentID == 0 || notification.Comment != nil {
continue
}
ids.Add(notification.CommentID)
}
return ids.Values()
}
func (nl NotificationList) getUserIDs() []int64 {
ids := make(container.Set[int64], len(nl))
for _, notification := range nl {
if notification.UserID == 0 || notification.User != nil {
continue
}
ids.Add(notification.UserID)
}
return ids.Values()
}
// LoadUsers loads users from database
func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
if len(nl) == 0 {
return []int{}, nil
}
userIDs := nl.getUserIDs()
users := make(map[int64]*user_model.User, len(userIDs))
left := len(userIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", userIDs[:limit]).
Rows(new(user_model.User))
if err != nil {
return nil, err
}
for rows.Next() {
var user user_model.User
err = rows.Scan(&user)
if err != nil {
rows.Close()
return nil, err
}
users[user.ID] = &user
}
_ = rows.Close()
left -= limit
userIDs = userIDs[limit:]
}
failures := []int{}
for i, notification := range nl {
if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
notification.User = users[notification.UserID]
if notification.User == nil {
log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
failures = append(failures, i)
continue
}
}
}
return failures, nil
}
// LoadComments loads comments from database
func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
if len(nl) == 0 {
return []int{}, nil
}
commentIDs := nl.getPendingCommentIDs()
comments := make(map[int64]*issues_model.Comment, len(commentIDs))
left := len(commentIDs)
for left > 0 {
limit := db.DefaultMaxInSize
if left < limit {
limit = left
}
rows, err := db.GetEngine(ctx).
In("id", commentIDs[:limit]).
Rows(new(issues_model.Comment))
if err != nil {
return nil, err
}
for rows.Next() {
var comment issues_model.Comment
err = rows.Scan(&comment)
if err != nil {
rows.Close()
return nil, err
}
comments[comment.ID] = &comment
}
_ = rows.Close()
left -= limit
commentIDs = commentIDs[limit:]
}
failures := []int{}
for i, notification := range nl {
if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
notification.Comment = comments[notification.CommentID]
if notification.Comment == nil {
log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
failures = append(failures, i)
continue
}
notification.Comment.Issue = notification.Issue
}
}
return failures, nil
}

View file

@ -198,6 +198,8 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(f)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
@ -207,11 +209,12 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
} }
_, err = t.WriteString(line + "\n") _, err = t.WriteString(line + "\n")
if err != nil { if err != nil {
f.Close()
return err return err
} }
} }
f.Close() if err = scanner.Err(); err != nil {
return fmt.Errorf("RegeneratePublicKeys scan: %w", err)
}
} }
return nil return nil
} }

View file

@ -120,6 +120,8 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(f)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
@ -129,11 +131,12 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
} }
_, err = t.WriteString(line + "\n") _, err = t.WriteString(line + "\n")
if err != nil { if err != nil {
f.Close()
return err return err
} }
} }
f.Close() if err = scanner.Err(); err != nil {
return fmt.Errorf("regeneratePrincipalKeys scan: %w", err)
}
} }
return nil return nil
} }

View file

@ -40,7 +40,7 @@ func TestParseCommitWithSSHSignature(t *testing.T) {
Committer: &git.Signature{ Committer: &git.Signature{
Email: "non-existent", Email: "non-existent",
}, },
Signature: &git.CommitGPGSignature{ Signature: &git.ObjectSignature{
Payload: `tree 2d491b2985a7ff848d5c02748e7ea9f9f7619f9f Payload: `tree 2d491b2985a7ff848d5c02748e7ea9f9f7619f9f
parent 45b03601635a1f463b81963a4022c7f87ce96ef9 parent 45b03601635a1f463b81963a4022c7f87ce96ef9
author user2 <non-existent> 1699710556 +0100 author user2 <non-existent> 1699710556 +0100
@ -67,7 +67,7 @@ AAAAQIMufOuSjZeDUujrkVK4sl7ICa0WwEftas8UAYxx0Thdkiw2qWjR1U1PKfTLm16/w8
Committer: &git.Signature{ Committer: &git.Signature{
Email: "user2@example.com", Email: "user2@example.com",
}, },
Signature: &git.CommitGPGSignature{ Signature: &git.ObjectSignature{
Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
parent c2780d5c313da2a947eae22efd7dacf4213f4e7f parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
author user2 <user2@example.com> 1699707877 +0100 author user2 <user2@example.com> 1699707877 +0100
@ -89,7 +89,7 @@ Add content
Committer: &git.Signature{ Committer: &git.Signature{
Email: "user2@example.com", Email: "user2@example.com",
}, },
Signature: &git.CommitGPGSignature{ Signature: &git.ObjectSignature{
Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
parent c2780d5c313da2a947eae22efd7dacf4213f4e7f parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
author user2 <user2@example.com> 1699707877 +0100 author user2 <user2@example.com> 1699707877 +0100
@ -120,7 +120,7 @@ fs9cMpZVM9BfIKNUSO8QY=
Committer: &git.Signature{ Committer: &git.Signature{
Email: "user2@noreply.example.com", Email: "user2@noreply.example.com",
}, },
Signature: &git.CommitGPGSignature{ Signature: &git.ObjectSignature{
Payload: `tree 4836c7f639f37388bab4050ef5c97bbbd54272fc Payload: `tree 4836c7f639f37388bab4050ef5c97bbbd54272fc
parent 795be1b0117ea5c65456050bb9fd84744d4fd9c6 parent 795be1b0117ea5c65456050bb9fd84744d4fd9c6
author user2 <user2@noreply.example.com> 1699709594 +0100 author user2 <user2@noreply.example.com> 1699709594 +0100

View file

@ -24,7 +24,7 @@ import (
const ( const (
// DefaultAvatarClass is the default class of a rendered avatar // DefaultAvatarClass is the default class of a rendered avatar
DefaultAvatarClass = "ui avatar gt-vm" DefaultAvatarClass = "ui avatar tw-align-middle"
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar // DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
DefaultAvatarPixelSize = 28 DefaultAvatarPixelSize = 28
) )

View file

@ -6,6 +6,7 @@
repo_id: 2 # private repo_id: 2 # private
is_private: true is_private: true
created_unix: 1603228283 created_unix: 1603228283
content: '1|' # issueId 4
- -
id: 2 id: 2

View file

@ -17,6 +17,195 @@
updated: 1683636626 updated: 1683636626
need_approval: 0 need_approval: 0
approved_by: 0 approved_by: 0
event_payload: |
{
"after": "7a3858dc7f059543a8807a8b551304b7e362a7ef",
"before": "0000000000000000000000000000000000000000",
"commits": [
{
"added": [
".forgejo/workflows/test.yml"
],
"author": {
"email": "root@example.com",
"name": "username",
"username": "root"
},
"committer": {
"email": "root@example.com",
"name": "username",
"username": "root"
},
"id": "7a3858dc7f059543a8807a8b551304b7e362a7ef",
"message": "initial commit\n",
"modified": [],
"removed": [],
"timestamp": "2024-01-24T18:59:25Z",
"url": "http://10.201.14.40:3000/root/example-push/commit/7a3858dc7f059543a8807a8b551304b7e362a7ef",
"verification": null
}
],
"compare_url": "http://10.201.14.40:3000/",
"head_commit": {
"added": [
".forgejo/workflows/test.yml"
],
"author": {
"email": "root@example.com",
"name": "username",
"username": "root"
},
"committer": {
"email": "root@example.com",
"name": "username",
"username": "root"
},
"id": "7a3858dc7f059543a8807a8b551304b7e362a7ef",
"message": "initial commit\n",
"modified": [],
"removed": [],
"timestamp": "2024-01-24T18:59:25Z",
"url": "http://10.201.14.40:3000/root/example-push/commit/7a3858dc7f059543a8807a8b551304b7e362a7ef",
"verification": null
},
"pusher": {
"active": false,
"avatar_url": "http://10.201.14.40:3000/avatars/04edfc0ef6c6cf6d6b88fbc69f9f9071",
"created": "2024-01-24T18:57:32Z",
"description": "",
"email": "root@noreply.10.201.14.40",
"followers_count": 0,
"following_count": 0,
"full_name": "",
"id": 1,
"is_admin": false,
"language": "",
"last_login": "0001-01-01T00:00:00Z",
"location": "",
"login": "root",
"login_name": "",
"prohibit_login": false,
"restricted": false,
"starred_repos_count": 0,
"username": "root",
"visibility": "public",
"website": ""
},
"ref": "refs/heads/main",
"repository": {
"allow_merge_commits": true,
"allow_rebase": true,
"allow_rebase_explicit": true,
"allow_rebase_update": true,
"allow_squash_merge": true,
"archived": false,
"archived_at": "1970-01-01T00:00:00Z",
"avatar_url": "",
"clone_url": "http://10.201.14.40:3000/root/example-push.git",
"created_at": "2024-01-24T18:59:25Z",
"default_allow_maintainer_edit": false,
"default_branch": "main",
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"description": "",
"empty": false,
"fork": false,
"forks_count": 0,
"full_name": "root/example-push",
"has_actions": true,
"has_issues": true,
"has_packages": true,
"has_projects": true,
"has_pull_requests": true,
"has_releases": true,
"has_wiki": true,
"html_url": "http://10.201.14.40:3000/root/example-push",
"id": 2,
"ignore_whitespace_conflicts": false,
"internal": false,
"internal_tracker": {
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true,
"enable_time_tracker": true
},
"language": "",
"languages_url": "http://10.201.14.40:3000/api/v1/repos/root/example-push/languages",
"link": "",
"mirror": false,
"mirror_interval": "",
"mirror_updated": "0001-01-01T00:00:00Z",
"name": "example-push",
"object_format_name": "",
"open_issues_count": 0,
"open_pr_counter": 0,
"original_url": "",
"owner": {
"active": false,
"avatar_url": "http://10.201.14.40:3000/avatars/04edfc0ef6c6cf6d6b88fbc69f9f9071",
"created": "2024-01-24T18:57:32Z",
"description": "",
"email": "root@example.com",
"followers_count": 0,
"following_count": 0,
"full_name": "",
"id": 1,
"is_admin": false,
"language": "",
"last_login": "0001-01-01T00:00:00Z",
"location": "",
"login": "root",
"login_name": "",
"prohibit_login": false,
"restricted": false,
"starred_repos_count": 0,
"username": "root",
"visibility": "public",
"website": ""
},
"parent": null,
"permissions": {
"admin": true,
"pull": true,
"push": true
},
"private": false,
"release_counter": 0,
"repo_transfer": null,
"size": 25,
"ssh_url": "forgejo@10.201.14.40:root/example-push.git",
"stars_count": 0,
"template": false,
"updated_at": "2024-01-24T18:59:25Z",
"url": "http://10.201.14.40:3000/api/v1/repos/root/example-push",
"watchers_count": 1,
"website": ""
},
"sender": {
"active": false,
"avatar_url": "http://10.201.14.40:3000/avatars/04edfc0ef6c6cf6d6b88fbc69f9f9071",
"created": "2024-01-24T18:57:32Z",
"description": "",
"email": "root@noreply.10.201.14.40",
"followers_count": 0,
"following_count": 0,
"full_name": "",
"id": 1,
"is_admin": false,
"language": "",
"last_login": "0001-01-01T00:00:00Z",
"location": "",
"login": "root",
"login_name": "",
"prohibit_login": false,
"restricted": false,
"starred_repos_count": 0,
"username": "root",
"visibility": "public",
"website": ""
},
"total_commits": 0
}
- -
id: 792 id: 792
title: "update actions" title: "update actions"
@ -36,3 +225,191 @@
updated: 1683636626 updated: 1683636626
need_approval: 0 need_approval: 0
approved_by: 0 approved_by: 0
event_payload: |
{
"after": "7a3858dc7f059543a8807a8b551304b7e362a7ef",
"before": "0000000000000000000000000000000000000000",
"commits": [
{
"added": [
".forgejo/workflows/test.yml"
],
"author": {
"email": "root@example.com",
"name": "username",
"username": "root"
},
"committer": {
"email": "root@example.com",
"name": "username",
"username": "root"
},
"id": "7a3858dc7f059543a8807a8b551304b7e362a7ef",
"message": "initial commit\n",
"modified": [],
"removed": [],
"timestamp": "2024-01-24T18:59:25Z",
"url": "http://10.201.14.40:3000/root/example-push/commit/7a3858dc7f059543a8807a8b551304b7e362a7ef",
"verification": null
}
],
"compare_url": "http://10.201.14.40:3000/",
"head_commit": {
"added": [
".forgejo/workflows/test.yml"
],
"author": {
"email": "root@example.com",
"name": "username",
"username": "root"
},
"committer": {
"email": "root@example.com",
"name": "username",
"username": "root"
},
"id": "7a3858dc7f059543a8807a8b551304b7e362a7ef",
"message": "initial commit\n",
"modified": [],
"removed": [],
"timestamp": "2024-01-24T18:59:25Z",
"url": "http://10.201.14.40:3000/root/example-push/commit/7a3858dc7f059543a8807a8b551304b7e362a7ef",
"verification": null
},
"pusher": {
"active": false,
"avatar_url": "http://10.201.14.40:3000/avatars/04edfc0ef6c6cf6d6b88fbc69f9f9071",
"created": "2024-01-24T18:57:32Z",
"description": "",
"email": "root@noreply.10.201.14.40",
"followers_count": 0,
"following_count": 0,
"full_name": "",
"id": 1,
"is_admin": false,
"language": "",
"last_login": "0001-01-01T00:00:00Z",
"location": "",
"login": "root",
"login_name": "",
"prohibit_login": false,
"restricted": false,
"starred_repos_count": 0,
"username": "root",
"visibility": "public",
"website": ""
},
"ref": "refs/heads/main",
"repository": {
"allow_merge_commits": true,
"allow_rebase": true,
"allow_rebase_explicit": true,
"allow_rebase_update": true,
"allow_squash_merge": true,
"archived": false,
"archived_at": "1970-01-01T00:00:00Z",
"avatar_url": "",
"clone_url": "http://10.201.14.40:3000/root/example-push.git",
"created_at": "2024-01-24T18:59:25Z",
"default_allow_maintainer_edit": false,
"default_branch": "main",
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"description": "",
"empty": false,
"fork": false,
"forks_count": 0,
"full_name": "root/example-push",
"has_actions": true,
"has_issues": true,
"has_packages": true,
"has_projects": true,
"has_pull_requests": true,
"has_releases": true,
"has_wiki": true,
"html_url": "http://10.201.14.40:3000/root/example-push",
"id": 2,
"ignore_whitespace_conflicts": false,
"internal": false,
"internal_tracker": {
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true,
"enable_time_tracker": true
},
"language": "",
"languages_url": "http://10.201.14.40:3000/api/v1/repos/root/example-push/languages",
"link": "",
"mirror": false,
"mirror_interval": "",
"mirror_updated": "0001-01-01T00:00:00Z",
"name": "example-push",
"object_format_name": "",
"open_issues_count": 0,
"open_pr_counter": 0,
"original_url": "",
"owner": {
"active": false,
"avatar_url": "http://10.201.14.40:3000/avatars/04edfc0ef6c6cf6d6b88fbc69f9f9071",
"created": "2024-01-24T18:57:32Z",
"description": "",
"email": "root@example.com",
"followers_count": 0,
"following_count": 0,
"full_name": "",
"id": 1,
"is_admin": false,
"language": "",
"last_login": "0001-01-01T00:00:00Z",
"location": "",
"login": "root",
"login_name": "",
"prohibit_login": false,
"restricted": false,
"starred_repos_count": 0,
"username": "root",
"visibility": "public",
"website": ""
},
"parent": null,
"permissions": {
"admin": true,
"pull": true,
"push": true
},
"private": false,
"release_counter": 0,
"repo_transfer": null,
"size": 25,
"ssh_url": "forgejo@10.201.14.40:root/example-push.git",
"stars_count": 0,
"template": false,
"updated_at": "2024-01-24T18:59:25Z",
"url": "http://10.201.14.40:3000/api/v1/repos/root/example-push",
"watchers_count": 1,
"website": ""
},
"sender": {
"active": false,
"avatar_url": "http://10.201.14.40:3000/avatars/04edfc0ef6c6cf6d6b88fbc69f9f9071",
"created": "2024-01-24T18:57:32Z",
"description": "",
"email": "root@noreply.10.201.14.40",
"followers_count": 0,
"following_count": 0,
"full_name": "",
"id": 1,
"is_admin": false,
"language": "",
"last_login": "0001-01-01T00:00:00Z",
"location": "",
"login": "root",
"login_name": "",
"prohibit_login": false,
"restricted": false,
"starred_repos_count": 0,
"username": "root",
"visibility": "public",
"website": ""
},
"total_commits": 0
}

View file

@ -1,15 +1,18 @@
- -
id: 1 id: 1
repo_id: 1 repo_id: 1
url: www.example.com/url1 url: http://www.example.com/url1
http_method: POST
type: forgejo
content_type: 1 # json content_type: 1 # json
events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}' events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}'
is_active: true is_active: false # disable to prevent sending hook task during unrelated tests
- -
id: 2 id: 2
repo_id: 1 repo_id: 1
url: www.example.com/url2 url: http://www.example.com/url2
http_method: POST
content_type: 1 # json content_type: 1 # json
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}' events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
is_active: false is_active: false
@ -18,14 +21,16 @@
id: 3 id: 3
owner_id: 3 owner_id: 3
repo_id: 3 repo_id: 3
url: www.example.com/url3 url: http://www.example.com/url3
http_method: POST
content_type: 1 # json content_type: 1 # json
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}' events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
is_active: true is_active: false
- -
id: 4 id: 4
repo_id: 2 repo_id: 2
url: www.example.com/url4 url: http://www.example.com/url4
http_method: POST
content_type: 1 # json content_type: 1 # json
events: '{"push_only":true,"branch_filter":"{master,feature*}"}' events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
is_active: true is_active: false

View file

@ -39,17 +39,21 @@ func NewMigration(desc string, fn func(*xorm.Engine) error) *Migration {
// Add new migrations to the bottom of the list. // Add new migrations to the bottom of the list.
var migrations = []*Migration{ var migrations = []*Migration{
// v0 -> v1 // v0 -> v1
NewMigration("Add Forgejo Blocked Users table", forgejo_v1_20.AddForgejoBlockedUser), NewMigration("Create the `forgejo_blocked_user` table", forgejo_v1_20.AddForgejoBlockedUser),
// v1 -> v2 // v1 -> v2
NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable), NewMigration("Create the `forgejo_sem_ver` table", forgejo_v1_20.CreateSemVerTable),
// v2 -> v3 // v2 -> v3
NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable), NewMigration("Create the `forgejo_auth_token` table", forgejo_v1_20.CreateAuthorizationTokenTable),
// v3 -> v4 // v3 -> v4
NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit), NewMigration("Add the `default_permissions` column to the `repo_unit` table", forgejo_v1_22.AddDefaultPermissionsToRepoUnit),
// v4 -> v5 // v4 -> v5
NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable), NewMigration("Create the `forgejo_repo_flag` table", forgejo_v1_22.CreateRepoFlagTable),
// v5 -> v6 // v5 -> v6
NewMigration("Add wiki_branch to repository", forgejo_v1_22.AddWikiBranchToRepository), NewMigration("Add the `wiki_branch` column to the `repository` table", forgejo_v1_22.AddWikiBranchToRepository),
// v6 -> v7
NewMigration("Add the `enable_repo_unit_hints` column to the `user` table", forgejo_v1_22.AddUserRepoUnitHintsSetting),
// v7 -> v8
NewMigration("Modify the `release`.`note` content to remove SSH signatures", forgejo_v1_22.RemoveSSHSignaturesFromReleaseNotes),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -0,0 +1,14 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
)
func TestMain(m *testing.M) {
base.MainTest(m)
}

View file

@ -9,7 +9,7 @@ import (
func AddWikiBranchToRepository(x *xorm.Engine) error { func AddWikiBranchToRepository(x *xorm.Engine) error {
type Repository struct { type Repository struct {
ID int64 ID int64 `xorm:"pk autoincr"`
WikiBranch string WikiBranch string
} }

View file

@ -0,0 +1,17 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"xorm.io/xorm"
)
func AddUserRepoUnitHintsSetting(x *xorm.Engine) error {
type User struct {
ID int64 `xorm:"pk autoincr"`
EnableRepoUnitHints bool `xorm:"NOT NULL DEFAULT true"`
}
return x.Sync(&User{})
}

View file

@ -0,0 +1,51 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"strings"
"xorm.io/xorm"
)
func RemoveSSHSignaturesFromReleaseNotes(x *xorm.Engine) error {
type Release struct {
ID int64 `xorm:"pk autoincr"`
Note string `xorm:"TEXT"`
}
if err := x.Sync(&Release{}); err != nil {
return err
}
var releaseNotes []struct {
ID int64
Note string
}
if err := x.Table("release").Where("note LIKE '%-----BEGIN SSH SIGNATURE-----%'").Find(&releaseNotes); err != nil {
return err
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
for _, release := range releaseNotes {
idx := strings.LastIndex(release.Note, "-----BEGIN SSH SIGNATURE-----")
if idx == -1 {
continue
}
release.Note = release.Note[:idx]
_, err := sess.Exec("UPDATE `release` SET note = ? WHERE id = ?", release.Note, release.ID)
if err != nil {
return err
}
}
return sess.Commit()
}

View file

@ -0,0 +1,34 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"github.com/stretchr/testify/assert"
)
func Test_RemoveSSHSignaturesFromReleaseNotes(t *testing.T) {
// A reduced mock of the `repo_model.Release` struct.
type Release struct {
ID int64 `xorm:"pk autoincr"`
Note string `xorm:"TEXT"`
}
x, deferable := base.PrepareTestEnv(t, 0, new(Release))
defer deferable()
assert.NoError(t, RemoveSSHSignaturesFromReleaseNotes(x))
var releases []Release
err := x.Table("release").OrderBy("id ASC").Find(&releases)
assert.NoError(t, err)
assert.Len(t, releases, 3)
assert.Equal(t, "", releases[0].Note)
assert.Equal(t, "A message.\n", releases[1].Note)
assert.Equal(t, "no signature present here", releases[2].Note)
}

View file

@ -292,7 +292,7 @@ func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *
} }
// RenameBranch rename a branch // RenameBranch rename a branch
func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) { func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(ctx context.Context, isDefault bool) error) (err error) {
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
@ -367,7 +367,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
} }
// 5. do git action // 5. do git action
if err = gitAction(isDefault); err != nil { if err = gitAction(ctx, isDefault); err != nil {
return err return err
} }

View file

@ -4,6 +4,7 @@
package git_test package git_test
import ( import (
"context"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -132,7 +133,7 @@ func TestRenameBranch(t *testing.T) {
}, git_model.WhitelistOptions{})) }, git_model.WhitelistOptions{}))
assert.NoError(t, committer.Commit()) assert.NoError(t, committer.Commit())
assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(isDefault bool) error { assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(ctx context.Context, isDefault bool) error {
_isDefault = isDefault _isDefault = isDefault
return nil return nil
})) }))

View file

@ -199,22 +199,17 @@ func (status *CommitStatus) LocaleString(lang translation.Locale) string {
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus { func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
var lastStatus *CommitStatus if len(statuses) == 0 {
state := api.CommitStatusSuccess return nil
for _, status := range statuses { }
if status.State.NoBetterThan(state) {
state = status.State latestWorstStatus := statuses[0]
lastStatus = status for _, status := range statuses[1:] {
if status.State.NoBetterThan(latestWorstStatus.State) {
latestWorstStatus = status
} }
} }
if lastStatus == nil { return latestWorstStatus
if len(statuses) > 0 {
lastStatus = statuses[0]
} else {
lastStatus = &CommitStatus{}
}
}
return lastStatus
} }
// CommitStatusOptions holds the options for query commit statuses // CommitStatusOptions holds the options for query commit statuses

View file

@ -141,16 +141,20 @@ func Test_CalcCommitStatus(t *testing.T) {
statuses: []*git_model.CommitStatus{ statuses: []*git_model.CommitStatus{
{ {
State: structs.CommitStatusSuccess, State: structs.CommitStatusSuccess,
ID: 1,
}, },
{ {
State: structs.CommitStatusSuccess, State: structs.CommitStatusSuccess,
ID: 2,
}, },
{ {
State: structs.CommitStatusSuccess, State: structs.CommitStatusSuccess,
ID: 3,
}, },
}, },
expected: &git_model.CommitStatus{ expected: &git_model.CommitStatus{
State: structs.CommitStatusSuccess, State: structs.CommitStatusSuccess,
ID: 3,
}, },
}, },
{ {
@ -169,6 +173,10 @@ func Test_CalcCommitStatus(t *testing.T) {
State: structs.CommitStatusError, State: structs.CommitStatusError,
}, },
}, },
{
statuses: []*git_model.CommitStatus{},
expected: nil,
},
} }
for _, kase := range kases { for _, kase := range kases {

View file

@ -194,20 +194,6 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
return issue.Repo.IsTimetrackerEnabled(ctx) return issue.Repo.IsTimetrackerEnabled(ctx)
} }
// GetPullRequest returns the issue pull request
func (issue *Issue) GetPullRequest(ctx context.Context) (pr *PullRequest, err error) {
if !issue.IsPull {
return nil, fmt.Errorf("Issue is not a pull request")
}
pr, err = GetPullRequestByIssueID(ctx, issue.ID)
if err != nil {
return nil, err
}
pr.Issue = issue
return pr, err
}
// LoadPoster loads poster // LoadPoster loads poster
func (issue *Issue) LoadPoster(ctx context.Context) (err error) { func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
if issue.Poster == nil && issue.PosterID != 0 { if issue.Poster == nil && issue.PosterID != 0 {
@ -500,7 +486,7 @@ func (issue *Issue) GetLastEventLabelFake() string {
// GetIssueByIndex returns raw issue without loading attributes by index in a repository. // GetIssueByIndex returns raw issue without loading attributes by index in a repository.
func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) { func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
if index < 1 { if index < 1 {
return nil, ErrIssueNotExist{} return nil, ErrIssueNotExist{0, repoID, index}
} }
issue := &Issue{ issue := &Issue{
RepoID: repoID, RepoID: repoID,

View file

@ -9,14 +9,6 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
) )
func GetMaxIssueIndexForRepo(ctx context.Context, repoID int64) (int64, error) {
var max int64
if _, err := db.GetEngine(ctx).Select("MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
return 0, err
}
return max, nil
}
// RecalculateIssueIndexForRepo create issue_index for repo if not exist and // RecalculateIssueIndexForRepo create issue_index for repo if not exist and
// update it based on highest index of existing issues assigned to a repo // update it based on highest index of existing issues assigned to a repo
func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error { func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error {
@ -26,8 +18,8 @@ func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error {
} }
defer committer.Close() defer committer.Close()
max, err := GetMaxIssueIndexForRepo(ctx, repoID) var max int64
if err != nil { if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
return err return err
} }

View file

@ -1,38 +0,0 @@
// Copyright 2024 The Forgejo Authors
// SPDX-License-Identifier: MIT
package issues_test
import (
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestGetMaxIssueIndexForRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
maxPR, err := issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID)
assert.NoError(t, err)
issue := testCreateIssue(t, repo.ID, repo.OwnerID, "title1", "content1", false)
assert.Greater(t, issue.Index, maxPR)
maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID)
assert.NoError(t, err)
pull := testCreateIssue(t, repo.ID, repo.OwnerID, "title2", "content2", true)
assert.Greater(t, pull.Index, maxPR)
maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID)
assert.NoError(t, err)
assert.Equal(t, maxPR, pull.Index)
}

View file

@ -370,6 +370,9 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error {
for _, issue := range issues { for _, issue := range issues {
issue.PullRequest = pullRequestMaps[issue.ID] issue.PullRequest = pullRequestMaps[issue.ID]
if issue.PullRequest != nil {
issue.PullRequest.Issue = issue
}
} }
return nil return nil
} }

View file

@ -19,7 +19,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -884,92 +883,6 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
} }
func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullRequest) error {
files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
if pr.IsWorkInProgress(ctx) {
return nil
}
if err := pull.LoadRepo(ctx); err != nil {
return err
}
if pull.Repo.IsFork {
return nil
}
if err := pr.LoadBaseRepo(ctx); err != nil {
return err
}
repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
if err != nil {
return err
}
defer repo.Close()
commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
if err != nil {
return err
}
var data string
for _, file := range files {
if blob, err := commit.GetBlobByPath(file); err == nil {
data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err == nil {
break
}
}
}
rules, _ := GetCodeOwnersFromContent(ctx, data)
prInfo, err := repo.GetCompareInfo(repo.Path, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName(), false, false)
if err != nil {
return err
}
// Use the merge base as the base instead of the main branch to avoid problems
// if the pull request is out of date with the base branch.
changedFiles, err := repo.GetFilesChangedBetween(prInfo.MergeBase, prInfo.HeadCommitID)
if err != nil {
return err
}
uniqUsers := make(map[int64]*user_model.User)
uniqTeams := make(map[string]*org_model.Team)
for _, rule := range rules {
for _, f := range changedFiles {
if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) {
for _, u := range rule.Users {
uniqUsers[u.ID] = u
}
for _, t := range rule.Teams {
uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t
}
}
}
}
for _, u := range uniqUsers {
if u.ID != pull.Poster.ID {
if _, err := AddReviewRequest(ctx, pull, u, pull.Poster); err != nil {
log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
return err
}
}
}
for _, t := range uniqTeams {
if _, err := AddTeamReviewRequest(ctx, pull, t, pull.Poster); err != nil {
log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
return err
}
}
return nil
}
// GetCodeOwnersFromContent returns the code owners configuration // GetCodeOwnersFromContent returns the code owners configuration
// Return empty slice if files missing // Return empty slice if files missing
// Return warning messages on parsing errors // Return warning messages on parsing errors

View file

@ -11,7 +11,6 @@ import (
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -23,7 +22,7 @@ type PullRequestsOptions struct {
db.ListOptions db.ListOptions
State string State string
SortType string SortType string
Labels []string Labels []int64
MilestoneID int64 MilestoneID int64
} }
@ -36,11 +35,9 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
sess.And("issue.is_closed=?", opts.State == "closed") sess.And("issue.is_closed=?", opts.State == "closed")
} }
if labelIDs, err := base.StringsToInt64s(opts.Labels); err != nil { if len(opts.Labels) > 0 {
return nil, err
} else if len(labelIDs) > 0 {
sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id"). sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id").
In("issue_label.label_id", labelIDs) In("issue_label.label_id", opts.Labels)
} }
if opts.MilestoneID > 0 { if opts.MilestoneID > 0 {
@ -50,14 +47,6 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
return sess, nil return sess, nil
} }
func GetUnmergedPullRequestsByHeadInfoMax(ctx context.Context, repoID, maxIndex int64, branch string) ([]*PullRequest, error) {
prs := make([]*PullRequest, 0, 2)
sess := db.GetEngine(ctx).
Join("INNER", "issue", "issue.id = `pull_request`.issue_id").
Where("`pull_request`.head_repo_id = ? AND `pull_request`.head_branch = ? AND `pull_request`.has_merged = ? AND `issue`.is_closed = ? AND `pull_request`.flow = ? AND `issue`.`index` <= ?", repoID, branch, false, false, PullRequestFlowGithub, maxIndex)
return prs, sess.Find(&prs)
}
// GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged // GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged
func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) { func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) {
prs := make([]*PullRequest, 0, 2) prs := make([]*PullRequest, 0, 2)
@ -220,3 +209,12 @@ func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bo
Limit(1). Limit(1).
Get(new(Issue)) Get(new(Issue))
} }
// GetPullRequestByIssueIDs returns all pull requests by issue ids
func GetPullRequestByIssueIDs(ctx context.Context, issueIDs []int64) (PullRequestList, error) {
prs := make([]*PullRequest, 0, len(issueIDs))
return prs, db.GetEngine(ctx).
Where("issue_id > 0").
In("issue_id", issueIDs).
Find(&prs)
}

View file

@ -4,7 +4,6 @@
package issues_test package issues_test
import ( import (
"fmt"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -67,7 +66,6 @@ func TestPullRequestsNewest(t *testing.T) {
}, },
State: "open", State: "open",
SortType: "newest", SortType: "newest",
Labels: []string{},
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 3, count) assert.EqualValues(t, 3, count)
@ -114,7 +112,6 @@ func TestPullRequestsOldest(t *testing.T) {
}, },
State: "open", State: "open",
SortType: "oldest", SortType: "oldest",
Labels: []string{},
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 3, count) assert.EqualValues(t, 3, count)
@ -159,91 +156,6 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
} }
} }
func TestGetUnmergedPullRequestsByHeadInfoMax(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repoID := int64(1)
maxPR := int64(0)
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2")
assert.NoError(t, err)
assert.Len(t, prs, 0)
maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repoID)
assert.NoError(t, err)
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2")
assert.NoError(t, err)
assert.Len(t, prs, 1)
for _, pr := range prs {
assert.Equal(t, int64(1), pr.HeadRepoID)
assert.Equal(t, "branch2", pr.HeadBranch)
}
pr := prs[0]
for _, testCase := range []struct {
table string
field string
id int64
match any
nomatch any
}{
{
table: "issue",
field: "is_closed",
id: pr.IssueID,
match: false,
nomatch: true,
},
{
table: "pull_request",
field: "flow",
id: pr.ID,
match: issues_model.PullRequestFlowGithub,
nomatch: issues_model.PullRequestFlowAGit,
},
{
table: "pull_request",
field: "head_repo_id",
id: pr.ID,
match: pr.HeadRepoID,
nomatch: 0,
},
{
table: "pull_request",
field: "head_branch",
id: pr.ID,
match: pr.HeadBranch,
nomatch: "something else",
},
{
table: "pull_request",
field: "has_merged",
id: pr.ID,
match: false,
nomatch: true,
},
} {
t.Run(testCase.field, func(t *testing.T) {
update := fmt.Sprintf("UPDATE `%s` SET `%s` = ? WHERE `id` = ?", testCase.table, testCase.field)
// expect no match
_, err = db.GetEngine(db.DefaultContext).Exec(update, testCase.nomatch, testCase.id)
assert.NoError(t, err)
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2")
assert.NoError(t, err)
assert.Len(t, prs, 0)
// expect one match
_, err = db.GetEngine(db.DefaultContext).Exec(update, testCase.match, testCase.id)
assert.NoError(t, err)
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2")
assert.NoError(t, err)
assert.Len(t, prs, 1)
// identical to the known PR
assert.Equal(t, pr.ID, prs[0].ID)
})
}
}
func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(db.DefaultContext, 1, "master") prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(db.DefaultContext, 1, "master")

View file

@ -239,11 +239,11 @@ type CreateReviewOptions struct {
// IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals) // IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.User) (bool, error) { func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.User) (bool, error) {
pr, err := GetPullRequestByIssueID(ctx, issue.ID) if err := issue.LoadPullRequest(ctx); err != nil {
if err != nil {
return false, err return false, err
} }
pr := issue.PullRequest
rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil { if err != nil {
return false, err return false, err
@ -271,11 +271,10 @@ func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.
// IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals) // IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) { func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) {
pr, err := GetPullRequestByIssueID(ctx, issue.ID) if err := issue.LoadPullRequest(ctx); err != nil {
if err != nil {
return false, err return false, err
} }
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, issue.PullRequest.BaseRepoID, issue.PullRequest.BaseBranch)
if err != nil { if err != nil {
return false, err return false, err
} }

View file

@ -160,6 +160,10 @@ func MainTest(m *testing.M) {
exitStatus := m.Run() exitStatus := m.Run()
if err := testlogger.WriterCloser.Reset(); err != nil && exitStatus == 0 {
fmt.Printf("testlogger.WriterCloser.Reset: %v\n", err)
os.Exit(1)
}
if err := removeAllWithRetry(setting.RepoRootPath); err != nil { if err := removeAllWithRetry(setting.RepoRootPath); err != nil {
fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
} }

View file

@ -0,0 +1,22 @@
# type Release struct {
# ID int64 `xorm:"pk autoincr"`
# Note string `xorm:"TEXT"`
# }
-
id: 1
note: |
-----BEGIN SSH SIGNATURE-----
some signature
-----END SSH SIGNATURE-----
-
id: 2
note: |
A message.
-----BEGIN SSH SIGNATURE-----
some signature
-----END SSH SIGNATURE-----
-
id: 3
note: "no signature present here"

View file

@ -23,9 +23,9 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
Type int Type int
// Permissions // Permissions
IsAdmin bool IsAdmin bool
IsRestricted bool `xorm:"NOT NULL DEFAULT false"` // IsRestricted bool `xorm:"NOT NULL DEFAULT false"` glitch: this column was added in v1_12/v121.go
Visibility int `xorm:"NOT NULL DEFAULT 0"` // Visibility int `xorm:"NOT NULL DEFAULT 0"` glitch: this column was added in v1_12/v124.go
} }
type Review struct { type Review struct {
@ -51,9 +51,9 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
ReviewTypeReject int = 3 ReviewTypeReject int = 3
// VisibleTypePublic Visible for everyone // VisibleTypePublic Visible for everyone
VisibleTypePublic int = 0 // VisibleTypePublic int = 0
// VisibleTypePrivate Visible only for organization's members // VisibleTypePrivate Visible only for organization's members
VisibleTypePrivate int = 2 // VisibleTypePrivate int = 2
// unit.UnitTypeCode is unit type code // unit.UnitTypeCode is unit type code
UnitTypeCode int = 1 UnitTypeCode int = 1
@ -145,9 +145,9 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
hasOrgVisible := true hasOrgVisible := true
// Not SignedUser // Not SignedUser
if user == nil { if user == nil {
hasOrgVisible = repoOwner.Visibility == VisibleTypePublic // hasOrgVisible = repoOwner.Visibility == VisibleTypePublic // VisibleTypePublic is the default
} else if !user.IsAdmin { } else if !user.IsAdmin {
hasMemberWithUserID, err := sess. _, err := sess.
Where("uid=?", user.ID). Where("uid=?", user.ID).
And("org_id=?", repoOwner.ID). And("org_id=?", repoOwner.ID).
Table("org_user"). Table("org_user").
@ -155,9 +155,10 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
if err != nil { if err != nil {
hasOrgVisible = false hasOrgVisible = false
} }
if (repoOwner.Visibility == VisibleTypePrivate || user.IsRestricted) && !hasMemberWithUserID { // VisibleTypePublic is the default so the condition below is always false
hasOrgVisible = false // if (repoOwner.Visibility == VisibleTypePrivate) && !hasMemberWithUserID {
} // hasOrgVisible = false
// }
} }
isCollaborator, err := sess.Get(&Collaboration{RepoID: repo.ID, UserID: user.ID}) isCollaborator, err := sess.Get(&Collaboration{RepoID: repo.ID, UserID: user.ID})
@ -195,7 +196,7 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
if user != nil { if user != nil {
userID = user.ID userID = user.ID
restricted = user.IsRestricted restricted = false
} }
if !restricted && !repo.IsPrivate { if !restricted && !repo.IsPrivate {
@ -284,7 +285,7 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
} }
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units. // for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
if !found && !repo.IsPrivate && !user.IsRestricted { if !found && !repo.IsPrivate {
if _, ok := perm.UnitsMode[u.Type]; !ok { if _, ok := perm.UnitsMode[u.Type]; !ok {
perm.UnitsMode[u.Type] = AccessModeRead perm.UnitsMode[u.Type] = AccessModeRead
} }

View file

@ -94,7 +94,7 @@ func FixMergeBase(x *xorm.Engine) error {
} else { } else {
parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath})
if err != nil { if err != nil {
log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) log.Warn("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err)
continue continue
} }
parents := strings.Split(strings.TrimSpace(parentsString), " ") parents := strings.Split(strings.TrimSpace(parentsString), " ")

View file

@ -81,7 +81,7 @@ func RefixMergeBase(x *xorm.Engine) error {
parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath})
if err != nil { if err != nil {
log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) log.Warn("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err)
continue continue
} }
parents := strings.Split(strings.TrimSpace(parentsString), " ") parents := strings.Split(strings.TrimSpace(parentsString), " ")

View file

@ -319,8 +319,9 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode
// Add initial creator to organization and owner team. // Add initial creator to organization and owner team.
if err = db.Insert(ctx, &OrgUser{ if err = db.Insert(ctx, &OrgUser{
UID: owner.ID, UID: owner.ID,
OrgID: org.ID, OrgID: org.ID,
IsPublic: setting.Service.DefaultOrgMemberVisible,
}); err != nil { }); err != nil {
return fmt.Errorf("insert org-user relation: %w", err) return fmt.Errorf("insert org-user relation: %w", err)
} }

View file

@ -316,29 +316,3 @@ func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error {
_, err := db.GetEngine(ctx).ID(unit.ID).Update(unit) _, err := db.GetEngine(ctx).ID(unit.ID).Update(unit)
return err return err
} }
// UpdateRepositoryUnits updates a repository's units
func UpdateRepositoryUnits(ctx context.Context, repo *Repository, units []RepoUnit, deleteUnitTypes []unit.Type) (err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
// Delete existing settings of units before adding again
for _, u := range units {
deleteUnitTypes = append(deleteUnitTypes, u.Type)
}
if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(RepoUnit)); err != nil {
return err
}
if len(units) > 0 {
if err = db.Insert(ctx, units); err != nil {
return err
}
}
return committer.Commit()
}

View file

@ -443,7 +443,7 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()}) cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()})
} }
count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid"). count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.id = email_address.uid").
Where(cond).Count(new(EmailAddress)) Where(cond).Count(new(EmailAddress))
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("Count: %w", err) return nil, 0, fmt.Errorf("Count: %w", err)
@ -459,7 +459,7 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
emails := make([]*SearchEmailResult, 0, opts.PageSize) emails := make([]*SearchEmailResult, 0, opts.PageSize)
err = db.GetEngine(ctx).Table("email_address"). err = db.GetEngine(ctx).Table("email_address").
Select("email_address.*, `user`.name, `user`.full_name"). Select("email_address.*, `user`.name, `user`.full_name").
Join("INNER", "`user`", "`user`.ID = email_address.uid"). Join("INNER", "`user`", "`user`.id = email_address.uid").
Where(cond). Where(cond).
OrderBy(orderby). OrderBy(orderby).
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize). Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).

View file

@ -146,6 +146,7 @@ type User struct {
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
Theme string `xorm:"NOT NULL DEFAULT ''"` Theme string `xorm:"NOT NULL DEFAULT ''"`
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"` KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
EnableRepoUnitHints bool `xorm:"NOT NULL DEFAULT true"`
} }
func init() { func init() {

View file

@ -451,6 +451,13 @@ func (opts ListWebhookOptions) ToConds() builder.Cond {
return cond return cond
} }
var _ db.FindOptionsOrder = ListWebhookOptions{}
// ToOrders implements db.FindOptionsOrder, to sort the webhooks by id asc
func (opts ListWebhookOptions) ToOrders() string {
return "webhook.id"
}
// UpdateWebhook updates information of webhook. // UpdateWebhook updates information of webhook.
func UpdateWebhook(ctx context.Context, w *Webhook) error { func UpdateWebhook(ctx context.Context, w *Webhook) error {
_, err := db.GetEngine(ctx).ID(w.ID).AllCols().Update(w) _, err := db.GetEngine(ctx).ID(w.ID).AllCols().Update(w)

View file

@ -124,6 +124,9 @@ func TestGetWebhookByOwnerID(t *testing.T) {
func TestGetActiveWebhooksByRepoID(t *testing.T) { func TestGetActiveWebhooksByRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
activateWebhook(t, 1)
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)}) hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
assert.NoError(t, err) assert.NoError(t, err)
if assert.Len(t, hooks, 1) { if assert.Len(t, hooks, 1) {
@ -144,6 +147,9 @@ func TestGetWebhooksByRepoID(t *testing.T) {
func TestGetActiveWebhooksByOwnerID(t *testing.T) { func TestGetActiveWebhooksByOwnerID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
activateWebhook(t, 3)
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)}) hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
assert.NoError(t, err) assert.NoError(t, err)
if assert.Len(t, hooks, 1) { if assert.Len(t, hooks, 1) {
@ -152,8 +158,18 @@ func TestGetActiveWebhooksByOwnerID(t *testing.T) {
} }
} }
func activateWebhook(t *testing.T, hookID int64) {
t.Helper()
updated, err := db.GetEngine(db.DefaultContext).ID(hookID).Cols("is_active").Update(Webhook{IsActive: true})
assert.Equal(t, int64(1), updated)
assert.NoError(t, err)
}
func TestGetWebhooksByOwnerID(t *testing.T) { func TestGetWebhooksByOwnerID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
activateWebhook(t, 3)
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3}) hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3})
assert.NoError(t, err) assert.NoError(t, err)
if assert.Len(t, hooks, 1) { if assert.Len(t, hooks, 1) {

View file

@ -74,9 +74,6 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
case GithubEventGollum: case GithubEventGollum:
return triggedEvent == webhook_module.HookEventWiki return triggedEvent == webhook_module.HookEventWiki
case GithubEventSchedule:
return triggedEvent == webhook_module.HookEventSchedule
case GithubEventIssues: case GithubEventIssues:
switch triggedEvent { switch triggedEvent {
case webhook_module.HookEventIssues, case webhook_module.HookEventIssues,
@ -119,6 +116,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
return triggedEvent == webhook_module.HookEventIssueComment || return triggedEvent == webhook_module.HookEventIssueComment ||
triggedEvent == webhook_module.HookEventPullRequestComment triggedEvent == webhook_module.HookEventPullRequestComment
case GithubEventSchedule:
return triggedEvent == webhook_module.HookEventSchedule
default: default:
return eventName == string(triggedEvent) return eventName == string(triggedEvent)
} }

View file

@ -100,7 +100,7 @@ func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limi
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scan: %w", err) return nil, fmt.Errorf("ReadLogs scan: %w", err)
} }
return rows, nil return rows, nil

View file

@ -41,6 +41,12 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
} }
logIndex += preStep.LogLength logIndex += preStep.LogLength
// lastHasRunStep is the last step that has run.
// For example,
// 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1.
// 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3.
// 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2.
// So its Stopped is the Started of postStep when there are no more steps to run.
var lastHasRunStep *actions_model.ActionTaskStep var lastHasRunStep *actions_model.ActionTaskStep
for _, step := range task.Steps { for _, step := range task.Steps {
if step.Status.HasRun() { if step.Status.HasRun() {
@ -56,11 +62,15 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
Name: postStepName, Name: postStepName,
Status: actions_model.StatusWaiting, Status: actions_model.StatusWaiting,
} }
if task.Status.IsDone() { // If the lastHasRunStep is the last step, or it has failed, postStep has started.
if lastHasRunStep.Status.IsFailure() || lastHasRunStep == task.Steps[len(task.Steps)-1] {
postStep.LogIndex = logIndex postStep.LogIndex = logIndex
postStep.LogLength = task.LogLength - postStep.LogIndex postStep.LogLength = task.LogLength - postStep.LogIndex
postStep.Status = task.Status
postStep.Started = lastHasRunStep.Stopped postStep.Started = lastHasRunStep.Stopped
postStep.Status = actions_model.StatusRunning
}
if task.Status.IsDone() {
postStep.Status = task.Status
postStep.Stopped = task.Stopped postStep.Stopped = task.Stopped
} }
ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2) ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2)

View file

@ -103,6 +103,40 @@ func TestFullSteps(t *testing.T) {
{Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100}, {Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100},
}, },
}, },
{
name: "all steps finished but task is running",
task: &actions_model.ActionTask{
Steps: []*actions_model.ActionTaskStep{
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
},
Status: actions_model.StatusRunning,
Started: 10000,
Stopped: 0,
LogLength: 100,
},
want: []*actions_model.ActionTaskStep{
{Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010},
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
{Name: postStepName, Status: actions_model.StatusRunning, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 0},
},
},
{
name: "skipped task",
task: &actions_model.ActionTask{
Steps: []*actions_model.ActionTaskStep{
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
},
Status: actions_model.StatusSkipped,
Started: 0,
Stopped: 0,
LogLength: 0,
},
want: []*actions_model.ActionTaskStep{
{Name: preStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
{Name: postStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View file

@ -22,7 +22,7 @@ import (
type DetectedWorkflow struct { type DetectedWorkflow struct {
EntryName string EntryName string
TriggerEvent string TriggerEvent *jobparser.Event
Content []byte Content []byte
} }
@ -103,6 +103,7 @@ func DetectWorkflows(
commit *git.Commit, commit *git.Commit,
triggedEvent webhook_module.HookEventType, triggedEvent webhook_module.HookEventType,
payload api.Payloader, payload api.Payloader,
detectSchedule bool,
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { ) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit) entries, err := ListWorkflows(commit)
if err != nil { if err != nil {
@ -117,6 +118,7 @@ func DetectWorkflows(
return nil, nil, err return nil, nil, err
} }
// one workflow may have multiple events
events, err := GetEventsFromContent(content) events, err := GetEventsFromContent(content)
if err != nil { if err != nil {
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
@ -125,17 +127,18 @@ func DetectWorkflows(
for _, evt := range events { for _, evt := range events {
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent) log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
if evt.IsSchedule() { if evt.IsSchedule() {
dwf := &DetectedWorkflow{ if detectSchedule {
EntryName: entry.Name(), dwf := &DetectedWorkflow{
TriggerEvent: evt.Name, EntryName: entry.Name(),
Content: content, TriggerEvent: evt,
Content: content,
}
schedules = append(schedules, dwf)
} }
schedules = append(schedules, dwf) } else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
}
if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
dwf := &DetectedWorkflow{ dwf := &DetectedWorkflow{
EntryName: entry.Name(), EntryName: entry.Name(),
TriggerEvent: evt.Name, TriggerEvent: evt,
Content: content, Content: content,
} }
workflows = append(workflows, dwf) workflows = append(workflows, dwf)
@ -146,6 +149,41 @@ func DetectWorkflows(
return workflows, schedules, nil return workflows, schedules, nil
} }
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit)
if err != nil {
return nil, err
}
wfs := make([]*DetectedWorkflow, 0, len(entries))
for _, entry := range entries {
content, err := GetContentFromEntry(entry)
if err != nil {
return nil, err
}
// one workflow may have multiple events
events, err := GetEventsFromContent(content)
if err != nil {
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
continue
}
for _, evt := range events {
if evt.IsSchedule() {
log.Trace("detect scheduled workflow: %q", entry.Name())
dwf := &DetectedWorkflow{
EntryName: entry.Name(),
TriggerEvent: evt,
Content: content,
}
wfs = append(wfs, dwf)
}
}
}
return wfs, nil
}
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
if !canGithubEventMatch(evt.Name, triggedEvent) { if !canGithubEventMatch(evt.Name, triggedEvent) {
return false return false
@ -153,11 +191,11 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
switch triggedEvent { switch triggedEvent {
case // events with no activity types case // events with no activity types
webhook_module.HookEventSchedule,
webhook_module.HookEventCreate, webhook_module.HookEventCreate,
webhook_module.HookEventDelete, webhook_module.HookEventDelete,
webhook_module.HookEventFork, webhook_module.HookEventFork,
webhook_module.HookEventWiki: webhook_module.HookEventWiki,
webhook_module.HookEventSchedule:
if len(evt.Acts()) != 0 { if len(evt.Acts()) != 0 {
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts()) log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
} }

View file

@ -150,13 +150,16 @@ func TruncateString(str string, limit int) string {
// StringsToInt64s converts a slice of string to a slice of int64. // StringsToInt64s converts a slice of string to a slice of int64.
func StringsToInt64s(strs []string) ([]int64, error) { func StringsToInt64s(strs []string) ([]int64, error) {
ints := make([]int64, len(strs)) if strs == nil {
for i := range strs { return nil, nil
n, err := strconv.ParseInt(strs[i], 10, 64) }
ints := make([]int64, 0, len(strs))
for _, s := range strs {
n, err := strconv.ParseInt(s, 10, 64)
if err != nil { if err != nil {
return ints, err return nil, err
} }
ints[i] = n ints = append(ints, n)
} }
return ints, nil return ints, nil
} }

View file

@ -138,12 +138,13 @@ func TestStringsToInt64s(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
} }
testSuccess(nil, nil)
testSuccess([]string{}, []int64{}) testSuccess([]string{}, []int64{})
testSuccess([]string{"-1234"}, []int64{-1234}) testSuccess([]string{"-1234"}, []int64{-1234})
testSuccess([]string{"1", "4", "16", "64", "256"}, testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})
[]int64{1, 4, 16, 64, 256})
_, err := StringsToInt64s([]string{"-1", "a", "$"}) ints, err := StringsToInt64s([]string{"-1", "a"})
assert.Len(t, ints, 0)
assert.Error(t, err) assert.Error(t, err)
} }

View file

@ -367,7 +367,6 @@ type RunStdError interface {
error error
Unwrap() error Unwrap() error
Stderr() string Stderr() string
IsExitCode(code int) bool
} }
type runStdError struct { type runStdError struct {
@ -392,9 +391,9 @@ func (r *runStdError) Stderr() string {
return r.stderr return r.stderr
} }
func (r *runStdError) IsExitCode(code int) bool { func IsErrorExitCode(err error, code int) bool {
var exitError *exec.ExitError var exitError *exec.ExitError
if errors.As(r.err, &exitError) { if errors.As(err, &exitError) {
return exitError.ExitCode() == code return exitError.ExitCode() == code
} }
return false return false

View file

@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"os/exec" "os/exec"
"strconv" "strconv"
@ -25,18 +26,12 @@ type Commit struct {
Author *Signature Author *Signature
Committer *Signature Committer *Signature
CommitMessage string CommitMessage string
Signature *CommitGPGSignature Signature *ObjectSignature
Parents []ObjectID // ID strings Parents []ObjectID // ID strings
submoduleCache *ObjectCache submoduleCache *ObjectCache
} }
// CommitGPGSignature represents a git commit signature part.
type CommitGPGSignature struct {
Signature string
Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
}
// Message returns the commit message. Same as retrieving CommitMessage directly. // Message returns the commit message. Same as retrieving CommitMessage directly.
func (c *Commit) Message() string { func (c *Commit) Message() string {
return c.CommitMessage return c.CommitMessage
@ -396,6 +391,9 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) {
} }
} }
} }
if err = scanner.Err(); err != nil {
return nil, fmt.Errorf("GetSubModules scan: %w", err)
}
return c.submoduleCache, nil return c.submoduleCache, nil
} }

View file

@ -13,7 +13,7 @@ import (
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
) )
func convertPGPSignature(c *object.Commit) *CommitGPGSignature { func convertPGPSignature(c *object.Commit) *ObjectSignature {
if c.PGPSignature == "" { if c.PGPSignature == "" {
return nil return nil
} }
@ -51,7 +51,7 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
return nil return nil
} }
return &CommitGPGSignature{ return &ObjectSignature{
Signature: c.PGPSignature, Signature: c.PGPSignature,
Payload: w.String(), Payload: w.String(),
} }

View file

@ -97,7 +97,7 @@ readLoop:
} }
} }
commit.CommitMessage = messageSB.String() commit.CommitMessage = messageSB.String()
commit.Signature = &CommitGPGSignature{ commit.Signature = &ObjectSignature{
Signature: signatureSB.String(), Signature: signatureSB.String(),
Payload: payloadSB.String(), Payload: payloadSB.String(),
} }

View file

@ -33,9 +33,10 @@ var (
// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx // DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
DefaultContext context.Context DefaultContext context.Context
SupportProcReceive bool // >= 2.29 SupportProcReceive bool // >= 2.29
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an experimental curiosity SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an experimental curiosity
InvertedGitFlushEnv bool // 2.43.1 InvertedGitFlushEnv bool // 2.43.1
SupportCheckAttrOnBare bool // >= 2.40
gitVersion *version.Version gitVersion *version.Version
) )
@ -187,6 +188,7 @@ func InitFull(ctx context.Context) (err error) {
} }
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit
SupportCheckAttrOnBare = CheckGitVersionAtLeast("2.40") == nil
if SupportHashSha256 { if SupportHashSha256 {
SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat) SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat)
} else { } else {
@ -340,7 +342,7 @@ func CheckGitVersionEqual(equal string) error {
func configSet(key, value string) error { func configSet(key, value string) error {
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
if err != nil && !err.IsExitCode(1) { if err != nil && !IsErrorExitCode(err, 1) {
return fmt.Errorf("failed to get git config %s, err: %w", key, err) return fmt.Errorf("failed to get git config %s, err: %w", key, err)
} }
@ -363,7 +365,7 @@ func configSetNonExist(key, value string) error {
// already exist // already exist
return nil return nil
} }
if err.IsExitCode(1) { if IsErrorExitCode(err, 1) {
// not exist, set new config // not exist, set new config
_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
if err != nil { if err != nil {
@ -381,7 +383,7 @@ func configAddNonExist(key, value string) error {
// already exist // already exist
return nil return nil
} }
if err.IsExitCode(1) { if IsErrorExitCode(err, 1) {
// not exist, add new config // not exist, add new config
_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil) _, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil)
if err != nil { if err != nil {
@ -402,7 +404,7 @@ func configUnsetAll(key, value string) error {
} }
return nil return nil
} }
if err.IsExitCode(1) { if IsErrorExitCode(err, 1) {
// not exist // not exist
return nil return nil
} }

117
modules/git/grep.go Normal file
View file

@ -0,0 +1,117 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bufio"
"bytes"
"cmp"
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"
)
type GrepResult struct {
Filename string
LineNumbers []int
LineCodes []string
}
type GrepOptions struct {
RefName string
MaxResultLimit int
ContextLineNumber int
IsFuzzy bool
}
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("unable to create os pipe to grep: %w", err)
}
defer func() {
_ = stdoutReader.Close()
_ = stdoutWriter.Close()
}()
/*
The output is like this ( "^@" means \x00):
HEAD:.air.toml
6^@bin = "gitea"
HEAD:.changelog.yml
2^@repo: go-gitea/gitea
*/
var results []*GrepResult
cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name")
cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber))
if opts.IsFuzzy {
words := strings.Fields(search)
for _, word := range words {
cmd.AddOptionValues("-e", strings.TrimLeft(word, "-"))
}
} else {
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
}
cmd.AddDynamicArguments(cmp.Or(opts.RefName, "HEAD"))
opts.MaxResultLimit = cmp.Or(opts.MaxResultLimit, 50)
stderr := bytes.Buffer{}
err = cmd.Run(&RunOpts{
Dir: repo.Path,
Stdout: stdoutWriter,
Stderr: &stderr,
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
_ = stdoutWriter.Close()
defer stdoutReader.Close()
isInBlock := false
scanner := bufio.NewScanner(stdoutReader)
var res *GrepResult
for scanner.Scan() {
line := scanner.Text()
if !isInBlock {
if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok {
isInBlock = true
res = &GrepResult{Filename: filename}
results = append(results, res)
}
continue
}
if line == "" {
if len(results) >= opts.MaxResultLimit {
cancel()
break
}
isInBlock = false
continue
}
if line == "--" {
continue
}
if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok {
lineNumInt, _ := strconv.Atoi(lineNum)
res.LineNumbers = append(res.LineNumbers, lineNumInt)
res.LineCodes = append(res.LineCodes, lineCode)
}
}
return scanner.Err()
},
})
// git grep exits by cancel (killed), usually it is caused by the limit of results
if IsErrorExitCode(err, -1) && stderr.Len() == 0 {
return results, nil
}
// git grep exits with 1 if no results are found
if IsErrorExitCode(err, 1) && stderr.Len() == 0 {
return nil, nil
}
if err != nil && !errors.Is(err, context.Canceled) {
return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String())
}
return results, nil
}

51
modules/git/grep_test.go Normal file
View file

@ -0,0 +1,51 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGrepSearch(t *testing.T) {
repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "language_stats_repo"))
assert.NoError(t, err)
defer repo.Close()
res, err := GrepSearch(context.Background(), repo, "void", GrepOptions{})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "java-hello/main.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
{
Filename: "main.vendor.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "java-hello/main.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{})
assert.NoError(t, err)
assert.Len(t, res, 0)
res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{})
assert.Error(t, err)
assert.Len(t, res, 0)
}

View file

@ -0,0 +1,11 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
// ObjectSignature represents a git object (commit, tag) signature part.
type ObjectSignature struct {
Signature string
Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
}

View file

@ -283,7 +283,7 @@ type DivergeObject struct {
// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) { func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) {
cmd := NewCommand(ctx, "rev-list", "--count", "--left-right"). cmd := NewCommand(ctx, "rev-list", "--count", "--left-right").
AddDynamicArguments(baseBranch + "..." + targetBranch) AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")
stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
if err != nil { if err != nil {
return do, err return do, err

View file

@ -7,65 +7,147 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"strings"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
) )
// CheckAttributeOpts represents the possible options to CheckAttribute var LinguistAttributes = []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"}
type CheckAttributeOpts struct {
CachedOnly bool // GitAttribute exposes an attribute from the .gitattribute file
AllAttributes bool type GitAttribute string //nolint:revive
Attributes []string
Filenames []string // IsSpecified returns true if the gitattribute is set and not empty
IndexFile string func (ca GitAttribute) IsSpecified() bool {
WorkTree string return ca != "" && ca != "unspecified"
} }
// CheckAttribute return the Blame object of file // String returns the value of the attribute or "" if unspecified
func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) { func (ca GitAttribute) String() string {
env := []string{} if !ca.IsSpecified() {
return ""
if len(opts.IndexFile) > 0 {
env = append(env, "GIT_INDEX_FILE="+opts.IndexFile)
} }
if len(opts.WorkTree) > 0 { return string(ca)
env = append(env, "GIT_WORK_TREE="+opts.WorkTree) }
// Prefix returns the value of the attribute before any question mark '?'
//
// sometimes used within gitlab-language: https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type
func (ca GitAttribute) Prefix() string {
s := ca.String()
if i := strings.IndexByte(s, '?'); i >= 0 {
return s[:i]
}
return s
}
// Bool returns true if "set"/"true", false if "unset"/"false", none otherwise
func (ca GitAttribute) Bool() optional.Option[bool] {
switch ca {
case "set", "true":
return optional.Some(true)
case "unset", "false":
return optional.Some(false)
}
return optional.None[bool]()
}
// GitAttributeFirst returns the first specified attribute
//
// If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare).
func (repo *Repository) GitAttributeFirst(treeish, filename string, attributes ...string) (GitAttribute, error) {
values, err := repo.GitAttributes(treeish, filename, attributes...)
if err != nil {
return "", err
}
for _, a := range attributes {
if values[a].IsSpecified() {
return values[a], nil
}
}
return "", nil
}
func (repo *Repository) gitCheckAttrCommand(treeish string, attributes ...string) (*Command, *RunOpts, context.CancelFunc, error) {
if len(attributes) == 0 {
return nil, nil, nil, fmt.Errorf("no provided attributes to check-attr")
} }
if len(env) > 0 { env := os.Environ()
env = append(os.Environ(), env...) var deleteTemporaryFile context.CancelFunc
// git < 2.40 cannot run check-attr on bare repo, but needs INDEX + WORK_TREE
hasIndex := treeish == ""
if !hasIndex && !SupportCheckAttrOnBare {
indexFilename, worktree, cancel, err := repo.ReadTreeToTemporaryIndex(treeish)
if err != nil {
return nil, nil, nil, err
}
deleteTemporaryFile = cancel
env = append(env, "GIT_INDEX_FILE="+indexFilename, "GIT_WORK_TREE="+worktree)
hasIndex = true
// clear treeish to read from provided index/work_tree
treeish = ""
} }
ctx, cancel := context.WithCancel(repo.Ctx)
stdOut := new(bytes.Buffer) if deleteTemporaryFile != nil {
stdErr := new(bytes.Buffer) ctxCancel := cancel
cancel = func() {
cmd := NewCommand(repo.Ctx, "check-attr", "-z") ctxCancel()
deleteTemporaryFile()
if opts.AllAttributes {
cmd.AddArguments("-a")
} else {
for _, attribute := range opts.Attributes {
if attribute != "" {
cmd.AddDynamicArguments(attribute)
}
} }
} }
if opts.CachedOnly { cmd := NewCommand(ctx, "check-attr", "-z")
if hasIndex {
cmd.AddArguments("--cached") cmd.AddArguments("--cached")
} }
cmd.AddDashesAndList(opts.Filenames...) if len(treeish) > 0 {
cmd.AddArguments("--source")
cmd.AddDynamicArguments(treeish)
}
cmd.AddDynamicArguments(attributes...)
if err := cmd.Run(&RunOpts{ // Version 2.43.1 has a bug where the behavior of `GIT_FLUSH` is flipped.
Env: env, // Ref: https://lore.kernel.org/git/CABn0oJvg3M_kBW-u=j3QhKnO=6QOzk-YFTgonYw_UvFS1NTX4g@mail.gmail.com
Dir: repo.Path, if InvertedGitFlushEnv {
Stdout: stdOut, env = append(env, "GIT_FLUSH=0")
Stderr: stdErr, } else {
}); err != nil { env = append(env, "GIT_FLUSH=1")
}
return cmd, &RunOpts{
Env: env,
Dir: repo.Path,
}, cancel, nil
}
// GitAttributes returns gitattribute.
//
// If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare).
func (repo *Repository) GitAttributes(treeish, filename string, attributes ...string) (map[string]GitAttribute, error) {
cmd, runOpts, cancel, err := repo.gitCheckAttrCommand(treeish, attributes...)
if err != nil {
return nil, err
}
defer cancel()
stdOut := new(bytes.Buffer)
runOpts.Stdout = stdOut
stdErr := new(bytes.Buffer)
runOpts.Stderr = stdErr
cmd.AddDashesAndList(filename)
if err := cmd.Run(runOpts); err != nil {
return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String())
} }
@ -76,155 +158,14 @@ func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[
return nil, fmt.Errorf("wrong number of fields in return from check-attr") return nil, fmt.Errorf("wrong number of fields in return from check-attr")
} }
name2attribute2info := make(map[string]map[string]string) values := make(map[string]GitAttribute, len(attributes))
for ; len(fields) >= 3; fields = fields[3:] {
for i := 0; i < (len(fields) / 3); i++ { // filename := string(fields[0])
filename := string(fields[3*i]) attribute := string(fields[1])
attribute := string(fields[3*i+1]) value := string(fields[2])
info := string(fields[3*i+2]) values[attribute] = GitAttribute(value)
attribute2info := name2attribute2info[filename]
if attribute2info == nil {
attribute2info = make(map[string]string)
}
attribute2info[attribute] = info
name2attribute2info[filename] = attribute2info
} }
return values, nil
return name2attribute2info, nil
}
// CheckAttributeReader provides a reader for check-attribute content that can be long running
type CheckAttributeReader struct {
// params
Attributes []string
Repo *Repository
IndexFile string
WorkTree string
stdinReader io.ReadCloser
stdinWriter *os.File
stdOut attributeWriter
cmd *Command
env []string
ctx context.Context
cancel context.CancelFunc
}
// Init initializes the CheckAttributeReader
func (c *CheckAttributeReader) Init(ctx context.Context) error {
if len(c.Attributes) == 0 {
lw := new(nulSeparatedAttributeWriter)
lw.attributes = make(chan attributeTriple)
lw.closed = make(chan struct{})
c.stdOut = lw
c.stdOut.Close()
return fmt.Errorf("no provided Attributes to check")
}
c.ctx, c.cancel = context.WithCancel(ctx)
c.cmd = NewCommand(c.ctx, "check-attr", "--stdin", "-z")
if len(c.IndexFile) > 0 {
c.cmd.AddArguments("--cached")
c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile)
}
if len(c.WorkTree) > 0 {
c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree)
}
// Version 2.43.1 has a bug where the behavior of `GIT_FLUSH` is flipped.
// Ref: https://lore.kernel.org/git/CABn0oJvg3M_kBW-u=j3QhKnO=6QOzk-YFTgonYw_UvFS1NTX4g@mail.gmail.com
if InvertedGitFlushEnv {
c.env = append(c.env, "GIT_FLUSH=0")
} else {
c.env = append(c.env, "GIT_FLUSH=1")
}
c.cmd.AddDynamicArguments(c.Attributes...)
var err error
c.stdinReader, c.stdinWriter, err = os.Pipe()
if err != nil {
c.cancel()
return err
}
lw := new(nulSeparatedAttributeWriter)
lw.attributes = make(chan attributeTriple, 5)
lw.closed = make(chan struct{})
c.stdOut = lw
return nil
}
// Run run cmd
func (c *CheckAttributeReader) Run() error {
defer func() {
_ = c.stdinReader.Close()
_ = c.stdOut.Close()
}()
stdErr := new(bytes.Buffer)
err := c.cmd.Run(&RunOpts{
Env: c.env,
Dir: c.Repo.Path,
Stdin: c.stdinReader,
Stdout: c.stdOut,
Stderr: stdErr,
})
if err != nil && // If there is an error we need to return but:
c.ctx.Err() != err && // 1. Ignore the context error if the context is cancelled or exceeds the deadline (RunWithContext could return c.ctx.Err() which is Canceled or DeadlineExceeded)
err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed
return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String())
}
return nil
}
// CheckPath check attr for given path
func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) {
defer func() {
if err != nil && err != c.ctx.Err() {
log.Error("Unexpected error when checking path %s in %s. Error: %v", path, c.Repo.Path, err)
}
}()
select {
case <-c.ctx.Done():
return nil, c.ctx.Err()
default:
}
if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil {
defer c.Close()
return nil, err
}
rs = make(map[string]string)
for range c.Attributes {
select {
case attr, ok := <-c.stdOut.ReadAttribute():
if !ok {
return nil, c.ctx.Err()
}
rs[attr.Attribute] = attr.Value
case <-c.ctx.Done():
return nil, c.ctx.Err()
}
}
return rs, nil
}
// Close close pip after use
func (c *CheckAttributeReader) Close() error {
c.cancel()
err := c.stdinWriter.Close()
return err
}
type attributeWriter interface {
io.WriteCloser
ReadAttribute() <-chan attributeTriple
} }
type attributeTriple struct { type attributeTriple struct {
@ -275,10 +216,6 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
return len(p), nil return len(p), nil
} }
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
return wr.attributes
}
func (wr *nulSeparatedAttributeWriter) Close() error { func (wr *nulSeparatedAttributeWriter) Close() error {
select { select {
case <-wr.closed: case <-wr.closed:
@ -290,49 +227,87 @@ func (wr *nulSeparatedAttributeWriter) Close() error {
return nil return nil
} }
// Create a check attribute reader for the current repository and provided commit ID // GitAttributeChecker creates an AttributeChecker for the given repository and provided commit ID.
func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) { //
indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID) // If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare).
func (repo *Repository) GitAttributeChecker(treeish string, attributes ...string) (AttributeChecker, error) {
cmd, runOpts, cancel, err := repo.gitCheckAttrCommand(treeish, attributes...)
if err != nil { if err != nil {
return nil, func() {} return AttributeChecker{}, err
} }
checker := &CheckAttributeReader{ ac := AttributeChecker{
Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"}, attributeNumber: len(attributes),
Repo: repo, ctx: cmd.parentContext,
IndexFile: indexFilename, cancel: cancel, // will be cancelled on Close
WorkTree: worktree,
}
ctx, cancel := context.WithCancel(repo.Ctx)
if err := checker.Init(ctx); err != nil {
log.Error("Unable to open checker for %s. Error: %v", commitID, err)
} else {
go func() {
err := checker.Run()
if err != nil && err != ctx.Err() {
log.Error("Unable to open checker for %s. Error: %v", commitID, err)
}
cancel()
}()
}
deferable := func() {
_ = checker.Close()
cancel()
deleteTemporaryFile()
} }
return checker, deferable stdinReader, stdinWriter, err := os.Pipe()
if err != nil {
ac.cancel()
return AttributeChecker{}, err
}
ac.stdinWriter = stdinWriter // will be closed on Close
lw := new(nulSeparatedAttributeWriter)
lw.attributes = make(chan attributeTriple, len(attributes))
lw.closed = make(chan struct{})
ac.attributesCh = lw.attributes
cmd.AddArguments("--stdin")
go func() {
defer stdinReader.Close()
defer lw.Close()
stdErr := new(bytes.Buffer)
runOpts.Stdin = stdinReader
runOpts.Stdout = lw
runOpts.Stderr = stdErr
err := cmd.Run(runOpts)
if err != nil && // If there is an error we need to return but:
cmd.parentContext.Err() != err && // 1. Ignore the context error if the context is cancelled or exceeds the deadline (RunWithContext could return c.ctx.Err() which is Canceled or DeadlineExceeded)
err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed
log.Error("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String())
}
}()
return ac, nil
} }
// true if "set"/"true", false if "unset"/"false", none otherwise type AttributeChecker struct {
func attributeToBool(attr map[string]string, name string) optional.Option[bool] { ctx context.Context
if value, has := attr[name]; has && value != "unspecified" { cancel context.CancelFunc
switch value { stdinWriter *os.File
case "set", "true": attributeNumber int
return optional.Some(true) attributesCh <-chan attributeTriple
case "unset", "false": }
return optional.Some(false)
func (ac AttributeChecker) CheckPath(path string) (map[string]GitAttribute, error) {
if err := ac.ctx.Err(); err != nil {
return nil, err
}
if _, err := ac.stdinWriter.Write([]byte(path + "\x00")); err != nil {
return nil, err
}
rs := make(map[string]GitAttribute)
for i := 0; i < ac.attributeNumber; i++ {
select {
case attr, ok := <-ac.attributesCh:
if !ok {
return nil, ac.ctx.Err()
}
rs[attr.Attribute] = GitAttribute(attr.Value)
case <-ac.ctx.Done():
return nil, ac.ctx.Err()
} }
} }
return optional.None[bool]() return rs, nil
}
func (ac AttributeChecker) Close() error {
ac.cancel()
return ac.stdinWriter.Close()
} }

View file

@ -4,10 +4,14 @@
package git package git
import ( import (
"path/filepath"
"testing" "testing"
"time" "time"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
@ -22,7 +26,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
assert.Len(t, testStr, n) assert.Len(t, testStr, n)
assert.NoError(t, err) assert.NoError(t, err)
select { select {
case attr := <-wr.ReadAttribute(): case attr := <-wr.attributes:
assert.Equal(t, ".gitignore\"\n", attr.Filename) assert.Equal(t, ".gitignore\"\n", attr.Filename)
assert.Equal(t, "linguist-vendored", attr.Attribute) assert.Equal(t, "linguist-vendored", attr.Attribute)
assert.Equal(t, "unspecified", attr.Value) assert.Equal(t, "unspecified", attr.Value)
@ -36,7 +40,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
select { select {
case attr := <-wr.ReadAttribute(): case attr := <-wr.attributes:
assert.Equal(t, ".gitignore\"\n", attr.Filename) assert.Equal(t, ".gitignore\"\n", attr.Filename)
assert.Equal(t, "linguist-vendored", attr.Attribute) assert.Equal(t, "linguist-vendored", attr.Attribute)
assert.Equal(t, "unspecified", attr.Value) assert.Equal(t, "unspecified", attr.Value)
@ -51,14 +55,14 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
select { select {
case <-wr.ReadAttribute(): case <-wr.attributes:
assert.FailNow(t, "There should not be an attribute ready to read") assert.FailNow(t, "There should not be an attribute ready to read")
case <-time.After(100 * time.Millisecond): case <-time.After(100 * time.Millisecond):
} }
_, err = wr.Write([]byte("attribute\x00")) _, err = wr.Write([]byte("attribute\x00"))
assert.NoError(t, err) assert.NoError(t, err)
select { select {
case <-wr.ReadAttribute(): case <-wr.attributes:
assert.FailNow(t, "There should not be an attribute ready to read") assert.FailNow(t, "There should not be an attribute ready to read")
case <-time.After(100 * time.Millisecond): case <-time.After(100 * time.Millisecond):
} }
@ -66,28 +70,28 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
_, err = wr.Write([]byte("value\x00")) _, err = wr.Write([]byte("value\x00"))
assert.NoError(t, err) assert.NoError(t, err)
attr := <-wr.ReadAttribute() attr := <-wr.attributes
assert.Equal(t, "incomplete-filename", attr.Filename) assert.Equal(t, "incomplete-filename", attr.Filename)
assert.Equal(t, "attribute", attr.Attribute) assert.Equal(t, "attribute", attr.Attribute)
assert.Equal(t, "value", attr.Value) assert.Equal(t, "value", attr.Value)
_, err = wr.Write([]byte("shouldbe.vendor\x00linguist-vendored\x00set\x00shouldbe.vendor\x00linguist-generated\x00unspecified\x00shouldbe.vendor\x00linguist-language\x00unspecified\x00")) _, err = wr.Write([]byte("shouldbe.vendor\x00linguist-vendored\x00set\x00shouldbe.vendor\x00linguist-generated\x00unspecified\x00shouldbe.vendor\x00linguist-language\x00unspecified\x00"))
assert.NoError(t, err) assert.NoError(t, err)
attr = <-wr.ReadAttribute() attr = <-wr.attributes
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, attributeTriple{ assert.EqualValues(t, attributeTriple{
Filename: "shouldbe.vendor", Filename: "shouldbe.vendor",
Attribute: "linguist-vendored", Attribute: "linguist-vendored",
Value: "set", Value: "set",
}, attr) }, attr)
attr = <-wr.ReadAttribute() attr = <-wr.attributes
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, attributeTriple{ assert.EqualValues(t, attributeTriple{
Filename: "shouldbe.vendor", Filename: "shouldbe.vendor",
Attribute: "linguist-generated", Attribute: "linguist-generated",
Value: "unspecified", Value: "unspecified",
}, attr) }, attr)
attr = <-wr.ReadAttribute() attr = <-wr.attributes
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, attributeTriple{ assert.EqualValues(t, attributeTriple{
Filename: "shouldbe.vendor", Filename: "shouldbe.vendor",
@ -95,3 +99,112 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
Value: "unspecified", Value: "unspecified",
}, attr) }, attr)
} }
func TestGitAttributeBareNonBare(t *testing.T) {
if !SupportCheckAttrOnBare {
t.Skip("git check-attr supported on bare repo starting with git 2.40")
}
repoPath := filepath.Join(testReposDir, "language_stats_repo")
gitRepo, err := openRepositoryWithDefaultContext(repoPath)
require.NoError(t, err)
defer gitRepo.Close()
for _, commitID := range []string{
"8fee858da5796dfb37704761701bb8e800ad9ef3",
"341fca5b5ea3de596dc483e54c2db28633cd2f97",
} {
t.Run("GitAttributeChecker/"+commitID, func(t *testing.T) {
bareChecker, err := gitRepo.GitAttributeChecker(commitID, LinguistAttributes...)
assert.NoError(t, err)
t.Cleanup(func() { bareChecker.Close() })
bareStats, err := bareChecker.CheckPath("i-am-a-python.p")
assert.NoError(t, err)
defer test.MockVariableValue(&SupportCheckAttrOnBare, false)()
cloneChecker, err := gitRepo.GitAttributeChecker(commitID, LinguistAttributes...)
assert.NoError(t, err)
t.Cleanup(func() { cloneChecker.Close() })
cloneStats, err := cloneChecker.CheckPath("i-am-a-python.p")
assert.NoError(t, err)
assert.EqualValues(t, cloneStats, bareStats)
})
t.Run("GitAttributes/"+commitID, func(t *testing.T) {
bareStats, err := gitRepo.GitAttributes(commitID, "i-am-a-python.p", LinguistAttributes...)
assert.NoError(t, err)
defer test.MockVariableValue(&SupportCheckAttrOnBare, false)()
cloneStats, err := gitRepo.GitAttributes(commitID, "i-am-a-python.p", LinguistAttributes...)
assert.NoError(t, err)
assert.EqualValues(t, cloneStats, bareStats)
})
}
}
func TestGitAttributes(t *testing.T) {
repoPath := filepath.Join(testReposDir, "language_stats_repo")
gitRepo, err := openRepositoryWithDefaultContext(repoPath)
require.NoError(t, err)
defer gitRepo.Close()
attr, err := gitRepo.GitAttributes("8fee858da5796dfb37704761701bb8e800ad9ef3", "i-am-a-python.p", LinguistAttributes...)
assert.NoError(t, err)
assert.EqualValues(t, map[string]GitAttribute{
"gitlab-language": "unspecified",
"linguist-detectable": "unspecified",
"linguist-documentation": "unspecified",
"linguist-generated": "unspecified",
"linguist-language": "Python",
"linguist-vendored": "unspecified",
}, attr)
attr, err = gitRepo.GitAttributes("341fca5b5ea3de596dc483e54c2db28633cd2f97", "i-am-a-python.p", LinguistAttributes...)
assert.NoError(t, err)
assert.EqualValues(t, map[string]GitAttribute{
"gitlab-language": "unspecified",
"linguist-detectable": "unspecified",
"linguist-documentation": "unspecified",
"linguist-generated": "unspecified",
"linguist-language": "Cobra",
"linguist-vendored": "unspecified",
}, attr)
}
func TestGitAttributeFirst(t *testing.T) {
repoPath := filepath.Join(testReposDir, "language_stats_repo")
gitRepo, err := openRepositoryWithDefaultContext(repoPath)
require.NoError(t, err)
defer gitRepo.Close()
t.Run("first is specified", func(t *testing.T) {
language, err := gitRepo.GitAttributeFirst("8fee858da5796dfb37704761701bb8e800ad9ef3", "i-am-a-python.p", "linguist-language", "gitlab-language")
assert.NoError(t, err)
assert.Equal(t, "Python", language.String())
})
t.Run("second is specified", func(t *testing.T) {
language, err := gitRepo.GitAttributeFirst("8fee858da5796dfb37704761701bb8e800ad9ef3", "i-am-a-python.p", "gitlab-language", "linguist-language")
assert.NoError(t, err)
assert.Equal(t, "Python", language.String())
})
t.Run("none is specified", func(t *testing.T) {
language, err := gitRepo.GitAttributeFirst("8fee858da5796dfb37704761701bb8e800ad9ef3", "i-am-a-python.p", "linguist-detectable", "gitlab-language", "non-existing")
assert.NoError(t, err)
assert.Equal(t, "", language.String())
})
}
func TestGitAttributeStruct(t *testing.T) {
assert.Equal(t, "", GitAttribute("").String())
assert.Equal(t, "", GitAttribute("unspecified").String())
assert.Equal(t, "python", GitAttribute("python").String())
assert.Equal(t, "text?token=Error", GitAttribute("text?token=Error").String())
assert.Equal(t, "text", GitAttribute("text?token=Error").Prefix())
}

View file

@ -8,8 +8,8 @@ package git
import ( import (
"bytes" "bytes"
"cmp"
"io" "io"
"strings"
"code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/analyze"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -61,8 +61,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
return nil, err return nil, err
} }
checker, deferable := repo.CheckAttributeReader(commitID) checker, err := repo.GitAttributeChecker(commitID, LinguistAttributes...)
defer deferable() if err != nil {
return nil, err
}
defer checker.Close()
contentBuf := bytes.Buffer{} contentBuf := bytes.Buffer{}
var content []byte var content []byte
@ -102,41 +105,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
isDocumentation := optional.None[bool]() isDocumentation := optional.None[bool]()
isDetectable := optional.None[bool]() isDetectable := optional.None[bool]()
if checker != nil { attrs, err := checker.CheckPath(f.Name())
attrs, err := checker.CheckPath(f.Name()) if err == nil {
if err == nil { isVendored = attrs["linguist-vendored"].Bool()
isVendored = attributeToBool(attrs, "linguist-vendored") isGenerated = attrs["linguist-generated"].Bool()
isGenerated = attributeToBool(attrs, "linguist-generated") isDocumentation = attrs["linguist-documentation"].Bool()
isDocumentation = attributeToBool(attrs, "linguist-documentation") isDetectable = attrs["linguist-detectable"].Bool()
isDetectable = attributeToBool(attrs, "linguist-detectable") if language := cmp.Or(
if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { attrs["linguist-language"].String(),
// group languages, such as Pug -> HTML; SCSS -> CSS attrs["gitlab-language"].Prefix(),
group := enry.GetLanguageGroup(language) ); language != "" {
if len(group) != 0 { // group languages, such as Pug -> HTML; SCSS -> CSS
language = group group := enry.GetLanguageGroup(language)
} if len(group) != 0 {
language = group
// this language will always be added to the size
sizes[language] += f.Size()
continue
} else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
// strip off a ? if present
if idx := strings.IndexByte(language, '?'); idx >= 0 {
language = language[:idx]
}
if len(language) != 0 {
// group languages, such as Pug -> HTML; SCSS -> CSS
group := enry.GetLanguageGroup(language)
if len(group) != 0 {
language = group
}
// this language will always be added to the size
sizes[language] += f.Size()
continue
}
} }
// this language will always be added to the size
sizes[language] += f.Size()
continue
} }
} }

View file

@ -124,6 +124,10 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
} }
} }
} }
if err = scanner.Err(); err != nil {
_ = stdoutReader.Close()
return fmt.Errorf("GetCodeActivityStats scan: %w", err)
}
a := make([]*CodeActivityAuthor, 0, len(authors)) a := make([]*CodeActivityAuthor, 0, len(authors))
for _, v := range authors { for _, v := range authors {
a = append(a, v) a = append(a, v)

View file

@ -185,17 +185,22 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) {
tag.Tagger = parseSignatureFromCommitLine(ref["creator"]) tag.Tagger = parseSignatureFromCommitLine(ref["creator"])
tag.Message = ref["contents"] tag.Message = ref["contents"]
// strip PGP signature if present in contents field // strip the signature if present in contents field
pgpStart := strings.Index(tag.Message, beginpgp) pgpStart := strings.Index(tag.Message, beginpgp)
if pgpStart >= 0 { if pgpStart >= 0 {
tag.Message = tag.Message[0:pgpStart] tag.Message = tag.Message[0:pgpStart]
} else {
sshStart := strings.Index(tag.Message, beginssh)
if sshStart >= 0 {
tag.Message = tag.Message[0:sshStart]
}
} }
// annotated tag with GPG signature // annotated tag with signature
if tag.Type == "tag" && ref["contents:signature"] != "" { if tag.Type == "tag" && ref["contents:signature"] != "" {
payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n", payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n",
tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message)) tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message))
tag.Signature = &CommitGPGSignature{ tag.Signature = &ObjectSignature{
Signature: ref["contents:signature"], Signature: ref["contents:signature"],
Payload: payload, Payload: payload,
} }

View file

@ -315,7 +315,7 @@ qbHDASXl
Type: "tag", Type: "tag",
Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"), Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"),
Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md", Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md",
Signature: &CommitGPGSignature{ Signature: &ObjectSignature{
Signature: `-----BEGIN PGP SIGNATURE----- Signature: `-----BEGIN PGP SIGNATURE-----
aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3 aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3

View file

@ -14,6 +14,8 @@ import (
const ( const (
beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n" beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n"
endpgp = "\n-----END PGP SIGNATURE-----" endpgp = "\n-----END PGP SIGNATURE-----"
beginssh = "\n-----BEGIN SSH SIGNATURE-----\n"
endssh = "\n-----END SSH SIGNATURE-----"
) )
// Tag represents a Git tag. // Tag represents a Git tag.
@ -24,7 +26,7 @@ type Tag struct {
Type string Type string
Tagger *Signature Tagger *Signature
Message string Message string
Signature *CommitGPGSignature Signature *ObjectSignature
} }
// Commit return the commit of the tag reference // Commit return the commit of the tag reference
@ -71,17 +73,36 @@ l:
break l break l
} }
} }
idx := strings.LastIndex(tag.Message, beginpgp)
if idx > 0 { extractTagSignature := func(signatureBeginMark, signatureEndMark string) (bool, *ObjectSignature, string) {
endSigIdx := strings.Index(tag.Message[idx:], endpgp) idx := strings.LastIndex(tag.Message, signatureBeginMark)
if endSigIdx > 0 { if idx == -1 {
tag.Signature = &CommitGPGSignature{ return false, nil, ""
Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)],
Payload: string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]),
}
tag.Message = tag.Message[:idx+1]
} }
endSigIdx := strings.Index(tag.Message[idx:], signatureEndMark)
if endSigIdx == -1 {
return false, nil, ""
}
return true, &ObjectSignature{
Signature: tag.Message[idx+1 : idx+endSigIdx+len(signatureEndMark)],
Payload: string(data[:bytes.LastIndex(data, []byte(signatureBeginMark))+1]),
}, tag.Message[:idx+1]
} }
// Try to find an OpenPGP signature
found, sig, message := extractTagSignature(beginpgp, endpgp)
if !found {
// If not found, try an SSH one
found, sig, message = extractTagSignature(beginssh, endssh)
}
// If either is found, update the tag Signature and Message
if found {
tag.Signature = sig
tag.Message = message
}
return tag, nil return tag, nil
} }

View file

@ -46,6 +46,41 @@ ono`), tag: Tag{
Message: "test message\no\n\nono", Message: "test message\no\n\nono",
Signature: nil, Signature: nil,
}}, }},
{data: []byte(`object d8d1fdb5b20eaca882e34ee510eb55941a242b24
type commit
tag v0
tagger Jane Doe <jane.doe@example.com> 1709146405 +0100
v0
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvD4pK7baygXxoWoVoKjVEc/xZh
6w+1FUn5hypFqJXNAAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
AAAAQKFeTnxi9ssRqSg+sJcmjAgpgoPq1k5SXm306+mJmkPwvhim8f9Gz6uy1AddPmXaD7
5LVB3fV2GmmFDKGB+wCAo=
-----END SSH SIGNATURE-----
`), tag: Tag{
Name: "",
ID: Sha1ObjectFormat.EmptyObjectID(),
Object: &Sha1Hash{0xd8, 0xd1, 0xfd, 0xb5, 0xb2, 0x0e, 0xac, 0xa8, 0x82, 0xe3, 0x4e, 0xe5, 0x10, 0xeb, 0x55, 0x94, 0x1a, 0x24, 0x2b, 0x24},
Type: "commit",
Tagger: &Signature{Name: "Jane Doe", Email: "jane.doe@example.com", When: time.Unix(1709146405, 0)},
Message: "v0\n",
Signature: &ObjectSignature{
Signature: `-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvD4pK7baygXxoWoVoKjVEc/xZh
6w+1FUn5hypFqJXNAAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
AAAAQKFeTnxi9ssRqSg+sJcmjAgpgoPq1k5SXm306+mJmkPwvhim8f9Gz6uy1AddPmXaD7
5LVB3fV2GmmFDKGB+wCAo=
-----END SSH SIGNATURE-----`,
Payload: `object d8d1fdb5b20eaca882e34ee510eb55941a242b24
type commit
tag v0
tagger Jane Doe <jane.doe@example.com> 1709146405 +0100
v0
`,
},
}},
} }
for _, test := range testData { for _, test := range testData {

View file

@ -1,5 +1,5 @@
[core] [core]
repositoryformatversion = 0 repositoryformatversion = 0
filemode = true filemode = true
bare = false bare = true
logallrefupdates = true logallrefupdates = true

View file

@ -1 +1,2 @@
0000000000000000000000000000000000000000 8fee858da5796dfb37704761701bb8e800ad9ef3 Andrew Thornton <art27@cantab.net> 1632140318 +0100 commit (initial): Add some test files for GetLanguageStats 0000000000000000000000000000000000000000 8fee858da5796dfb37704761701bb8e800ad9ef3 Andrew Thornton <art27@cantab.net> 1632140318 +0100 commit (initial): Add some test files for GetLanguageStats
8fee858da5796dfb37704761701bb8e800ad9ef3 341fca5b5ea3de596dc483e54c2db28633cd2f97 oliverpool <git@olivier.pfad.fr> 1711278775 +0100 push

View file

@ -1 +1,2 @@
0000000000000000000000000000000000000000 8fee858da5796dfb37704761701bb8e800ad9ef3 Andrew Thornton <art27@cantab.net> 1632140318 +0100 commit (initial): Add some test files for GetLanguageStats 0000000000000000000000000000000000000000 8fee858da5796dfb37704761701bb8e800ad9ef3 Andrew Thornton <art27@cantab.net> 1632140318 +0100 commit (initial): Add some test files for GetLanguageStats
8fee858da5796dfb37704761701bb8e800ad9ef3 341fca5b5ea3de596dc483e54c2db28633cd2f97 oliverpool <git@olivier.pfad.fr> 1711278775 +0100 push

Some files were not shown because too many files have changed in this diff Show more