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
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:
commit
f56408b8a9
549 changed files with 9074 additions and 10126 deletions
11
.air.toml
11
.air.toml
|
@ -8,6 +8,15 @@ delay = 1000
|
|||
include_ext = ["go", "tmpl"]
|
||||
include_file = ["main.go"]
|
||||
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$"]
|
||||
stop_on_error = true
|
||||
|
|
|
@ -271,13 +271,19 @@ package "code.gitea.io/gitea/modules/sync"
|
|||
|
||||
package "code.gitea.io/gitea/modules/testlogger"
|
||||
func (*testLoggerWriterCloser).pushT
|
||||
func (*testLoggerWriterCloser).Write
|
||||
func (*testLoggerWriterCloser).Log
|
||||
func (*testLoggerWriterCloser).recordError
|
||||
func (*testLoggerWriterCloser).printMsg
|
||||
func (*testLoggerWriterCloser).popT
|
||||
func (*testLoggerWriterCloser).Close
|
||||
func (*testLoggerWriterCloser).Reset
|
||||
func PrintCurrentTest
|
||||
func Printf
|
||||
func NewTestLoggerWriter
|
||||
func (*TestLogEventWriter).Base
|
||||
func (*TestLogEventWriter).GetLevel
|
||||
func (*TestLogEventWriter).GetWriterName
|
||||
func (*TestLogEventWriter).GetWriterType
|
||||
func (*TestLogEventWriter).Run
|
||||
|
||||
package "code.gitea.io/gitea/modules/timeutil"
|
||||
func GetExecutableModTime
|
||||
|
@ -323,7 +329,6 @@ package "code.gitea.io/gitea/services/pull"
|
|||
|
||||
package "code.gitea.io/gitea/services/repository"
|
||||
func IsErrForkAlreadyExist
|
||||
func UpdateRepositoryUnits
|
||||
|
||||
package "code.gitea.io/gitea/services/repository/archiver"
|
||||
func ArchiveRepository
|
||||
|
@ -336,4 +341,5 @@ package "code.gitea.io/gitea/services/repository/files"
|
|||
|
||||
package "code.gitea.io/gitea/services/webhook"
|
||||
func NewNotifier
|
||||
func List
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"features": {
|
||||
// installs nodejs into container
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version":"20"
|
||||
"version": "20"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git-lfs:1.1.0": {},
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
|
||||
|
@ -24,7 +24,7 @@
|
|||
"DavidAnson.vscode-markdownlint",
|
||||
"Vue.volar",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"zixuanchen.vitest-explorer",
|
||||
"vitest.explorer",
|
||||
"qwtel.sqlite-viewer",
|
||||
"GitHub.vscode-pull-request-github"
|
||||
]
|
||||
|
|
|
@ -62,7 +62,6 @@ cpu.out
|
|||
/data
|
||||
/indexers
|
||||
/log
|
||||
/public/img/avatar
|
||||
/tests/integration/gitea-integration-*
|
||||
/tests/integration/indexers-*
|
||||
/tests/e2e/gitea-e2e-*
|
||||
|
@ -77,6 +76,7 @@ cpu.out
|
|||
/public/assets/js
|
||||
/public/assets/css
|
||||
/public/assets/fonts
|
||||
/public/assets/img/avatar
|
||||
/public/assets/img/webpack
|
||||
/vendor
|
||||
/web_src/fomantic/node_modules
|
||||
|
|
|
@ -42,10 +42,6 @@ overrides:
|
|||
worker: true
|
||||
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]
|
||||
- files: ["build/generate-images.js"]
|
||||
rules:
|
||||
i/no-unresolved: [0]
|
||||
i/no-extraneous-dependencies: [0]
|
||||
- files: ["*.config.*"]
|
||||
rules:
|
||||
i/no-unused-modules: [0]
|
||||
|
@ -123,7 +119,7 @@ rules:
|
|||
"@stylistic/js/arrow-spacing": [2, {before: true, after: true}]
|
||||
"@stylistic/js/block-spacing": [0]
|
||||
"@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-style": [2, last]
|
||||
"@stylistic/js/computed-property-spacing": [2, never]
|
||||
|
@ -290,7 +286,7 @@ rules:
|
|||
jquery/no-class: [0]
|
||||
jquery/no-clone: [2]
|
||||
jquery/no-closest: [0]
|
||||
jquery/no-css: [0]
|
||||
jquery/no-css: [2]
|
||||
jquery/no-data: [0]
|
||||
jquery/no-deferred: [2]
|
||||
jquery/no-delegate: [2]
|
||||
|
@ -413,7 +409,7 @@ rules:
|
|||
no-jquery/no-constructor-attributes: [2]
|
||||
no-jquery/no-contains: [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-deferred: [2]
|
||||
no-jquery/no-delegate: [2]
|
||||
|
|
|
@ -34,10 +34,14 @@ jobs:
|
|||
!startsWith(vars.ROLE, 'forgejo-') && (
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport/')
|
||||
(
|
||||
github.event.action == 'closed' &&
|
||||
contains(toJSON(github.event.pull_request.labels), 'backport/v')
|
||||
)
|
||||
||
|
||||
(
|
||||
github.event.action == 'labeled' &&
|
||||
contains(github.event.label.name, 'backport/v')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -54,7 +58,7 @@ jobs:
|
|||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
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
|
||||
env:
|
||||
LABELS: ${{ toJSON(github.event.pull_request.labels) }}
|
||||
|
|
|
@ -9,6 +9,8 @@ on:
|
|||
- docker/**
|
||||
- .forgejo/workflows/build-release.yml
|
||||
- .forgejo/workflows/build-release-integration.yml
|
||||
branches-ignore:
|
||||
- renovate/**
|
||||
pull_request:
|
||||
paths:
|
||||
- Makefile
|
||||
|
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.22"
|
||||
check-latest: true
|
||||
|
||||
- name: version from ref
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
go-version: "~1.21"
|
||||
go-version: "1.22"
|
||||
check-latest: true
|
||||
- run: |
|
||||
apt-get -qq update
|
||||
|
|
|
@ -64,7 +64,7 @@ jobs:
|
|||
if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != ''
|
||||
uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.22"
|
||||
check-latest: true
|
||||
- name: update the _release.experimental DNS record
|
||||
if: vars.ROLE == 'forgejo-experimental' && secrets.OVH_APP_KEY != ''
|
||||
|
|
53
.forgejo/workflows/renovate.yml
Normal file
53
.forgejo/workflows/renovate.yml
Normal 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 }}
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.22"
|
||||
check-latest: true
|
||||
- run: make deps-backend deps-tools
|
||||
- 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/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.22"
|
||||
- run: |
|
||||
git config --add safe.directory '*'
|
||||
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/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.22"
|
||||
- name: install dependencies & git >= 2.42
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
@ -144,7 +144,7 @@ jobs:
|
|||
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.22"
|
||||
- name: install dependencies & git >= 2.42
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
@ -181,7 +181,7 @@ jobs:
|
|||
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.22"
|
||||
- name: install dependencies & git >= 2.42
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: 🦋 Bug Report (web interface / frontend)
|
||||
description: Something doesn't look quite as it should? Report it here!
|
||||
title: "[BUG] "
|
||||
labels: ["bug", "forgejo/ui"]
|
||||
labels: ["bug/new-report", "forgejo/ui"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: 🐛 Bug Report (server / backend)
|
||||
description: Found something you weren't expecting? Report it here!
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
labels: bug/new-report
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
|
|
@ -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:
|
||||
https://codeberg.org/forgejo/forgejo/src/branch/forgejo/CONTRIBUTING.md
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -64,7 +64,7 @@ cpu.out
|
|||
/data
|
||||
/indexers
|
||||
/log
|
||||
/public/img/avatar
|
||||
/public/assets/img/avatar
|
||||
/tests/integration/gitea-integration-*
|
||||
/tests/integration/indexers-*
|
||||
/tests/e2e/gitea-e2e-*
|
||||
|
|
|
@ -42,7 +42,7 @@ vscode:
|
|||
- DavidAnson.vscode-markdownlint
|
||||
- Vue.volar
|
||||
- ms-azuretools.vscode-docker
|
||||
- zixuanchen.vitest-explorer
|
||||
- vitest.explorer
|
||||
- qwtel.sqlite-viewer
|
||||
- GitHub.vscode-pull-request-github
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ rules:
|
|||
"@stylistic/block-opening-brace-newline-after": null
|
||||
"@stylistic/block-opening-brace-newline-before": 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/declaration-bang-space-after": never
|
||||
"@stylistic/declaration-bang-space-before": null
|
||||
|
@ -140,7 +140,7 @@ rules:
|
|||
function-disallowed-list: null
|
||||
function-linear-gradient-no-nonstandard-direction: true
|
||||
function-name-case: lower
|
||||
function-no-unknown: null
|
||||
function-no-unknown: true
|
||||
function-url-no-scheme-relative: null
|
||||
function-url-quotes: always
|
||||
function-url-scheme-allowed-list: null
|
||||
|
@ -168,7 +168,7 @@ rules:
|
|||
no-duplicate-selectors: true
|
||||
no-empty-source: 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-unknown-animations: null
|
||||
no-unknown-custom-properties: null
|
||||
|
@ -181,6 +181,7 @@ rules:
|
|||
rule-empty-line-before: 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}]
|
||||
selector-anb-no-unmatchable: true
|
||||
selector-attribute-name-disallowed-list: null
|
||||
selector-attribute-operator-allowed-list: null
|
||||
selector-attribute-operator-disallowed-list: null
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
ENV GOPROXY ${GOPROXY:-direct}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
ENV GOPROXY ${GOPROXY:-direct}
|
||||
|
|
19
Makefile
19
Makefile
|
@ -44,9 +44,6 @@ DOCKER_TAG ?= latest
|
|||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
|
||||
ifeq ($(HAS_GO), yes)
|
||||
GOPATH ?= $(shell $(GO) env GOPATH)
|
||||
export PATH := $(GOPATH)/bin:$(PATH)
|
||||
|
||||
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
|
||||
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
|
||||
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
|
||||
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
|
||||
|
||||
GO_SOURCES := $(wildcard *.go)
|
||||
|
@ -396,19 +395,19 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
|
|||
|
||||
.PHONY: lint-js
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
lint-swagger: node_modules
|
||||
|
@ -468,7 +467,7 @@ lint-yaml: .venv
|
|||
|
||||
.PHONY: watch
|
||||
watch:
|
||||
@bash build/watch.sh
|
||||
@bash tools/watch.sh
|
||||
|
||||
.PHONY: watch-frontend
|
||||
watch-frontend: node-check node_modules
|
||||
|
@ -962,7 +961,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
|
|||
.PHONY: svg
|
||||
svg: node-check | node_modules
|
||||
rm -rf $(SVG_DEST_DIR)
|
||||
node build/generate-svg.js
|
||||
node tools/generate-svg.js
|
||||
|
||||
.PHONY: svg-check
|
||||
svg-check: svg
|
||||
|
@ -997,7 +996,7 @@ generate-gitignore:
|
|||
.PHONY: generate-images
|
||||
generate-images: | node_modules
|
||||
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
|
||||
generate-manpage:
|
||||
|
|
27
cmd/hook.go
27
cmd/hook.go
|
@ -293,9 +293,32 @@ Forgejo or set your environment appropriately.`, "")
|
|||
return nil
|
||||
}
|
||||
|
||||
// runHookUpdate process the update hook: https://git-scm.com/docs/githooks#update
|
||||
func runHookUpdate(c *cli.Context) error {
|
||||
// Update is empty and is kept only for backwards compatibility
|
||||
return nil
|
||||
// Now if we're an internal don't do anything else
|
||||
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 {
|
||||
|
|
|
@ -37,7 +37,7 @@ gitea embedded list [--include-vendored] [patterns...]
|
|||
|
||||
- 列出所有模板文件,无论在哪个虚拟目录下:`**.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
|
||||
$ gitea embedded list '**openid**'
|
||||
public/img/auth/openid_connect.svg
|
||||
public/img/openid-16x16.png
|
||||
public/assets/img/auth/openid_connect.svg
|
||||
public/assets/img/openid-16x16.png
|
||||
templates/user/auth/finalize_openid.tmpl
|
||||
templates/user/auth/signin_openid.tmpl
|
||||
templates/user/auth/signup_openid_connect.tmpl
|
||||
|
|
|
@ -17,6 +17,12 @@ menu:
|
|||
|
||||
# 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
|
||||
|
||||
Gitea can search through the files of the repositories by enabling this function in your [`app.ini`](administration/config-cheat-sheet.md):
|
||||
|
|
|
@ -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.
|
||||
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.
|
||||
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.
|
||||
|
||||
### Accessibility / ARIA
|
||||
|
|
|
@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
|
|||
9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
|
||||
10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
|
||||
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类。如果内联脚本和样式不可避免,请解释无法避免的原因。
|
||||
|
||||
### 可访问性 / ARIA
|
||||
|
|
|
@ -214,7 +214,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
|
|||
|
||||
### 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
|
||||
|
||||
|
|
|
@ -201,7 +201,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
|
|||
|
||||
### 构建和添加 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
|
||||
|
||||
|
|
|
@ -87,6 +87,9 @@ _Symbols used in table:_
|
|||
| Git Blame | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| 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
|
||||
|
||||
| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | RhodeCode EE |
|
||||
|
|
14
go.mod
14
go.mod
|
@ -1,6 +1,6 @@
|
|||
module code.gitea.io/gitea
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
code.gitea.io/actions-proto-go v0.4.0
|
||||
|
@ -10,8 +10,8 @@ require (
|
|||
connectrpc.com/connect v1.15.0
|
||||
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669
|
||||
gitea.com/go-chi/cache v0.2.0
|
||||
gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384
|
||||
gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
||||
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
|
||||
|
@ -29,18 +29,18 @@ require (
|
|||
github.com/djherbis/nio/v3 v3.0.1
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5
|
||||
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/emirpasic/gods v1.18.1
|
||||
github.com/felixge/fgprof v0.9.3
|
||||
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/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||
github.com/go-chi/chi/v5 v5.0.11
|
||||
github.com/go-chi/cors v1.2.1
|
||||
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-git/go-billy/v5 v5.5.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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
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/lib/pq v1.10.9
|
||||
github.com/markbates/goth v1.78.0
|
||||
|
|
38
go.sum
38
go.sum
|
@ -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/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/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/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-20230415143339-2c0754df4384/go.mod h1:hQ9SYHKdOX968wJglb/NMQ+UqpOKwW4L+EYdvkWjHSo=
|
||||
gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3 h1:4FuO+MahrkDjdjVIS8ExmY9FEHTZS8TPheEm4uU5xLI=
|
||||
gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3/go.mod h1:fc/pjt5EqNKgqQXYzcas1Z5L5whkZHyOvTA7OzWVJck=
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
|
||||
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 h1:IFDiMBObsP6CZIRaDLd54SR6zPYAffPXiXck5Xslu0Q=
|
||||
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/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
|
||||
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/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/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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
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/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/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/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
|
||||
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/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/editorconfig/editorconfig-core-go/v2 v2.6.0 h1:5O8paxMLmi/5ONoKXzWNYxoSZU7+ITVbGcPga0IrzfE=
|
||||
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 h1:iPCqofzMO41WVbcS/B5Ym7AwHQg9cyQ7Ie/R2XU5L3A=
|
||||
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/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=
|
||||
|
@ -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/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
|
||||
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.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||
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/go.mod h1:cJ9Ye0ZNSMN7RzZDBRY3E+8M3Bpf/R1JX22Ir9yX6WI=
|
||||
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-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-enry/go-enry/v2 v2.8.6 h1:T6ljs5+qNiUTDqpfK5GUD5EvLNdDbf804u8iC30vw7U=
|
||||
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 h1:vbab0pcf5Yo1cHQLzbWZ+QomUh3EfEU8EiR5n7W0lnQ=
|
||||
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/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
|
||||
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/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/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.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
|
||||
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.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.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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
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.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
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.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
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/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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.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.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
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/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.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.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||
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.3/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/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
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-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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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-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-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-20200803210538-64077c9b5642/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.5/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/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
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/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
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/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
|
|
|
@ -170,14 +170,16 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
|
|||
return err
|
||||
}
|
||||
|
||||
// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow.
|
||||
func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string) error {
|
||||
// Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'.
|
||||
// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
|
||||
// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
|
||||
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{
|
||||
RepoID: repoID,
|
||||
Ref: ref,
|
||||
WorkflowID: workflowID,
|
||||
Status: []Status{StatusRunning, StatusWaiting},
|
||||
RepoID: repoID,
|
||||
Ref: ref,
|
||||
WorkflowID: workflowID,
|
||||
TriggerEvent: event,
|
||||
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
@ -71,6 +72,7 @@ type FindRunOptions struct {
|
|||
WorkflowID string
|
||||
Ref string // the commit/tag/… that caused this workflow
|
||||
TriggerUserID int64
|
||||
TriggerEvent webhook_module.HookEventType
|
||||
Approved bool // not util.OptionalBool, it works only when it's true
|
||||
Status []Status
|
||||
}
|
||||
|
@ -98,6 +100,9 @@ func (opts FindRunOptions) ToConds() builder.Cond {
|
|||
if opts.Ref != "" {
|
||||
cond = cond.And(builder.Eq{"ref": opts.Ref})
|
||||
}
|
||||
if opts.TriggerEvent != "" {
|
||||
cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ package actions
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -118,3 +119,22 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -12,14 +12,11 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
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"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
@ -79,53 +76,6 @@ func init() {
|
|||
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
|
||||
func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) 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 {
|
||||
notification := &Notification{
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func SetIssueReadBy(ctx context.Context, issueID, userID int64) error {
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
|
480
models/activities/notification_list.go
Normal file
480
models/activities/notification_list.go
Normal 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
|
||||
}
|
|
@ -198,6 +198,8 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
@ -207,11 +209,12 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
|
|||
}
|
||||
_, err = t.WriteString(line + "\n")
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
if err = scanner.Err(); err != nil {
|
||||
return fmt.Errorf("RegeneratePublicKeys scan: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -120,6 +120,8 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
@ -129,11 +131,12 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
|
|||
}
|
||||
_, err = t.WriteString(line + "\n")
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
if err = scanner.Err(); err != nil {
|
||||
return fmt.Errorf("regeneratePrincipalKeys scan: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ func TestParseCommitWithSSHSignature(t *testing.T) {
|
|||
Committer: &git.Signature{
|
||||
Email: "non-existent",
|
||||
},
|
||||
Signature: &git.CommitGPGSignature{
|
||||
Signature: &git.ObjectSignature{
|
||||
Payload: `tree 2d491b2985a7ff848d5c02748e7ea9f9f7619f9f
|
||||
parent 45b03601635a1f463b81963a4022c7f87ce96ef9
|
||||
author user2 <non-existent> 1699710556 +0100
|
||||
|
@ -67,7 +67,7 @@ AAAAQIMufOuSjZeDUujrkVK4sl7ICa0WwEftas8UAYxx0Thdkiw2qWjR1U1PKfTLm16/w8
|
|||
Committer: &git.Signature{
|
||||
Email: "user2@example.com",
|
||||
},
|
||||
Signature: &git.CommitGPGSignature{
|
||||
Signature: &git.ObjectSignature{
|
||||
Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
|
||||
parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
|
||||
author user2 <user2@example.com> 1699707877 +0100
|
||||
|
@ -89,7 +89,7 @@ Add content
|
|||
Committer: &git.Signature{
|
||||
Email: "user2@example.com",
|
||||
},
|
||||
Signature: &git.CommitGPGSignature{
|
||||
Signature: &git.ObjectSignature{
|
||||
Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
|
||||
parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
|
||||
author user2 <user2@example.com> 1699707877 +0100
|
||||
|
@ -120,7 +120,7 @@ fs9cMpZVM9BfIKNUSO8QY=
|
|||
Committer: &git.Signature{
|
||||
Email: "user2@noreply.example.com",
|
||||
},
|
||||
Signature: &git.CommitGPGSignature{
|
||||
Signature: &git.ObjectSignature{
|
||||
Payload: `tree 4836c7f639f37388bab4050ef5c97bbbd54272fc
|
||||
parent 795be1b0117ea5c65456050bb9fd84744d4fd9c6
|
||||
author user2 <user2@noreply.example.com> 1699709594 +0100
|
||||
|
|
|
@ -24,7 +24,7 @@ import (
|
|||
|
||||
const (
|
||||
// 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 = 28
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
repo_id: 2 # private
|
||||
is_private: true
|
||||
created_unix: 1603228283
|
||||
content: '1|' # issueId 4
|
||||
|
||||
-
|
||||
id: 2
|
||||
|
|
|
@ -17,6 +17,195 @@
|
|||
updated: 1683636626
|
||||
need_approval: 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
|
||||
title: "update actions"
|
||||
|
@ -36,3 +225,191 @@
|
|||
updated: 1683636626
|
||||
need_approval: 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
|
||||
}
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
-
|
||||
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
|
||||
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
|
||||
repo_id: 1
|
||||
url: www.example.com/url2
|
||||
url: http://www.example.com/url2
|
||||
http_method: POST
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
|
||||
is_active: false
|
||||
|
@ -18,14 +21,16 @@
|
|||
id: 3
|
||||
owner_id: 3
|
||||
repo_id: 3
|
||||
url: www.example.com/url3
|
||||
url: http://www.example.com/url3
|
||||
http_method: POST
|
||||
content_type: 1 # json
|
||||
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
|
||||
repo_id: 2
|
||||
url: www.example.com/url4
|
||||
url: http://www.example.com/url4
|
||||
http_method: POST
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
||||
is_active: true
|
||||
is_active: false
|
||||
|
|
|
@ -39,17 +39,21 @@ func NewMigration(desc string, fn func(*xorm.Engine) error) *Migration {
|
|||
// Add new migrations to the bottom of the list.
|
||||
var migrations = []*Migration{
|
||||
// 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
|
||||
NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable),
|
||||
NewMigration("Create the `forgejo_sem_ver` table", forgejo_v1_20.CreateSemVerTable),
|
||||
// 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
|
||||
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
|
||||
NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable),
|
||||
NewMigration("Create the `forgejo_repo_flag` table", forgejo_v1_22.CreateRepoFlagTable),
|
||||
// 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.
|
||||
|
|
14
models/forgejo_migrations/v1_22/main_test.go
Normal file
14
models/forgejo_migrations/v1_22/main_test.go
Normal 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)
|
||||
}
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
func AddWikiBranchToRepository(x *xorm.Engine) error {
|
||||
type Repository struct {
|
||||
ID int64
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
WikiBranch string
|
||||
}
|
||||
|
||||
|
|
17
models/forgejo_migrations/v1_22/v7.go
Normal file
17
models/forgejo_migrations/v1_22/v7.go
Normal 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{})
|
||||
}
|
51
models/forgejo_migrations/v1_22/v8.go
Normal file
51
models/forgejo_migrations/v1_22/v8.go
Normal 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()
|
||||
}
|
34
models/forgejo_migrations/v1_22/v8_test.go
Normal file
34
models/forgejo_migrations/v1_22/v8_test.go
Normal 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)
|
||||
}
|
|
@ -292,7 +292,7 @@ func FindRenamedBranch(ctx context.Context, repoID int64, from string) (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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -367,7 +367,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
|||
}
|
||||
|
||||
// 5. do git action
|
||||
if err = gitAction(isDefault); err != nil {
|
||||
if err = gitAction(ctx, isDefault); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package git_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -132,7 +133,7 @@ func TestRenameBranch(t *testing.T) {
|
|||
}, git_model.WhitelistOptions{}))
|
||||
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
|
||||
return nil
|
||||
}))
|
||||
|
|
|
@ -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
|
||||
func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
|
||||
var lastStatus *CommitStatus
|
||||
state := api.CommitStatusSuccess
|
||||
for _, status := range statuses {
|
||||
if status.State.NoBetterThan(state) {
|
||||
state = status.State
|
||||
lastStatus = status
|
||||
if len(statuses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
latestWorstStatus := statuses[0]
|
||||
for _, status := range statuses[1:] {
|
||||
if status.State.NoBetterThan(latestWorstStatus.State) {
|
||||
latestWorstStatus = status
|
||||
}
|
||||
}
|
||||
if lastStatus == nil {
|
||||
if len(statuses) > 0 {
|
||||
lastStatus = statuses[0]
|
||||
} else {
|
||||
lastStatus = &CommitStatus{}
|
||||
}
|
||||
}
|
||||
return lastStatus
|
||||
return latestWorstStatus
|
||||
}
|
||||
|
||||
// CommitStatusOptions holds the options for query commit statuses
|
||||
|
|
|
@ -141,16 +141,20 @@ func Test_CalcCommitStatus(t *testing.T) {
|
|||
statuses: []*git_model.CommitStatus{
|
||||
{
|
||||
State: structs.CommitStatusSuccess,
|
||||
ID: 1,
|
||||
},
|
||||
{
|
||||
State: structs.CommitStatusSuccess,
|
||||
ID: 2,
|
||||
},
|
||||
{
|
||||
State: structs.CommitStatusSuccess,
|
||||
ID: 3,
|
||||
},
|
||||
},
|
||||
expected: &git_model.CommitStatus{
|
||||
State: structs.CommitStatusSuccess,
|
||||
ID: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -169,6 +173,10 @@ func Test_CalcCommitStatus(t *testing.T) {
|
|||
State: structs.CommitStatusError,
|
||||
},
|
||||
},
|
||||
{
|
||||
statuses: []*git_model.CommitStatus{},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, kase := range kases {
|
||||
|
|
|
@ -194,20 +194,6 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
|||
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
|
||||
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
||||
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.
|
||||
func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
|
||||
if index < 1 {
|
||||
return nil, ErrIssueNotExist{}
|
||||
return nil, ErrIssueNotExist{0, repoID, index}
|
||||
}
|
||||
issue := &Issue{
|
||||
RepoID: repoID,
|
||||
|
|
|
@ -9,14 +9,6 @@ import (
|
|||
"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
|
||||
// update it based on highest index of existing issues assigned to a repo
|
||||
func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error {
|
||||
|
@ -26,8 +18,8 @@ func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error {
|
|||
}
|
||||
defer committer.Close()
|
||||
|
||||
max, err := GetMaxIssueIndexForRepo(ctx, repoID)
|
||||
if err != nil {
|
||||
var max int64
|
||||
if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -370,6 +370,9 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.PullRequest = pullRequestMaps[issue.ID]
|
||||
if issue.PullRequest != nil {
|
||||
issue.PullRequest.Issue = issue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
@ -884,92 +883,6 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *
|
|||
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
|
||||
// Return empty slice if files missing
|
||||
// Return warning messages on parsing errors
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
|
@ -23,7 +22,7 @@ type PullRequestsOptions struct {
|
|||
db.ListOptions
|
||||
State string
|
||||
SortType string
|
||||
Labels []string
|
||||
Labels []int64
|
||||
MilestoneID int64
|
||||
}
|
||||
|
||||
|
@ -36,11 +35,9 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
|
|||
sess.And("issue.is_closed=?", opts.State == "closed")
|
||||
}
|
||||
|
||||
if labelIDs, err := base.StringsToInt64s(opts.Labels); err != nil {
|
||||
return nil, err
|
||||
} else if len(labelIDs) > 0 {
|
||||
if len(opts.Labels) > 0 {
|
||||
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 {
|
||||
|
@ -50,14 +47,6 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
|
|||
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
|
||||
func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) {
|
||||
prs := make([]*PullRequest, 0, 2)
|
||||
|
@ -220,3 +209,12 @@ func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bo
|
|||
Limit(1).
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
package issues_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -67,7 +66,6 @@ func TestPullRequestsNewest(t *testing.T) {
|
|||
},
|
||||
State: "open",
|
||||
SortType: "newest",
|
||||
Labels: []string{},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, count)
|
||||
|
@ -114,7 +112,6 @@ func TestPullRequestsOldest(t *testing.T) {
|
|||
},
|
||||
State: "open",
|
||||
SortType: "oldest",
|
||||
Labels: []string{},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
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) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(db.DefaultContext, 1, "master")
|
||||
|
|
|
@ -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)
|
||||
func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.User) (bool, error) {
|
||||
pr, err := GetPullRequestByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
pr := issue.PullRequest
|
||||
rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||
if err != nil {
|
||||
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)
|
||||
func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) {
|
||||
pr, err := GetPullRequestByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||
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 {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
@ -160,6 +160,10 @@ func MainTest(m *testing.M) {
|
|||
|
||||
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 {
|
||||
fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
|
||||
}
|
||||
|
|
|
@ -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"
|
|
@ -23,9 +23,9 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
|
|||
Type int
|
||||
|
||||
// Permissions
|
||||
IsAdmin bool
|
||||
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Visibility int `xorm:"NOT NULL DEFAULT 0"`
|
||||
IsAdmin bool
|
||||
// IsRestricted bool `xorm:"NOT NULL DEFAULT false"` glitch: this column was added in v1_12/v121.go
|
||||
// Visibility int `xorm:"NOT NULL DEFAULT 0"` glitch: this column was added in v1_12/v124.go
|
||||
}
|
||||
|
||||
type Review struct {
|
||||
|
@ -51,9 +51,9 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
|
|||
ReviewTypeReject int = 3
|
||||
|
||||
// VisibleTypePublic Visible for everyone
|
||||
VisibleTypePublic int = 0
|
||||
// VisibleTypePublic int = 0
|
||||
// VisibleTypePrivate Visible only for organization's members
|
||||
VisibleTypePrivate int = 2
|
||||
// VisibleTypePrivate int = 2
|
||||
|
||||
// unit.UnitTypeCode is unit type code
|
||||
UnitTypeCode int = 1
|
||||
|
@ -145,9 +145,9 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
|
|||
hasOrgVisible := true
|
||||
// Not SignedUser
|
||||
if user == nil {
|
||||
hasOrgVisible = repoOwner.Visibility == VisibleTypePublic
|
||||
// hasOrgVisible = repoOwner.Visibility == VisibleTypePublic // VisibleTypePublic is the default
|
||||
} else if !user.IsAdmin {
|
||||
hasMemberWithUserID, err := sess.
|
||||
_, err := sess.
|
||||
Where("uid=?", user.ID).
|
||||
And("org_id=?", repoOwner.ID).
|
||||
Table("org_user").
|
||||
|
@ -155,9 +155,10 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
|
|||
if err != nil {
|
||||
hasOrgVisible = false
|
||||
}
|
||||
if (repoOwner.Visibility == VisibleTypePrivate || user.IsRestricted) && !hasMemberWithUserID {
|
||||
hasOrgVisible = false
|
||||
}
|
||||
// VisibleTypePublic is the default so the condition below is always false
|
||||
// if (repoOwner.Visibility == VisibleTypePrivate) && !hasMemberWithUserID {
|
||||
// hasOrgVisible = false
|
||||
// }
|
||||
}
|
||||
|
||||
isCollaborator, err := sess.Get(&Collaboration{RepoID: repo.ID, UserID: user.ID})
|
||||
|
@ -195,7 +196,7 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
|
|||
|
||||
if user != nil {
|
||||
userID = user.ID
|
||||
restricted = user.IsRestricted
|
||||
restricted = false
|
||||
}
|
||||
|
||||
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.
|
||||
if !found && !repo.IsPrivate && !user.IsRestricted {
|
||||
if !found && !repo.IsPrivate {
|
||||
if _, ok := perm.UnitsMode[u.Type]; !ok {
|
||||
perm.UnitsMode[u.Type] = AccessModeRead
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ func FixMergeBase(x *xorm.Engine) error {
|
|||
} else {
|
||||
parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath})
|
||||
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
|
||||
}
|
||||
parents := strings.Split(strings.TrimSpace(parentsString), " ")
|
||||
|
|
|
@ -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})
|
||||
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
|
||||
}
|
||||
parents := strings.Split(strings.TrimSpace(parentsString), " ")
|
||||
|
|
|
@ -319,8 +319,9 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode
|
|||
|
||||
// Add initial creator to organization and owner team.
|
||||
if err = db.Insert(ctx, &OrgUser{
|
||||
UID: owner.ID,
|
||||
OrgID: org.ID,
|
||||
UID: owner.ID,
|
||||
OrgID: org.ID,
|
||||
IsPublic: setting.Service.DefaultOrgMemberVisible,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("insert org-user relation: %w", err)
|
||||
}
|
||||
|
|
|
@ -316,29 +316,3 @@ func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error {
|
|||
_, err := db.GetEngine(ctx).ID(unit.ID).Update(unit)
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -443,7 +443,7 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail
|
|||
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))
|
||||
if err != nil {
|
||||
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)
|
||||
err = db.GetEngine(ctx).Table("email_address").
|
||||
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).
|
||||
OrderBy(orderby).
|
||||
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
|
||||
|
|
|
@ -146,6 +146,7 @@ type User struct {
|
|||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||
Theme string `xorm:"NOT NULL DEFAULT ''"`
|
||||
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
||||
EnableRepoUnitHints bool `xorm:"NOT NULL DEFAULT true"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -451,6 +451,13 @@ func (opts ListWebhookOptions) ToConds() builder.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.
|
||||
func UpdateWebhook(ctx context.Context, w *Webhook) error {
|
||||
_, err := db.GetEngine(ctx).ID(w.ID).AllCols().Update(w)
|
||||
|
|
|
@ -124,6 +124,9 @@ func TestGetWebhookByOwnerID(t *testing.T) {
|
|||
|
||||
func TestGetActiveWebhooksByRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
activateWebhook(t, 1)
|
||||
|
||||
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, hooks, 1) {
|
||||
|
@ -144,6 +147,9 @@ func TestGetWebhooksByRepoID(t *testing.T) {
|
|||
|
||||
func TestGetActiveWebhooksByOwnerID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
activateWebhook(t, 3)
|
||||
|
||||
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
|
||||
assert.NoError(t, err)
|
||||
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) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
activateWebhook(t, 3)
|
||||
|
||||
hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, hooks, 1) {
|
||||
|
|
|
@ -74,9 +74,6 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
|
|||
case GithubEventGollum:
|
||||
return triggedEvent == webhook_module.HookEventWiki
|
||||
|
||||
case GithubEventSchedule:
|
||||
return triggedEvent == webhook_module.HookEventSchedule
|
||||
|
||||
case GithubEventIssues:
|
||||
switch triggedEvent {
|
||||
case webhook_module.HookEventIssues,
|
||||
|
@ -119,6 +116,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
|
|||
return triggedEvent == webhook_module.HookEventIssueComment ||
|
||||
triggedEvent == webhook_module.HookEventPullRequestComment
|
||||
|
||||
case GithubEventSchedule:
|
||||
return triggedEvent == webhook_module.HookEventSchedule
|
||||
|
||||
default:
|
||||
return eventName == string(triggedEvent)
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limi
|
|||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("scan: %w", err)
|
||||
return nil, fmt.Errorf("ReadLogs scan: %w", err)
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
|
|
|
@ -41,6 +41,12 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
|
|||
}
|
||||
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
|
||||
for _, step := range task.Steps {
|
||||
if step.Status.HasRun() {
|
||||
|
@ -56,11 +62,15 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
|
|||
Name: postStepName,
|
||||
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.LogLength = task.LogLength - postStep.LogIndex
|
||||
postStep.Status = task.Status
|
||||
postStep.Started = lastHasRunStep.Stopped
|
||||
postStep.Status = actions_model.StatusRunning
|
||||
}
|
||||
if task.Status.IsDone() {
|
||||
postStep.Status = task.Status
|
||||
postStep.Stopped = task.Stopped
|
||||
}
|
||||
ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2)
|
||||
|
|
|
@ -103,6 +103,40 @@ func TestFullSteps(t *testing.T) {
|
|||
{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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -22,7 +22,7 @@ import (
|
|||
|
||||
type DetectedWorkflow struct {
|
||||
EntryName string
|
||||
TriggerEvent string
|
||||
TriggerEvent *jobparser.Event
|
||||
Content []byte
|
||||
}
|
||||
|
||||
|
@ -103,6 +103,7 @@ func DetectWorkflows(
|
|||
commit *git.Commit,
|
||||
triggedEvent webhook_module.HookEventType,
|
||||
payload api.Payloader,
|
||||
detectSchedule bool,
|
||||
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
|
||||
entries, err := ListWorkflows(commit)
|
||||
if err != nil {
|
||||
|
@ -117,6 +118,7 @@ func DetectWorkflows(
|
|||
return nil, 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)
|
||||
|
@ -125,17 +127,18 @@ func DetectWorkflows(
|
|||
for _, evt := range events {
|
||||
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
|
||||
if evt.IsSchedule() {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt.Name,
|
||||
Content: content,
|
||||
if detectSchedule {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt,
|
||||
Content: content,
|
||||
}
|
||||
schedules = append(schedules, dwf)
|
||||
}
|
||||
schedules = append(schedules, dwf)
|
||||
}
|
||||
if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
||||
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt.Name,
|
||||
TriggerEvent: evt,
|
||||
Content: content,
|
||||
}
|
||||
workflows = append(workflows, dwf)
|
||||
|
@ -146,6 +149,41 @@ func DetectWorkflows(
|
|||
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 {
|
||||
if !canGithubEventMatch(evt.Name, triggedEvent) {
|
||||
return false
|
||||
|
@ -153,11 +191,11 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
|||
|
||||
switch triggedEvent {
|
||||
case // events with no activity types
|
||||
webhook_module.HookEventSchedule,
|
||||
webhook_module.HookEventCreate,
|
||||
webhook_module.HookEventDelete,
|
||||
webhook_module.HookEventFork,
|
||||
webhook_module.HookEventWiki:
|
||||
webhook_module.HookEventWiki,
|
||||
webhook_module.HookEventSchedule:
|
||||
if len(evt.Acts()) != 0 {
|
||||
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
|
||||
}
|
||||
|
|
|
@ -150,13 +150,16 @@ func TruncateString(str string, limit int) string {
|
|||
|
||||
// StringsToInt64s converts a slice of string to a slice of int64.
|
||||
func StringsToInt64s(strs []string) ([]int64, error) {
|
||||
ints := make([]int64, len(strs))
|
||||
for i := range strs {
|
||||
n, err := strconv.ParseInt(strs[i], 10, 64)
|
||||
if strs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
ints := make([]int64, 0, len(strs))
|
||||
for _, s := range strs {
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return ints, err
|
||||
return nil, err
|
||||
}
|
||||
ints[i] = n
|
||||
ints = append(ints, n)
|
||||
}
|
||||
return ints, nil
|
||||
}
|
||||
|
|
|
@ -138,12 +138,13 @@ func TestStringsToInt64s(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
testSuccess(nil, nil)
|
||||
testSuccess([]string{}, []int64{})
|
||||
testSuccess([]string{"-1234"}, []int64{-1234})
|
||||
testSuccess([]string{"1", "4", "16", "64", "256"},
|
||||
[]int64{1, 4, 16, 64, 256})
|
||||
testSuccess([]string{"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)
|
||||
}
|
||||
|
||||
|
|
|
@ -367,7 +367,6 @@ type RunStdError interface {
|
|||
error
|
||||
Unwrap() error
|
||||
Stderr() string
|
||||
IsExitCode(code int) bool
|
||||
}
|
||||
|
||||
type runStdError struct {
|
||||
|
@ -392,9 +391,9 @@ func (r *runStdError) Stderr() string {
|
|||
return r.stderr
|
||||
}
|
||||
|
||||
func (r *runStdError) IsExitCode(code int) bool {
|
||||
func IsErrorExitCode(err error, code int) bool {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(r.err, &exitError) {
|
||||
if errors.As(err, &exitError) {
|
||||
return exitError.ExitCode() == code
|
||||
}
|
||||
return false
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
@ -25,18 +26,12 @@ type Commit struct {
|
|||
Author *Signature
|
||||
Committer *Signature
|
||||
CommitMessage string
|
||||
Signature *CommitGPGSignature
|
||||
Signature *ObjectSignature
|
||||
|
||||
Parents []ObjectID // ID strings
|
||||
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.
|
||||
func (c *Commit) Message() string {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
||||
func convertPGPSignature(c *object.Commit) *ObjectSignature {
|
||||
if c.PGPSignature == "" {
|
||||
return nil
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
|||
return nil
|
||||
}
|
||||
|
||||
return &CommitGPGSignature{
|
||||
return &ObjectSignature{
|
||||
Signature: c.PGPSignature,
|
||||
Payload: w.String(),
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ readLoop:
|
|||
}
|
||||
}
|
||||
commit.CommitMessage = messageSB.String()
|
||||
commit.Signature = &CommitGPGSignature{
|
||||
commit.Signature = &ObjectSignature{
|
||||
Signature: signatureSB.String(),
|
||||
Payload: payloadSB.String(),
|
||||
}
|
||||
|
|
|
@ -33,9 +33,10 @@ var (
|
|||
// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
|
||||
DefaultContext context.Context
|
||||
|
||||
SupportProcReceive bool // >= 2.29
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
InvertedGitFlushEnv bool // 2.43.1
|
||||
SupportProcReceive bool // >= 2.29
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
InvertedGitFlushEnv bool // 2.43.1
|
||||
SupportCheckAttrOnBare bool // >= 2.40
|
||||
|
||||
gitVersion *version.Version
|
||||
)
|
||||
|
@ -187,6 +188,7 @@ func InitFull(ctx context.Context) (err error) {
|
|||
}
|
||||
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
||||
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit
|
||||
SupportCheckAttrOnBare = CheckGitVersionAtLeast("2.40") == nil
|
||||
if SupportHashSha256 {
|
||||
SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat)
|
||||
} else {
|
||||
|
@ -340,7 +342,7 @@ func CheckGitVersionEqual(equal string) error {
|
|||
|
||||
func configSet(key, value string) error {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -363,7 +365,7 @@ func configSetNonExist(key, value string) error {
|
|||
// already exist
|
||||
return nil
|
||||
}
|
||||
if err.IsExitCode(1) {
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist, set new config
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
|
@ -381,7 +383,7 @@ func configAddNonExist(key, value string) error {
|
|||
// already exist
|
||||
return nil
|
||||
}
|
||||
if err.IsExitCode(1) {
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist, add new config
|
||||
_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil)
|
||||
if err != nil {
|
||||
|
@ -402,7 +404,7 @@ func configUnsetAll(key, value string) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
if err.IsExitCode(1) {
|
||||
if IsErrorExitCode(err, 1) {
|
||||
// not exist
|
||||
return nil
|
||||
}
|
||||
|
|
117
modules/git/grep.go
Normal file
117
modules/git/grep.go
Normal 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
51
modules/git/grep_test.go
Normal 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)
|
||||
}
|
11
modules/git/object_signature.go
Normal file
11
modules/git/object_signature.go
Normal 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
|
||||
}
|
|
@ -283,7 +283,7 @@ type DivergeObject struct {
|
|||
// 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) {
|
||||
cmd := NewCommand(ctx, "rev-list", "--count", "--left-right").
|
||||
AddDynamicArguments(baseBranch + "..." + targetBranch)
|
||||
AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")
|
||||
stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
|
||||
if err != nil {
|
||||
return do, err
|
||||
|
|
|
@ -7,65 +7,147 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
// CheckAttributeOpts represents the possible options to CheckAttribute
|
||||
type CheckAttributeOpts struct {
|
||||
CachedOnly bool
|
||||
AllAttributes bool
|
||||
Attributes []string
|
||||
Filenames []string
|
||||
IndexFile string
|
||||
WorkTree string
|
||||
var LinguistAttributes = []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"}
|
||||
|
||||
// GitAttribute exposes an attribute from the .gitattribute file
|
||||
type GitAttribute string //nolint:revive
|
||||
|
||||
// IsSpecified returns true if the gitattribute is set and not empty
|
||||
func (ca GitAttribute) IsSpecified() bool {
|
||||
return ca != "" && ca != "unspecified"
|
||||
}
|
||||
|
||||
// CheckAttribute return the Blame object of file
|
||||
func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) {
|
||||
env := []string{}
|
||||
|
||||
if len(opts.IndexFile) > 0 {
|
||||
env = append(env, "GIT_INDEX_FILE="+opts.IndexFile)
|
||||
// String returns the value of the attribute or "" if unspecified
|
||||
func (ca GitAttribute) String() string {
|
||||
if !ca.IsSpecified() {
|
||||
return ""
|
||||
}
|
||||
if len(opts.WorkTree) > 0 {
|
||||
env = append(env, "GIT_WORK_TREE="+opts.WorkTree)
|
||||
return string(ca)
|
||||
}
|
||||
|
||||
// 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 = append(os.Environ(), env...)
|
||||
env := os.Environ()
|
||||
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 = ""
|
||||
}
|
||||
|
||||
stdOut := new(bytes.Buffer)
|
||||
stdErr := new(bytes.Buffer)
|
||||
|
||||
cmd := NewCommand(repo.Ctx, "check-attr", "-z")
|
||||
|
||||
if opts.AllAttributes {
|
||||
cmd.AddArguments("-a")
|
||||
} else {
|
||||
for _, attribute := range opts.Attributes {
|
||||
if attribute != "" {
|
||||
cmd.AddDynamicArguments(attribute)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(repo.Ctx)
|
||||
if deleteTemporaryFile != nil {
|
||||
ctxCancel := cancel
|
||||
cancel = func() {
|
||||
ctxCancel()
|
||||
deleteTemporaryFile()
|
||||
}
|
||||
}
|
||||
|
||||
if opts.CachedOnly {
|
||||
cmd := NewCommand(ctx, "check-attr", "-z")
|
||||
|
||||
if hasIndex {
|
||||
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{
|
||||
Env: env,
|
||||
Dir: repo.Path,
|
||||
Stdout: stdOut,
|
||||
Stderr: stdErr,
|
||||
}); err != nil {
|
||||
// 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 {
|
||||
env = append(env, "GIT_FLUSH=0")
|
||||
} else {
|
||||
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())
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
name2attribute2info := make(map[string]map[string]string)
|
||||
|
||||
for i := 0; i < (len(fields) / 3); i++ {
|
||||
filename := string(fields[3*i])
|
||||
attribute := string(fields[3*i+1])
|
||||
info := string(fields[3*i+2])
|
||||
attribute2info := name2attribute2info[filename]
|
||||
if attribute2info == nil {
|
||||
attribute2info = make(map[string]string)
|
||||
}
|
||||
attribute2info[attribute] = info
|
||||
name2attribute2info[filename] = attribute2info
|
||||
values := make(map[string]GitAttribute, len(attributes))
|
||||
for ; len(fields) >= 3; fields = fields[3:] {
|
||||
// filename := string(fields[0])
|
||||
attribute := string(fields[1])
|
||||
value := string(fields[2])
|
||||
values[attribute] = GitAttribute(value)
|
||||
}
|
||||
|
||||
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
|
||||
return values, nil
|
||||
}
|
||||
|
||||
type attributeTriple struct {
|
||||
|
@ -275,10 +216,6 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
|
|||
return len(p), nil
|
||||
}
|
||||
|
||||
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
|
||||
return wr.attributes
|
||||
}
|
||||
|
||||
func (wr *nulSeparatedAttributeWriter) Close() error {
|
||||
select {
|
||||
case <-wr.closed:
|
||||
|
@ -290,49 +227,87 @@ func (wr *nulSeparatedAttributeWriter) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Create a check attribute reader for the current repository and provided commit ID
|
||||
func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) {
|
||||
indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
|
||||
// GitAttributeChecker creates an AttributeChecker for the given repository and provided commit ID.
|
||||
//
|
||||
// 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 {
|
||||
return nil, func() {}
|
||||
return AttributeChecker{}, err
|
||||
}
|
||||
|
||||
checker := &CheckAttributeReader{
|
||||
Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"},
|
||||
Repo: repo,
|
||||
IndexFile: indexFilename,
|
||||
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()
|
||||
ac := AttributeChecker{
|
||||
attributeNumber: len(attributes),
|
||||
ctx: cmd.parentContext,
|
||||
cancel: cancel, // will be cancelled on Close
|
||||
}
|
||||
|
||||
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
|
||||
func attributeToBool(attr map[string]string, name string) optional.Option[bool] {
|
||||
if value, has := attr[name]; has && value != "unspecified" {
|
||||
switch value {
|
||||
case "set", "true":
|
||||
return optional.Some(true)
|
||||
case "unset", "false":
|
||||
return optional.Some(false)
|
||||
type AttributeChecker struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
stdinWriter *os.File
|
||||
attributeNumber int
|
||||
attributesCh <-chan attributeTriple
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -4,10 +4,14 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
|
||||
|
@ -22,7 +26,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
|
|||
assert.Len(t, testStr, n)
|
||||
assert.NoError(t, err)
|
||||
select {
|
||||
case attr := <-wr.ReadAttribute():
|
||||
case attr := <-wr.attributes:
|
||||
assert.Equal(t, ".gitignore\"\n", attr.Filename)
|
||||
assert.Equal(t, "linguist-vendored", attr.Attribute)
|
||||
assert.Equal(t, "unspecified", attr.Value)
|
||||
|
@ -36,7 +40,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
select {
|
||||
case attr := <-wr.ReadAttribute():
|
||||
case attr := <-wr.attributes:
|
||||
assert.Equal(t, ".gitignore\"\n", attr.Filename)
|
||||
assert.Equal(t, "linguist-vendored", attr.Attribute)
|
||||
assert.Equal(t, "unspecified", attr.Value)
|
||||
|
@ -51,14 +55,14 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-wr.ReadAttribute():
|
||||
case <-wr.attributes:
|
||||
assert.FailNow(t, "There should not be an attribute ready to read")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
_, err = wr.Write([]byte("attribute\x00"))
|
||||
assert.NoError(t, err)
|
||||
select {
|
||||
case <-wr.ReadAttribute():
|
||||
case <-wr.attributes:
|
||||
assert.FailNow(t, "There should not be an attribute ready to read")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
@ -66,28 +70,28 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
|
|||
_, err = wr.Write([]byte("value\x00"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
attr := <-wr.ReadAttribute()
|
||||
attr := <-wr.attributes
|
||||
assert.Equal(t, "incomplete-filename", attr.Filename)
|
||||
assert.Equal(t, "attribute", attr.Attribute)
|
||||
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"))
|
||||
assert.NoError(t, err)
|
||||
attr = <-wr.ReadAttribute()
|
||||
attr = <-wr.attributes
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, attributeTriple{
|
||||
Filename: "shouldbe.vendor",
|
||||
Attribute: "linguist-vendored",
|
||||
Value: "set",
|
||||
}, attr)
|
||||
attr = <-wr.ReadAttribute()
|
||||
attr = <-wr.attributes
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, attributeTriple{
|
||||
Filename: "shouldbe.vendor",
|
||||
Attribute: "linguist-generated",
|
||||
Value: "unspecified",
|
||||
}, attr)
|
||||
attr = <-wr.ReadAttribute()
|
||||
attr = <-wr.attributes
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, attributeTriple{
|
||||
Filename: "shouldbe.vendor",
|
||||
|
@ -95,3 +99,112 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
|
|||
Value: "unspecified",
|
||||
}, 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())
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ package git
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/analyze"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -61,8 +61,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
return nil, err
|
||||
}
|
||||
|
||||
checker, deferable := repo.CheckAttributeReader(commitID)
|
||||
defer deferable()
|
||||
checker, err := repo.GitAttributeChecker(commitID, LinguistAttributes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer checker.Close()
|
||||
|
||||
contentBuf := bytes.Buffer{}
|
||||
var content []byte
|
||||
|
@ -102,41 +105,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
isDocumentation := optional.None[bool]()
|
||||
isDetectable := optional.None[bool]()
|
||||
|
||||
if checker != nil {
|
||||
attrs, err := checker.CheckPath(f.Name())
|
||||
if err == nil {
|
||||
isVendored = attributeToBool(attrs, "linguist-vendored")
|
||||
isGenerated = attributeToBool(attrs, "linguist-generated")
|
||||
isDocumentation = attributeToBool(attrs, "linguist-documentation")
|
||||
isDetectable = attributeToBool(attrs, "linguist-detectable")
|
||||
if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
|
||||
// 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
|
||||
} 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
|
||||
}
|
||||
attrs, err := checker.CheckPath(f.Name())
|
||||
if err == nil {
|
||||
isVendored = attrs["linguist-vendored"].Bool()
|
||||
isGenerated = attrs["linguist-generated"].Bool()
|
||||
isDocumentation = attrs["linguist-documentation"].Bool()
|
||||
isDetectable = attrs["linguist-detectable"].Bool()
|
||||
if language := cmp.Or(
|
||||
attrs["linguist-language"].String(),
|
||||
attrs["gitlab-language"].Prefix(),
|
||||
); language != "" {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
for _, v := range authors {
|
||||
a = append(a, v)
|
||||
|
|
|
@ -185,17 +185,22 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) {
|
|||
|
||||
tag.Tagger = parseSignatureFromCommitLine(ref["creator"])
|
||||
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)
|
||||
if pgpStart >= 0 {
|
||||
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"] != "" {
|
||||
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.Signature = &CommitGPGSignature{
|
||||
tag.Signature = &ObjectSignature{
|
||||
Signature: ref["contents:signature"],
|
||||
Payload: payload,
|
||||
}
|
||||
|
|
|
@ -315,7 +315,7 @@ qbHDASXl
|
|||
Type: "tag",
|
||||
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",
|
||||
Signature: &CommitGPGSignature{
|
||||
Signature: &ObjectSignature{
|
||||
Signature: `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
const (
|
||||
beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n"
|
||||
endpgp = "\n-----END PGP SIGNATURE-----"
|
||||
beginssh = "\n-----BEGIN SSH SIGNATURE-----\n"
|
||||
endssh = "\n-----END SSH SIGNATURE-----"
|
||||
)
|
||||
|
||||
// Tag represents a Git tag.
|
||||
|
@ -24,7 +26,7 @@ type Tag struct {
|
|||
Type string
|
||||
Tagger *Signature
|
||||
Message string
|
||||
Signature *CommitGPGSignature
|
||||
Signature *ObjectSignature
|
||||
}
|
||||
|
||||
// Commit return the commit of the tag reference
|
||||
|
@ -71,17 +73,36 @@ l:
|
|||
break l
|
||||
}
|
||||
}
|
||||
idx := strings.LastIndex(tag.Message, beginpgp)
|
||||
if idx > 0 {
|
||||
endSigIdx := strings.Index(tag.Message[idx:], endpgp)
|
||||
if endSigIdx > 0 {
|
||||
tag.Signature = &CommitGPGSignature{
|
||||
Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)],
|
||||
Payload: string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]),
|
||||
}
|
||||
tag.Message = tag.Message[:idx+1]
|
||||
|
||||
extractTagSignature := func(signatureBeginMark, signatureEndMark string) (bool, *ObjectSignature, string) {
|
||||
idx := strings.LastIndex(tag.Message, signatureBeginMark)
|
||||
if idx == -1 {
|
||||
return false, nil, ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,41 @@ ono`), tag: Tag{
|
|||
Message: "test message\no\n\nono",
|
||||
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 {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
bare = true
|
||||
logallrefupdates = true
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
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
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
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
|
||||
|
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue