mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-09 04:30:16 +09:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ebd5e319b | |||
| d6945fb0b5 | |||
| 69a2bfcfbd | |||
| 15a0ea83ca | |||
| 30cc41ff97 | |||
| 7f672160de | |||
| e27f69bc88 | |||
| a67171af37 | |||
| c7bd37e901 | |||
| 0a4078558e | |||
| e66e773a6f | |||
| 08c123299a | |||
| 219b0e7c17 | |||
| 02fd18970b | |||
| efee6d7ce4 | |||
| 0ca36eeb3c | |||
| 3ee1924069 | |||
| 99ac787400 | |||
| 1856fef7a8 | |||
| 7440f1e452 | |||
| 638c75a4d3 | |||
| 575754cbeb | |||
| f24f3e1eda | |||
| 1f615a406f | |||
| 9c77a87908 | |||
| 4c3df3ab3c | |||
| 565957503c | |||
| e48591ba64 | |||
| f2a08e0261 | |||
| 532fb8f4f4 | |||
| 25416e9be7 | |||
| 1c4fff096f | |||
| d16daed041 | |||
| 2d58c8c3df | |||
| 1e28bb1bd7 | |||
| f0e8c3c3d0 | |||
| 7c58b73243 | |||
| 6546382f4e | |||
| 0930bd71fe | |||
| 15b8a5805a |
@@ -63,6 +63,7 @@ cpu.out
|
||||
/public/assets/fonts
|
||||
/public/assets/img/avatar
|
||||
/vendor
|
||||
/VERSION
|
||||
/.air
|
||||
/.go-licenses
|
||||
/Dockerfile
|
||||
|
||||
@@ -34,7 +34,7 @@ runs:
|
||||
env:
|
||||
# pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
|
||||
GOTEST_FLAGS: -race -timeout=40m
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
TEST_LDAP: 1
|
||||
TEST_SHARD: ${{ inputs.shard }}
|
||||
TEST_TOTAL_SHARDS: ${{ inputs.total-shards }}
|
||||
|
||||
@@ -37,15 +37,22 @@ jobs:
|
||||
- uses: ./.github/actions/go-setup
|
||||
- run: make deps-backend deps-tools
|
||||
- run: TAGS="bindata" make backend
|
||||
- run: TAGS="bindata gogit" make backend
|
||||
- name: warm test compile cache (bindata)
|
||||
env:
|
||||
TAGS: bindata
|
||||
GOTEST_FLAGS: -race -list=^$$ -count=1
|
||||
run: make test-backend
|
||||
- name: warm test compile cache (bindata gogit)
|
||||
env:
|
||||
TAGS: bindata gogit
|
||||
GOTEST_FLAGS: -race -list=^$$ -count=1
|
||||
run: make test-backend
|
||||
- name: warm integration compile cache
|
||||
run: |
|
||||
TAGS="bindata" make test-integration-compile
|
||||
TAGS="bindata" GOTEST_FLAGS="-race" make test-integration-compile
|
||||
TAGS="bindata gogit" make test-integration-compile
|
||||
TAGS="bindata gogit" GOTEST_FLAGS="-race" make test-integration-compile
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -9,7 +9,7 @@ concurrency:
|
||||
group: cron-renovate
|
||||
|
||||
env:
|
||||
RENOVATE_VERSION: 44.121.4 # renovate: datasource=npm depName=renovate
|
||||
RENOVATE_VERSION: 44.109.1 # renovate: datasource=npm depName=renovate
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: renovatebot/github-action@6d26fcf0275dc65624cbc3d51fe78bc773f98321 # v46.3.6
|
||||
- uses: renovatebot/github-action@9fea9f0fbf80401026d11d03911d62b1f70fef1f # v46.3.3
|
||||
with:
|
||||
renovate-version: ${{ env.RENOVATE_VERSION }}
|
||||
configurationFile: renovate.json5
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: crowdin/github-action@9c23991700c0ec5256fd41089b9d9d7d540e424e # v3.3.0
|
||||
- uses: crowdin/github-action@df474cdfb9f41d6ae777118749477c2cdf7cacc8 # v3.2.0
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
@@ -93,13 +93,13 @@ jobs:
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: arm64
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- name: build-backend-windows
|
||||
run: go build -ldflags '-s -w' -o gitea-windows
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- name: build-backend-386
|
||||
run: go build -ldflags '-s -w' -o gitea-linux-386
|
||||
env:
|
||||
|
||||
@@ -95,17 +95,17 @@ jobs:
|
||||
- run: make deps-backend
|
||||
- run: make backend
|
||||
env:
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- run: GITEA_TEST_DATABASE=sqlite make test-migration
|
||||
env:
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
- name: run tests
|
||||
run: GITEA_TEST_DATABASE=sqlite make test-integration
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
|
||||
GOTEST_FLAGS: -timeout=40m
|
||||
TAGS: bindata
|
||||
TAGS: bindata gogit
|
||||
|
||||
test-unit:
|
||||
if: needs.files-changed.outputs.backend == 'true'
|
||||
@@ -163,6 +163,13 @@ jobs:
|
||||
GOTEST_FLAGS: -race -timeout=20m
|
||||
TAGS: bindata
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
- name: unit-tests-gogit
|
||||
run: make test-backend
|
||||
env:
|
||||
GOTEST_FLAGS: -race -timeout=20m
|
||||
TAGS: bindata gogit
|
||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
||||
GITEA_TEST_CI_SKIP_EXTERNAL: true
|
||||
- run: make test-check
|
||||
|
||||
test-mysql:
|
||||
|
||||
@@ -37,15 +37,8 @@ jobs:
|
||||
# retry snapcraft uploads which can be flaky
|
||||
- name: Publish snap
|
||||
run: |
|
||||
# part-gitea-pull.sh decides what is built, see the comments there:
|
||||
# * grade devel (main or a prerelease tag) goes to latest/edge
|
||||
# * grade stable (a new release tag) goes to latest/stable, and to latest/candidate which marks it as released
|
||||
channel=latest/edge
|
||||
if unsquashfs -cat "$SNAP" meta/snap.yaml | grep -qx 'grade: stable'; then
|
||||
channel=latest/stable,latest/candidate
|
||||
fi
|
||||
for attempt in 1 2 3 4 5; do
|
||||
snapcraft upload "$SNAP" --release "$channel" && exit 0
|
||||
snapcraft upload "$SNAP" --release latest/edge && exit 0
|
||||
echo "::warning::snap upload attempt $attempt failed, retrying in 15s"
|
||||
sleep 15
|
||||
done
|
||||
|
||||
@@ -76,10 +76,6 @@ jobs:
|
||||
run: |
|
||||
previous=$(git tag --list --sort=-v:refname | grep -xE 'v[0-9]+\.[0-9]+\.[0-9]+' | grep -A1 -xF "$GITHUB_REF_NAME" | tail -1) # highest stable version below this one
|
||||
echo "range=$previous..$GITHUB_SHA" >> "$GITHUB_OUTPUT"
|
||||
- name: skip reverts and reverted commits in changelog
|
||||
env:
|
||||
RANGE: ${{ steps.range.outputs.range }}
|
||||
run: ./tools/generate-cliffignore.sh "$RANGE" | sort -u > .cliffignore
|
||||
- uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
|
||||
with:
|
||||
args: --tag ${{ github.ref_name }} ${{ steps.range.outputs.range }}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
- In `options/locale`, only edit `locale_en-US.json`, other locales are synced automatically
|
||||
- In TS, use `!` instead of `?.`/`??` when a value always exists
|
||||
- In Go, prefer to use modern language features wherever possible
|
||||
- In Go, function-name prefixes in errors like `fmt.Errorf("Foo: %w", err)` must always name the function they are in
|
||||
- Write sizes as multiplications like `64 * 1024`, not bit shifts like `64 << 10`
|
||||
- Prefer `tw-*` utilities over inline `style` and `flex-*` helpers over per-child `tw-ml-*`/`tw-mr-*` margins, falling back to `tw-*` where specificity requires `!important`
|
||||
- Run `make fmt` after `.go` edits, `make tidy` after `go.mod` edits, `make generate-swagger` after API changes, and lint what changed with `make lint-go`, `lint-js`, `lint-css` or `lint-templates`
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ PR titles must follow the [Conventional Commits](https://www.conventionalcommits
|
||||
type(scope)!: subject
|
||||
```
|
||||
|
||||
The scope in parentheses is optional. A `!` immediately before the colon marks a [breaking change](https://www.conventionalcommits.org/en/v1.0.0/#summary): either `type!:` or `type(scope)!:` (not `type!(scope):`). Do not mark `build`, `chore`, `ci`, `docs`, `style`, or `test` as breaking.
|
||||
The scope in parentheses is optional. A `!` immediately before the colon marks a [breaking change](https://www.conventionalcommits.org/en/v1.0.0/#summary): either `type!:` or `type(scope)!:` (not `type!(scope):`).
|
||||
|
||||
Use one of these types:
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ SHASUM ?= shasum -a 256
|
||||
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1.67.4 # renovate: datasource=go
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v4/cmd/editorconfig-checker@v4.0.2 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0 # renovate: datasource=go
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2 # renovate: datasource=go
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.17 # renovate: datasource=go
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.36.6 # renovate: datasource=go
|
||||
@@ -75,7 +75,8 @@ STORED_VERSION_FILE := VERSION
|
||||
GITHUB_REF_TYPE ?= branch
|
||||
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# VERSION: the name for the build and filenames, e.g.: "1.2.3" for tag "v1.2.3", "main-nightly" for branch "main"
|
||||
# VERSION: the branch name for the build and filenames, e.g.: "feature/foo-bar", "main"
|
||||
# branch name "release/v1.27.2" is stripped to "1.27.2".
|
||||
# GITEA_VERSION: the Gitea's internal version for display, e.g. "1.28.0+dev-356-ge47d0b66ea"
|
||||
ifeq ($(GITHUB_REF_TYPE),tag)
|
||||
# convert tag "v1.2.3" to "1.2.3"
|
||||
@@ -100,6 +101,11 @@ else
|
||||
$(error unsupported ref type $(GITHUB_REF_TYPE))
|
||||
endif
|
||||
|
||||
# if version == "main" then add "-nightly" to the version for nightly builds: "main-nightly"
|
||||
ifeq ($(VERSION),main)
|
||||
VERSION := main-nightly
|
||||
endif
|
||||
|
||||
LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
||||
RELEASE_ENV = GO="$(GO)" TAGS="$(TAGS)" LDFLAGS="$(LDFLAGS)" DIST="$(DIST)" VERSION="$(VERSION)"
|
||||
|
||||
@@ -550,7 +556,7 @@ release-compress: | $(DIST_DIRS)
|
||||
|
||||
.PHONY: release-sources
|
||||
release-sources: | $(DIST_DIRS)
|
||||
echo $(GITEA_VERSION) > $(STORED_VERSION_FILE)
|
||||
echo $(VERSION) > $(STORED_VERSION_FILE)
|
||||
# bsdtar needs a ^ to prevent matching subdirectories
|
||||
$(eval EXCL := --exclude=$(shell tar --help | grep -q bsdtar && echo "^")./)
|
||||
# use transform to a add a release-folder prefix; in bsdtar the transform parameter equivalent is -s
|
||||
|
||||
Generated
+45
File diff suppressed because one or more lines are too long
+8
-2
@@ -13,11 +13,13 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/dump"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -247,8 +249,12 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
log.Info("Packing data directory...%s", setting.AppDataPath)
|
||||
|
||||
var excludes []string
|
||||
if setting.SessionConfig.Provider == "file" {
|
||||
excludes = append(excludes, setting.SessionConfig.ProviderConfig)
|
||||
if setting.SessionConfig.OriginalProvider == "file" {
|
||||
var opts session.Options
|
||||
if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
excludes = append(excludes, opts.ProviderConfig)
|
||||
}
|
||||
|
||||
if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
|
||||
|
||||
+50
-14
@@ -169,6 +169,10 @@ func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, re
|
||||
}
|
||||
|
||||
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
}
|
||||
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||||
@@ -209,6 +213,7 @@ Gitea or set your environment appropriately.`, "")
|
||||
refFullNames := make([]git.RefName, hookBatchSize)
|
||||
count := 0
|
||||
total := 0
|
||||
lastline := 0
|
||||
|
||||
out := io.Discard
|
||||
if setting.Git.VerbosePush {
|
||||
@@ -221,6 +226,8 @@ Gitea or set your environment appropriately.`, "")
|
||||
}
|
||||
}
|
||||
|
||||
supportProcReceive := git.DefaultFeatures().SupportProcReceive
|
||||
|
||||
for scanner.Scan() {
|
||||
// TODO: support news feeds for wiki
|
||||
if isWiki {
|
||||
@@ -233,23 +240,37 @@ Gitea or set your environment appropriately.`, "")
|
||||
}
|
||||
|
||||
total++
|
||||
oldCommitIDs[count] = oldCommitID
|
||||
newCommitIDs[count] = newCommitID
|
||||
refFullNames[count] = refFullName
|
||||
count++
|
||||
fmt.Fprintf(out, "*")
|
||||
lastline++
|
||||
|
||||
if count >= hookBatchSize {
|
||||
fmt.Fprintf(out, " Checking %d references\n", count)
|
||||
// If the ref is a branch or tag, check if it's protected
|
||||
// if supportProcReceive all ref should be checked because
|
||||
// permission check was delayed
|
||||
if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() {
|
||||
oldCommitIDs[count] = oldCommitID
|
||||
newCommitIDs[count] = newCommitID
|
||||
refFullNames[count] = refFullName
|
||||
count++
|
||||
fmt.Fprintf(out, "*")
|
||||
|
||||
hookOptions.OldCommitIDs = oldCommitIDs
|
||||
hookOptions.NewCommitIDs = newCommitIDs
|
||||
hookOptions.RefFullNames = refFullNames
|
||||
extra := private.HookPreReceive(ctx, ownerName, repoName, hookOptions)
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
|
||||
if count >= hookBatchSize {
|
||||
fmt.Fprintf(out, " Checking %d references\n", count)
|
||||
|
||||
hookOptions.OldCommitIDs = oldCommitIDs
|
||||
hookOptions.NewCommitIDs = newCommitIDs
|
||||
hookOptions.RefFullNames = refFullNames
|
||||
extra := private.HookPreReceive(ctx, ownerName, repoName, hookOptions)
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error)
|
||||
}
|
||||
count = 0
|
||||
lastline = 0
|
||||
}
|
||||
count = 0
|
||||
} else {
|
||||
fmt.Fprintf(out, ".")
|
||||
}
|
||||
if lastline >= hookBatchSize {
|
||||
fmt.Fprintf(out, "\n")
|
||||
lastline = 0
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
@@ -267,6 +288,8 @@ Gitea or set your environment appropriately.`, "")
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error)
|
||||
}
|
||||
} else if lastline > 0 {
|
||||
fmt.Fprintf(out, "\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "Checked %d references in total\n", total)
|
||||
@@ -276,6 +299,10 @@ Gitea or set your environment appropriately.`, "")
|
||||
// runHookUpdate avoid to do heavy operations on update hook because it will be
|
||||
// invoked for every ref update which does not like pre-receive and post-receive
|
||||
func runHookUpdate(_ context.Context, c *cli.Command) error {
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update is empty and is kept only for backwards compatibility
|
||||
if len(os.Args) < 3 {
|
||||
return nil
|
||||
@@ -296,6 +323,11 @@ func runHookPostReceive(ctx context.Context, c *cli.Command) error {
|
||||
return fmt.Errorf("failed to call 'git update-server-info': %w", err)
|
||||
}
|
||||
|
||||
// Now if we're an internal don't do anything else
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
|
||||
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
|
||||
return fail(ctx, `Rejecting changes as Gitea environment not set.
|
||||
@@ -443,6 +475,10 @@ Gitea or set your environment appropriately.`, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.")
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
repoUser := os.Getenv(repo_module.EnvRepoUsername)
|
||||
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
||||
|
||||
+6
-4
@@ -195,10 +195,12 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
if len(sshCmdArgs) < 2 {
|
||||
// for AGit Flow
|
||||
if cmd == "ssh_info" {
|
||||
cprintf(c, "%s", agit.SshInfoJson)
|
||||
return nil
|
||||
if git.DefaultFeatures().SupportProcReceive {
|
||||
// for AGit Flow
|
||||
if cmd == "ssh_info" {
|
||||
cprintf(c, "%s", agit.SshInfoJson)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
|
||||
}
|
||||
|
||||
+28
-24
@@ -155,7 +155,7 @@
|
||||
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
|
||||
;BUILTIN_SSH_SERVER_USER =
|
||||
;;
|
||||
;; Domain name to be exposed in clone URL, defaults to the domain part of ROOT_URL
|
||||
;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL
|
||||
;SSH_DOMAIN =
|
||||
;;
|
||||
;; Port number to be exposed in clone URL.
|
||||
@@ -198,7 +198,7 @@
|
||||
;; For the built-in SSH server, choose the keypair to offer as the host key
|
||||
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
|
||||
;; relative paths are made absolute relative to the APP_DATA_PATH
|
||||
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gitea.ed25519, ssh/gitea.ecdsa, ssh/gogs.rsa
|
||||
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
|
||||
;;
|
||||
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
|
||||
;SSH_AUTHORIZED_KEYS_BACKUP = false
|
||||
@@ -237,7 +237,7 @@
|
||||
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
|
||||
;;
|
||||
;; Indicate whether to check minimum key size with corresponding type
|
||||
;MINIMUM_KEY_SIZE_CHECK = true
|
||||
;MINIMUM_KEY_SIZE_CHECK = false
|
||||
;;
|
||||
;; TLS Settings: Either ACME or manual
|
||||
;; (Other common TLS configuration are found before)
|
||||
@@ -459,7 +459,7 @@ INTERNAL_TOKEN =
|
||||
;LOGIN_REMEMBER_DAYS = 31
|
||||
;;
|
||||
;; Name of cookie used to store authentication information.
|
||||
;COOKIE_REMEMBER_NAME = gitea_remember
|
||||
;COOKIE_REMEMBER_NAME = gitea_incredible
|
||||
;;
|
||||
;; URL or path that Gitea should redirect users to *after* performing its own logout.
|
||||
;; Use this, if needed, when authentication is handled by a reverse proxy or SSO.
|
||||
@@ -782,6 +782,8 @@ LEVEL = Info
|
||||
;; Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled)
|
||||
;PULL_REQUEST_PUSH_MESSAGE = true
|
||||
;;
|
||||
;; (Go-Git only) Don't cache objects greater than this in memory. (Set to 0 to disable.)
|
||||
;LARGE_OBJECT_THRESHOLD = 1048576
|
||||
;; Set to true to forcibly set core.protectNTFS=false
|
||||
;DISABLE_CORE_PROTECT_NTFS=false
|
||||
;; Disable the usage of using partial clones for git.
|
||||
@@ -836,7 +838,7 @@ LEVEL = Info
|
||||
;EMAIL_DOMAIN_BLOCKLIST =
|
||||
;;
|
||||
;; Disallow registration, only allow admins to create accounts.
|
||||
;DISABLE_REGISTRATION = true
|
||||
;DISABLE_REGISTRATION = false
|
||||
;;
|
||||
;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false
|
||||
;ALLOW_ONLY_INTERNAL_REGISTRATION = false
|
||||
@@ -968,11 +970,12 @@ LEVEL = Info
|
||||
;; Value for the domain part of the user's email address in the git log if user
|
||||
;; has set KeepEmailPrivate to true. The user's email will be replaced with a
|
||||
;; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default
|
||||
;; value is "noreply." + the domain part of ROOT_URL
|
||||
;NO_REPLY_ADDRESS =
|
||||
;; value is "noreply." + DOMAIN, where DOMAIN resolves to the value from server.DOMAIN
|
||||
;; Note: do not use the <DOMAIN> notation below
|
||||
;NO_REPLY_ADDRESS = ; noreply.<DOMAIN>
|
||||
;;
|
||||
;; Show Registration button, defaults to true only if both DISABLE_REGISTRATION and ALLOW_ONLY_EXTERNAL_REGISTRATION are false
|
||||
;SHOW_REGISTRATION_BUTTON = false
|
||||
;; Show Registration button
|
||||
;SHOW_REGISTRATION_BUTTON = true
|
||||
;;
|
||||
;; Show milestones dashboard page - a view of all the user's milestones
|
||||
;SHOW_MILESTONES_DASHBOARD_PAGE = true
|
||||
@@ -1095,9 +1098,8 @@ LEVEL = Info
|
||||
;; Force ssh:// clone url instead of scp-style uri when default SSH port is used
|
||||
;USE_COMPAT_SSH_URI = false
|
||||
;;
|
||||
;; Scheme of the returned URL for the "go get" response.
|
||||
;; Default is "https" if DISABLE_HTTP_GIT=false or SSH is disabled, otherwise "ssh".
|
||||
;GO_GET_CLONE_URL_PROTOCOL =
|
||||
;; Value for the "go get" request returns the repository url as https or ssh, default is https
|
||||
;GO_GET_CLONE_URL_PROTOCOL = https
|
||||
;;
|
||||
;; Close issues as long as a commit on any branch marks it as fixed
|
||||
;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
|
||||
@@ -1670,13 +1672,13 @@ LEVEL = Info
|
||||
;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; General queue type, currently support: level, channel, redis, dummy
|
||||
;; default to level
|
||||
;TYPE = level
|
||||
;; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy
|
||||
;; default to persistable-channel
|
||||
;TYPE = persistable-channel
|
||||
;;
|
||||
;; data-dir for storing level queues, individual queues will default to `queues/common` meaning the queue is shared.
|
||||
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
|
||||
;; Relative paths will be made absolute against "APP_DATA_PATH"
|
||||
;DATADIR = queues/common
|
||||
;DATADIR = queues/
|
||||
;;
|
||||
;; Default queue length before a channel queue will block
|
||||
;LENGTH = 100000
|
||||
@@ -1684,7 +1686,7 @@ LEVEL = Info
|
||||
;; Batch size to send for batched queues
|
||||
;BATCH_LENGTH = 20
|
||||
;;
|
||||
;; When `TYPE` is `level`, this provides a directory for the underlying leveldb
|
||||
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb
|
||||
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
|
||||
;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR.
|
||||
;CONN_STR =
|
||||
@@ -1752,6 +1754,7 @@ LEVEL = Info
|
||||
;ENABLE_OPENID_SIGNIN = false
|
||||
;;
|
||||
;; Whether to allow registering via OpenID
|
||||
;; Do not include to rely on rhw DISABLE_REGISTRATION setting
|
||||
;;ENABLE_OPENID_SIGNUP = false
|
||||
;;
|
||||
;; Allowed URI patterns (POSIX regexp).
|
||||
@@ -1982,7 +1985,7 @@ LEVEL = Info
|
||||
;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory"
|
||||
;ADAPTER = memory
|
||||
;;
|
||||
;; For "memory" and "twoqueue", GC interval in seconds, default is 60
|
||||
;; For "memory" only, GC interval in seconds, default is 60
|
||||
;INTERVAL = 60
|
||||
;;
|
||||
;; For "redis" and "memcache", connection host address
|
||||
@@ -2013,17 +2016,19 @@ LEVEL = Info
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; Either "memory", "file", "redis" or "db", default is "file". "db" will reuse the configuration in [database]
|
||||
;PROVIDER = file
|
||||
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
|
||||
;; Default is "memory". "db" will reuse the configuration in [database]
|
||||
;PROVIDER = memory
|
||||
;;
|
||||
;; Provider config options
|
||||
;; memory: doesn't have any config yet
|
||||
;; file: session file path, e.g. `data/sessions`, relative paths will be made absolute against _`AppWorkPath`_.
|
||||
;; redis: default to [redis] CONN_STR
|
||||
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
|
||||
;PROVIDER_CONFIG =
|
||||
;;
|
||||
;; Session cookie name
|
||||
;COOKIE_NAME = gitea_session
|
||||
;COOKIE_NAME = i_like_gitea
|
||||
;;
|
||||
;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
|
||||
;COOKIE_SECURE =
|
||||
@@ -2049,8 +2054,7 @@ LEVEL = Info
|
||||
;; How Gitea deals with missing repository avatars
|
||||
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
|
||||
;REPOSITORY_AVATAR_FALLBACK = none
|
||||
;; Image URL for the "image" fallback, used as-is, defaults to Gitea's builtin repository avatar
|
||||
;REPOSITORY_AVATAR_FALLBACK_IMAGE =
|
||||
;REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png
|
||||
;;
|
||||
;; Max Width and Height of uploaded avatars.
|
||||
;; This is to limit the amount of RAM used when resizing the image.
|
||||
|
||||
@@ -25,6 +25,7 @@ Depending on requirements, the following build tags can be included.
|
||||
- `bindata`: Build a single monolithic binary, with all assets included. Required for distribution and production build.
|
||||
- `pam`: Enable support for PAM (Linux Pluggable Authentication Modules).
|
||||
Can be used to authenticate local users or extend authentication to methods available to PAM.
|
||||
- `gogit`: (EXPERIMENTAL) Use go-git variants of Git commands.
|
||||
|
||||
To include all assets, use the `bindata` tag:
|
||||
|
||||
@@ -32,6 +33,13 @@ To include all assets, use the `bindata` tag:
|
||||
TAGS="bindata" make build
|
||||
```
|
||||
|
||||
Tag `gogit` is used to try to resolve some Windows-specific performance problems, POSIX systems don't need it.
|
||||
You can build a Windows binary by:
|
||||
|
||||
```bash
|
||||
GOOS=windows TAGS="bindata gogit" make build
|
||||
```
|
||||
|
||||
## Changing default paths
|
||||
|
||||
Gitea will search for a number of things from the _`CustomPath`_.
|
||||
|
||||
+29
-49
@@ -17,7 +17,6 @@ import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts
|
||||
|
||||
const jsExts = ['js', 'mjs', 'cjs'] as const;
|
||||
const tsExts = ['ts', 'mts', 'cts'] as const;
|
||||
const vueExts = ['vue'] as const;
|
||||
|
||||
const restrictedGlobals = [
|
||||
{name: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'},
|
||||
@@ -37,7 +36,7 @@ export default defineConfig([
|
||||
'public/assets/js',
|
||||
]),
|
||||
{
|
||||
files: [`**/*.{${[...jsExts, ...tsExts, ...vueExts].join(',')}}`],
|
||||
files: [`**/*.{${[...jsExts, ...tsExts].join(',')}}`],
|
||||
ignores: ['dist/*'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
@@ -49,9 +48,7 @@ export default defineConfig([
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true,
|
||||
},
|
||||
parser: typescriptParser,
|
||||
project: true,
|
||||
extraFileExtensions: vueExts.map((ext) => `.${ext}`),
|
||||
},
|
||||
},
|
||||
linterOptions: {
|
||||
@@ -69,11 +66,8 @@ export default defineConfig([
|
||||
wc,
|
||||
},
|
||||
settings: {
|
||||
'import-x/extensions': [...jsExts, ...tsExts, ...vueExts].map((ext) => `.${ext}`),
|
||||
'import-x/parsers': {
|
||||
'@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`),
|
||||
'vue-eslint-parser': vueExts.map((ext) => `.${ext}`),
|
||||
},
|
||||
'import-x/extensions': [...jsExts, ...tsExts].map((ext) => `.${ext}`),
|
||||
'import-x/parsers': {'@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`)},
|
||||
'import-x/resolver': {'typescript': true},
|
||||
},
|
||||
rules: {
|
||||
@@ -239,7 +233,6 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-unsafe-assignment': [0],
|
||||
'@typescript-eslint/no-unsafe-call': [0],
|
||||
'@typescript-eslint/no-unsafe-declaration-merging': [2],
|
||||
'@typescript-eslint/no-unsafe-enum-assignment': [0],
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': [0],
|
||||
'@typescript-eslint/no-unsafe-function-type': [2],
|
||||
'@typescript-eslint/no-unsafe-member-access': [0],
|
||||
@@ -711,7 +704,6 @@ export default defineConfig([
|
||||
'unicorn/better-dom-traversing': [2],
|
||||
'unicorn/catch-error-name': [0],
|
||||
'unicorn/class-reference-in-static-methods': [2],
|
||||
'unicorn/comma-spacing': [0], // only applies to json language
|
||||
'unicorn/comment-content': [0],
|
||||
'unicorn/consistent-arrow-return-style': [0],
|
||||
'unicorn/consistent-assert': [0],
|
||||
@@ -742,10 +734,8 @@ export default defineConfig([
|
||||
'unicorn/filename-case': [0],
|
||||
'unicorn/id-match': [2],
|
||||
'unicorn/import-style': [0],
|
||||
'unicorn/indent': [0], // only applies to json and css languages
|
||||
'unicorn/isolated-functions': [2, {functions: []}],
|
||||
'unicorn/iteration-fallback-style': [2, 'fallback'],
|
||||
'unicorn/key-name-casing': [0], // only applies to json, yaml and toml languages
|
||||
'unicorn/logical-assignment-operators': [0],
|
||||
'unicorn/max-nested-calls': [0],
|
||||
'unicorn/name-replacements': [0],
|
||||
@@ -778,20 +768,21 @@ export default defineConfig([
|
||||
'unicorn/no-chained-comparison': [2],
|
||||
'unicorn/no-collection-bracket-access': [2],
|
||||
'unicorn/no-computed-property-existence-check': [0],
|
||||
'unicorn/no-conflicting-constraints': [2],
|
||||
'unicorn/no-confusing-array-splice': [2],
|
||||
'unicorn/no-confusing-array-with': [2],
|
||||
'unicorn/no-console-spaces': [0],
|
||||
'unicorn/no-constant-zero-expression': [2],
|
||||
'unicorn/no-declarations-before-early-exit': [0],
|
||||
'unicorn/no-deprecated-css-features': [0],
|
||||
'unicorn/no-document-cookie': [2],
|
||||
'unicorn/no-double-comparison': [2],
|
||||
'unicorn/no-duplicate-css-selectors': [0],
|
||||
'unicorn/no-duplicate-font-family-names': [0],
|
||||
'unicorn/no-duplicate-if-branches': [2],
|
||||
'unicorn/no-duplicate-logical-operands': [2],
|
||||
'unicorn/no-duplicate-loops': [0],
|
||||
'unicorn/no-duplicate-set-values': [2],
|
||||
'unicorn/no-empty-file': [2],
|
||||
'unicorn/no-empty-link-text': [0], // only applies to markdown language
|
||||
'unicorn/no-error-property-assignment': [2],
|
||||
'unicorn/no-exports-in-scripts': [2],
|
||||
'unicorn/no-for-each': [2],
|
||||
@@ -799,34 +790,21 @@ export default defineConfig([
|
||||
'unicorn/no-global-object-property-assignment': [0],
|
||||
'unicorn/no-immediate-mutation': [0],
|
||||
'unicorn/no-impossible-length-comparison': [2],
|
||||
'unicorn/no-incomplete-accessor-override': [2],
|
||||
'unicorn/no-incorrect-query-selector': [2],
|
||||
'unicorn/no-incorrect-template-string-interpolation': [0],
|
||||
'unicorn/no-ineffective-csp-directives': [2],
|
||||
'unicorn/no-instanceof-builtins': [2],
|
||||
'unicorn/no-invalid-argument-count': [2],
|
||||
'unicorn/no-invalid-boolean-attribute-value': [2],
|
||||
'unicorn/no-invalid-character-comparison': [2],
|
||||
'unicorn/no-invalid-dom-token': [2],
|
||||
'unicorn/no-invalid-fetch-options': [2],
|
||||
'unicorn/no-invalid-file-input-accept': [2],
|
||||
'unicorn/no-invalid-integrity': [2],
|
||||
'unicorn/no-invalid-intl-options': [2],
|
||||
'unicorn/no-invalid-property-descriptor': [2],
|
||||
'unicorn/no-invalid-media-features': [0],
|
||||
'unicorn/no-invalid-remove-event-listener': [2],
|
||||
'unicorn/no-invalid-response-options': [2],
|
||||
'unicorn/no-invalid-style-set-property': [2],
|
||||
'unicorn/no-invalid-temporal-arithmetic': [2],
|
||||
'unicorn/no-invalid-url-protocol-comparison': [2],
|
||||
'unicorn/no-invalid-well-known-symbol-methods': [2],
|
||||
'unicorn/no-javascript-url': [0], // only applies to markdown language
|
||||
'unicorn/no-keyword-prefix': [0],
|
||||
'unicorn/no-late-current-target-access': [2],
|
||||
'unicorn/no-late-event-control': [2],
|
||||
'unicorn/no-leading-empty-lines': [0], // handled by @stylistic/no-multiple-empty-lines
|
||||
'unicorn/no-lonely-if': [2],
|
||||
'unicorn/no-loop-iterable-mutation': [2],
|
||||
'unicorn/no-loss-of-precision': [0], // only applies to json, toml and css languages
|
||||
'unicorn/no-magic-array-flat-depth': [0],
|
||||
'unicorn/no-manually-wrapped-comments': [0], // too opinionated
|
||||
'unicorn/no-mismatched-map-key': [2],
|
||||
@@ -839,6 +817,7 @@ export default defineConfig([
|
||||
'unicorn/no-negated-condition': [0],
|
||||
'unicorn/no-negation-in-equality-check': [2],
|
||||
'unicorn/no-nested-ternary': [0],
|
||||
'unicorn/no-nesting-with-mixed-specificity': [0],
|
||||
'unicorn/no-new-array': [0],
|
||||
'unicorn/no-new-buffer': [2],
|
||||
'unicorn/no-non-function-verb-prefix': [0],
|
||||
@@ -847,9 +826,9 @@ export default defineConfig([
|
||||
'unicorn/no-object-as-default-parameter': [0],
|
||||
'unicorn/no-object-methods-with-collections': [2],
|
||||
'unicorn/no-optional-chaining-on-undeclared-variable': [2],
|
||||
'unicorn/no-prevent-default-in-passive-listener': [2],
|
||||
'unicorn/no-process-exit': [0],
|
||||
'unicorn/no-redundant-comparison': [2],
|
||||
'unicorn/no-redundant-nested-style-rules': [0],
|
||||
'unicorn/no-return-array-push': [2],
|
||||
'unicorn/no-selector-as-dom-name': [2],
|
||||
'unicorn/no-shorthand-property-overrides': [0], // only applies to css language
|
||||
@@ -865,6 +844,8 @@ export default defineConfig([
|
||||
'unicorn/no-typeof-undefined': [2],
|
||||
'unicorn/no-uncalled-method': [2],
|
||||
'unicorn/no-undeclared-class-members': [2],
|
||||
'unicorn/no-unknown-css-annotations': [0],
|
||||
'unicorn/no-unknown-pseudo-selectors': [0],
|
||||
'unicorn/no-unnecessary-array-flat-depth': [2],
|
||||
'unicorn/no-unnecessary-array-flat-map': [2],
|
||||
'unicorn/no-unnecessary-array-splice-count': [2],
|
||||
@@ -873,7 +854,6 @@ export default defineConfig([
|
||||
'unicorn/no-unnecessary-fetch-options': [0],
|
||||
'unicorn/no-unnecessary-global-this': [0],
|
||||
'unicorn/no-unnecessary-nested-ternary': [2],
|
||||
'unicorn/no-unnecessary-parameters': [0],
|
||||
'unicorn/no-unnecessary-polyfills': [2],
|
||||
'unicorn/no-unnecessary-slice-end': [2],
|
||||
'unicorn/no-unnecessary-splice': [2],
|
||||
@@ -885,15 +865,14 @@ export default defineConfig([
|
||||
'unicorn/no-unreadable-object-destructuring': [0],
|
||||
'unicorn/no-unsafe-buffer-conversion': [2],
|
||||
'unicorn/no-unsafe-dom-html': [0],
|
||||
'unicorn/no-unsafe-json-serialization': [2],
|
||||
'unicorn/no-unsafe-promise-all-settled-values': [2],
|
||||
'unicorn/no-unsafe-property-key': [0],
|
||||
'unicorn/no-unsafe-sqlite-interpolation': [2],
|
||||
'unicorn/no-unsafe-string-replacement': [2],
|
||||
'unicorn/no-unscoped-css-nesting-selector': [0],
|
||||
'unicorn/no-unused-builtin-method-return': [2],
|
||||
'unicorn/no-unused-iterator-helper': [2],
|
||||
'unicorn/no-unused-properties': [2],
|
||||
'unicorn/no-url-in-search-params': [2],
|
||||
'unicorn/no-useless-boolean-cast': [2],
|
||||
'unicorn/no-useless-coercion': [2],
|
||||
'unicorn/no-useless-collection-argument': [2],
|
||||
@@ -930,12 +909,12 @@ export default defineConfig([
|
||||
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
|
||||
'unicorn/prefer-array-flat': [2],
|
||||
'unicorn/prefer-array-flat-map': [2],
|
||||
'unicorn/prefer-array-from-async': [0], // Array.fromAsync requires ES2026
|
||||
'unicorn/prefer-array-from-async': [2],
|
||||
'unicorn/prefer-array-from-map': [2],
|
||||
'unicorn/prefer-array-from-range': [2],
|
||||
'unicorn/prefer-array-index-of': [2],
|
||||
'unicorn/prefer-array-iterable-methods': [2],
|
||||
'unicorn/prefer-array-last-methods': [0], // Array#findLast/findLastIndex requires ES2023
|
||||
'unicorn/prefer-array-last-methods': [2],
|
||||
'unicorn/prefer-array-slice': [2],
|
||||
'unicorn/prefer-array-some': [2],
|
||||
'unicorn/prefer-at': [0],
|
||||
@@ -952,7 +931,7 @@ export default defineConfig([
|
||||
'unicorn/prefer-date-now': [2],
|
||||
'unicorn/prefer-default-parameters': [0],
|
||||
'unicorn/prefer-direct-iteration': [2],
|
||||
'unicorn/prefer-dispose': [0], // `using` requires ES2027
|
||||
'unicorn/prefer-dispose': [2],
|
||||
'unicorn/prefer-dom-node-append': [2],
|
||||
'unicorn/prefer-dom-node-html-methods': [0],
|
||||
'unicorn/prefer-dom-node-remove': [2],
|
||||
@@ -961,14 +940,14 @@ export default defineConfig([
|
||||
'unicorn/prefer-early-return': [0],
|
||||
'unicorn/prefer-else-if': [2],
|
||||
'unicorn/prefer-error-is-error': [0],
|
||||
'unicorn/prefer-escaped-irregular-whitespace': [2],
|
||||
'unicorn/prefer-event-target': [2],
|
||||
'unicorn/prefer-explicit-viewport-units': [0], // only applies to css language
|
||||
'unicorn/prefer-export-from': [0],
|
||||
'unicorn/prefer-flat-math-min-max': [2],
|
||||
'unicorn/prefer-get-or-insert-computed': [0], // Map#getOrInsertComputed requires ES2026
|
||||
'unicorn/prefer-get-or-insert-computed': [2],
|
||||
'unicorn/prefer-global-number-constants': [2],
|
||||
'unicorn/prefer-global-this': [0],
|
||||
'unicorn/prefer-group-by': [0], // Object.groupBy/Map.groupBy requires ES2024
|
||||
'unicorn/prefer-group-by': [2],
|
||||
'unicorn/prefer-has-check': [2],
|
||||
'unicorn/prefer-hoisting-branch-code': [2],
|
||||
'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs
|
||||
@@ -979,12 +958,11 @@ export default defineConfig([
|
||||
'unicorn/prefer-iterable-in-constructor': [2],
|
||||
'unicorn/prefer-iterator-concat': [0], // too opinionated
|
||||
'unicorn/prefer-iterator-helpers': [0],
|
||||
'unicorn/prefer-iterator-to-array': [0], // Iterator#toArray requires ES2025
|
||||
'unicorn/prefer-iterator-to-array': [2],
|
||||
'unicorn/prefer-iterator-to-array-at-end': [2],
|
||||
'unicorn/prefer-iterator-zip': [0],
|
||||
'unicorn/prefer-json-import': [0],
|
||||
'unicorn/prefer-keyboard-event-key': [2],
|
||||
'unicorn/prefer-literal-ascii': [2],
|
||||
'unicorn/prefer-location-assign': [2],
|
||||
'unicorn/prefer-logical-operator-over-ternary': [0],
|
||||
'unicorn/prefer-map-from-entries': [0],
|
||||
@@ -992,6 +970,7 @@ export default defineConfig([
|
||||
'unicorn/prefer-math-constants': [2],
|
||||
'unicorn/prefer-math-min-max': [2],
|
||||
'unicorn/prefer-math-trunc': [2],
|
||||
'unicorn/prefer-media-feature-range-syntax': [0],
|
||||
'unicorn/prefer-minimal-ternary': [0],
|
||||
'unicorn/prefer-modern-dom-apis': [0],
|
||||
'unicorn/prefer-modern-math-apis': [2],
|
||||
@@ -1010,9 +989,8 @@ export default defineConfig([
|
||||
'unicorn/prefer-optional-catch-binding': [2],
|
||||
'unicorn/prefer-path2d': [2],
|
||||
'unicorn/prefer-private-class-fields': [0],
|
||||
'unicorn/prefer-promise-static-methods': [2],
|
||||
'unicorn/prefer-promise-try': [0], // Promise.try requires ES2025
|
||||
'unicorn/prefer-promise-with-resolvers': [0], // Promise.withResolvers requires ES2024
|
||||
'unicorn/prefer-promise-try': [2],
|
||||
'unicorn/prefer-promise-with-resolvers': [2],
|
||||
'unicorn/prefer-prototype-methods': [2],
|
||||
'unicorn/prefer-query-selector': [2],
|
||||
'unicorn/prefer-queue-microtask': [2],
|
||||
@@ -1025,7 +1003,6 @@ export default defineConfig([
|
||||
'unicorn/prefer-set-methods': [0],
|
||||
'unicorn/prefer-set-size': [2],
|
||||
'unicorn/prefer-short-arrow-method': [2],
|
||||
'unicorn/prefer-short-escape-sequences': [2],
|
||||
'unicorn/prefer-simple-condition-first': [0],
|
||||
'unicorn/prefer-simple-sort-comparator': [2],
|
||||
'unicorn/prefer-simplified-conditions': [2],
|
||||
@@ -1047,7 +1024,7 @@ export default defineConfig([
|
||||
'unicorn/prefer-structured-clone': [2],
|
||||
'unicorn/prefer-switch': [0],
|
||||
'unicorn/prefer-temporal': [0],
|
||||
'unicorn/prefer-temporal-conversion': [0], // Temporal requires ES2027
|
||||
'unicorn/prefer-temporal-conversion': [2],
|
||||
'unicorn/prefer-ternary': [0],
|
||||
'unicorn/prefer-then-catch': [2],
|
||||
'unicorn/prefer-toggle-attribute': [2],
|
||||
@@ -1073,7 +1050,6 @@ export default defineConfig([
|
||||
'unicorn/require-passive-events': [2],
|
||||
'unicorn/require-post-message-target-origin': [0],
|
||||
'unicorn/require-proxy-trap-boolean-return': [2],
|
||||
'unicorn/require-text-decoder-streaming': [2],
|
||||
'unicorn/single-line-block-comment-style': [0],
|
||||
'unicorn/string-content': [0],
|
||||
'unicorn/switch-case-braces': [0],
|
||||
@@ -1120,13 +1096,17 @@ export default defineConfig([
|
||||
},
|
||||
},
|
||||
{
|
||||
files: vueExts.map((ext) => `**/*.${ext}`),
|
||||
files: ['**/*.vue'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
vue.configs['flat/recommended'],
|
||||
vueScopedCss.configs.recommended,
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-redundant-type-constituents': [0], // types imported from .vue files resolve to any via typescript-eslint's *.vue shim
|
||||
'vue/attributes-order': [0],
|
||||
'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}],
|
||||
'vue/max-attributes-per-line': [0],
|
||||
|
||||
@@ -2,26 +2,29 @@ module gitea.dev
|
||||
|
||||
go 1.27
|
||||
|
||||
toolchain go1.27.2
|
||||
toolchain go1.27.1
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.21.0
|
||||
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981
|
||||
gitea.com/go-chi/cache v0.2.1
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
||||
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6
|
||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||
gitea.dev/actionslib v1.3.0
|
||||
gitea.dev/actionslib v1.2.1
|
||||
gitea.dev/sdk v1.2.0
|
||||
github.com/42wim/httpsig v1.2.4
|
||||
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
|
||||
github.com/Azure/go-ntlmssp v0.1.1
|
||||
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347
|
||||
github.com/ProtonMail/go-crypto v1.5.2
|
||||
github.com/ProtonMail/go-crypto v1.5.0
|
||||
github.com/PuerkitoBio/goquery v1.13.0
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0
|
||||
github.com/alecthomas/chroma/v2 v2.27.0
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/blevesearch/bleve/v2 v2.6.1
|
||||
github.com/bohde/codel v0.2.0
|
||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c
|
||||
github.com/buildkite/terminal-to-html/v3 v3.17.1
|
||||
github.com/caddyserver/certmagic v0.25.4
|
||||
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33
|
||||
@@ -32,6 +35,7 @@ require (
|
||||
github.com/dustin/go-humanize v1.1.0
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
github.com/felixge/fgprof v0.9.5
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/getkin/kin-openapi v0.149.0
|
||||
@@ -39,6 +43,8 @@ require (
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-co-op/gocron/v2 v2.22.0
|
||||
github.com/go-enry/go-enry/v2 v2.9.6
|
||||
github.com/go-git/go-billy/v5 v5.9.1
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.14
|
||||
github.com/go-redsync/redsync/v4 v4.18.0
|
||||
github.com/go-sql-driver/mysql v1.10.1
|
||||
@@ -56,7 +62,7 @@ require (
|
||||
github.com/huandu/xstrings v1.6.1
|
||||
github.com/jhillyerd/enmime/v2 v2.5.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.20.1
|
||||
github.com/klauspost/compress v1.20.0
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/markbates/goth v1.82.0
|
||||
github.com/mattn/go-isatty v0.0.24
|
||||
@@ -87,7 +93,7 @@ require (
|
||||
github.com/yohcop/openid-go v1.0.1
|
||||
github.com/yuin/goldmark v1.8.6
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.15.0
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/image v0.46.0
|
||||
@@ -100,7 +106,7 @@ require (
|
||||
google.golang.org/grpc v1.84.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
gopkg.in/ini.v1 v1.67.3
|
||||
modernc.org/sqlite v1.60.1
|
||||
modernc.org/sqlite v1.59.0
|
||||
mvdan.cc/xurls/v2 v2.6.0
|
||||
xorm.io/builder v0.3.13
|
||||
xorm.io/xorm v1.4.3
|
||||
@@ -108,8 +114,10 @@ require (
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/DataDog/zstd v1.5.7 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.18.2 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
@@ -135,18 +143,23 @@ require (
|
||||
github.com/blevesearch/zapx/v15 v15.4.3 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.3.4 // indirect
|
||||
github.com/blevesearch/zapx/v17 v17.2.3 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.4 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c // indirect
|
||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/couchbase/go-couchbase v0.1.1 // indirect
|
||||
github.com/couchbase/gomemcached v0.3.4 // indirect
|
||||
github.com/couchbase/goutils v0.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
@@ -155,6 +168,7 @@ require (
|
||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
|
||||
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.23.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
@@ -162,7 +176,9 @@ require (
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@@ -172,8 +188,10 @@ require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/inbucket/html2text v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
@@ -201,9 +219,9 @@ require (
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.34.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.27 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
@@ -214,12 +232,15 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stangelandcl/ppmd v0.1.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/unknwon/com v1.0.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
@@ -230,11 +251,11 @@ require (
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.50.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||
modernc.org/libc v1.77.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
modernc.org/libc v1.75.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,16 +4,26 @@ code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
connectrpc.com/connect v1.21.0 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs=
|
||||
connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981 h1:LmdlwGbzgZFZA3bK3R1q8QrNabaSz3KpnOJBLmzhN6E=
|
||||
gitea.com/go-chi/binding v0.0.0-20260819122636-082915a69981/go.mod h1:q1SSPpkC9A0gfNnoqqZ3My6kEHIpKN6QsTI+Zx73B/o=
|
||||
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
||||
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
|
||||
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-20260708011333-ebced8a7a2d6 h1:YWzVGeC/8SZThrJS48ZmQYLkzssdeABxHPhbdnxPDIU=
|
||||
gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0=
|
||||
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=
|
||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
gitea.dev/actionslib v1.3.0 h1:xZRoTL1+sK/PaglT9uXkZJ9g0nk1WLMEH4FUXdXpKKw=
|
||||
gitea.dev/actionslib v1.3.0/go.mod h1:svCefEsavx4jmn8vJ4wTG7Q0KBcGfrFmGUjrhxuNUAQ=
|
||||
gitea.dev/actionslib v1.2.1 h1:GL//K/0zIZV6h1OOksv1awvFVg0rg7fug2xWcG7mkG0=
|
||||
gitea.dev/actionslib v1.2.1/go.mod h1:1+gqOKGSEPn2IFHgY8S3GC5Ld+Hn8jF3OxiFHQ/fpx4=
|
||||
gitea.dev/sdk v1.2.0 h1:avRtJl/nKCGispgSalo9czoZM9Rto1awnE0caNAoXGo=
|
||||
gitea.dev/sdk v1.2.0/go.mod h1:rfh5oNdIK24cbCREwIn1tqWKQW+IICXFGWJyebuOAOE=
|
||||
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
|
||||
@@ -36,10 +46,13 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI=
|
||||
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 h1:3JhDl+JysaO8nhNU1XMaw35VSGjV4IEQAefaG4Lyok4=
|
||||
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347/go.mod h1:2ErI0aycD43Ufr6CFK5lT/NrHGmoZuVbn1nlPThw69o=
|
||||
github.com/ProtonMail/go-crypto v1.5.2 h1:cucYnvqcY7UOXVD//mSyjeaPY0SSN3v5cDkYPxumINk=
|
||||
github.com/ProtonMail/go-crypto v1.5.2/go.mod h1:/RaSu30DaKO4RY+XdV/ACcCcZkGr7AhUIduq5sjzzCo=
|
||||
github.com/ProtonMail/go-crypto v1.5.0 h1:sKmuvjOgsnrtpMvZ+84MCnTJCpjxZ1qCFn076lz1yT0=
|
||||
github.com/ProtonMail/go-crypto v1.5.0/go.mod h1:/RaSu30DaKO4RY+XdV/ACcCcZkGr7AhUIduq5sjzzCo=
|
||||
github.com/PuerkitoBio/goquery v1.13.0 h1:mqHbjD7Jmnul4DTR24LKTjo1uUmHUh072kteGV+xpFM=
|
||||
github.com/PuerkitoBio/goquery v1.13.0/go.mod h1:Hip5mdBL8K2wEGKJdr27sRaNwIdDajmCwB/ExUPwW+g=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.18.2 h1:oPq3Cgx//iDuJQVp6xSInAKW34J9CEwE5GmLI2z+Eic=
|
||||
@@ -62,6 +75,10 @@ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eT
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
|
||||
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -108,8 +125,8 @@ github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaT
|
||||
github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
|
||||
github.com/blevesearch/zapx/v17 v17.2.3 h1:UYYJPAt5b2tVxldx5h0jmv23RMsg8/UZKFVya7v92po=
|
||||
github.com/blevesearch/zapx/v17 v17.2.3/go.mod h1:r7mb4QWbDQSkbAnOjCb9iCfkcrzajB4yBdJpuBIo/fE=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2 h1:eF7W7HWKg3z9NrWV9pTLnNeoXaqq3Tq9DNKXVMfoCnw=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
@@ -156,8 +173,17 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
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.3.4 h1:VGdrZUJbt5lLyI/MXnyVCZKHKYXg/vaud08lJIAeZps=
|
||||
github.com/couchbase/gomemcached v0.3.4/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68=
|
||||
github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
|
||||
github.com/couchbase/goutils v0.3.0 h1:rsv72B6BDjW9jmwlfiDUrdu3EpNvPuo5WLULHzQ0DLE=
|
||||
github.com/couchbase/goutils v0.3.0/go.mod h1:7Gm+D3vXfV4HS+hQWvKfy6e6ILCptGXNqBKvQXhplhk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -179,6 +205,8 @@ github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2
|
||||
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5 h1:MTcuJQkIFRLfNn9FfAvdO0p3FhNyaCf+IGTwy0TxX6E=
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5/go.mod h1:SizrS3EM1vFF0v/JZlJ5LfK6tXhwln2823YZwNh/vBE=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
||||
@@ -186,6 +214,8 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTe
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||
@@ -200,6 +230,8 @@ github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isd
|
||||
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
|
||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
|
||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
@@ -213,6 +245,14 @@ github.com/go-enry/go-enry/v2 v2.9.6 h1:np63eOtMV56zfYDHnFVgpEVOk8fr2kmylcMnAZUD
|
||||
github.com/go-enry/go-enry/v2 v2.9.6/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-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA=
|
||||
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
|
||||
@@ -257,6 +297,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
@@ -272,6 +314,9 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@@ -298,6 +343,8 @@ github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe h1:QAinXoAFJdGQYztXn3
|
||||
github.com/google/pprof v0.0.0-20260906184651-6331bc6350fe/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
@@ -334,6 +381,8 @@ github.com/huandu/xstrings v1.6.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs=
|
||||
github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
@@ -353,11 +402,16 @@ github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7X
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
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.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
@@ -472,6 +526,8 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
||||
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -517,8 +573,16 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -542,6 +606,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
@@ -554,6 +619,8 @@ github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77ro
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=
|
||||
github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
|
||||
github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
|
||||
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
|
||||
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
|
||||
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
|
||||
github.com/urfave/cli/v3 v3.13.0 h1:Dr6jqMfIyyFsRVn7Nz5mqLsMY+ZMpfh3a0aMs+umPVY=
|
||||
@@ -562,6 +629,8 @@ github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pc
|
||||
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
@@ -583,8 +652,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.15.0 h1:67OL6f2VrQJ4sBYBLgZPpP4XEbnA5qfCBKbbn46GBoU=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.15.0/go.mod h1:k7uYxRoIeuSqyWZSJfAPZlf0L32OH0lUBImAl4gWUFc=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0 h1:CTUXvcL6OrGjYNafbws5C1lJwlXrxGJHWQGMF+hCdz0=
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0/go.mod h1:k7uYxRoIeuSqyWZSJfAPZlf0L32OH0lUBImAl4gWUFc=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
@@ -612,6 +681,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
@@ -620,6 +694,10 @@ golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -628,8 +706,14 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
@@ -639,6 +723,11 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -647,22 +736,37 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -670,6 +774,11 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
@@ -678,8 +787,11 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -705,6 +817,8 @@ gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
|
||||
gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -714,10 +828,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg=
|
||||
modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE=
|
||||
modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA=
|
||||
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
|
||||
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w=
|
||||
modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
@@ -726,8 +840,8 @@ modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
|
||||
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs=
|
||||
modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0=
|
||||
modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew=
|
||||
modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||
@@ -736,8 +850,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
|
||||
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
|
||||
modernc.org/sqlite v1.59.0 h1:X1es1GpqBlS/5T+vbM4HLUdaa8OtQx468DF2vrx+38A=
|
||||
modernc.org/sqlite v1.59.0/go.mod h1:+paeT2A3iPRHkQDwG7oA6Tk0zQd5woMEI8q7orfry8k=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -431,7 +431,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(354, "Add Actions job queue indexes", v28.AddActionQueueIndexes),
|
||||
newMigration(355, "Add AutoMerge merged_commit_id column", v28.AddAutoMergeMergedCommitID),
|
||||
// Gitea 28.0.0 ends at migration ID number 355 (database version 356)
|
||||
|
||||
// HERE: 2 migrations from 29 are added since they don't change database structure
|
||||
newMigration(356, "Add index on action_run commit_sha", v29.AddActionRunCommitSHAIndex),
|
||||
newMigration(357, "Normalize legacy team authorize values", v29.NormalizeLegacyTeamAuthorize),
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
// NormalizeLegacyTeamAuthorize sets leftover read/write authorize values to none.
|
||||
// https://github.com/go-gitea/gitea/pull/34128 made non-admin teams use team_unit (authorize=none).
|
||||
// Any positive authorize now means blanket access on every unit; migrating legacy read/write
|
||||
// authorize>=write now means blanket access on every unit; migrating legacy read/write
|
||||
// to none preserves their existing team_unit-scoped access.
|
||||
func NormalizeLegacyTeamAuthorize(_ context.Context, x base.EngineMigration) error {
|
||||
// AccessModeNone=0, AccessModeRead=1, AccessModeWrite=2, AccessModeAdmin=3
|
||||
|
||||
@@ -200,7 +200,7 @@ func (r *ActionRunner) GenerateAndFillToken() {
|
||||
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
|
||||
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
|
||||
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
|
||||
return len(jobRunsOn) > 0 && !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) })
|
||||
return !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) })
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -86,5 +86,4 @@ func TestCanMatchLabelsCaseInsensitive(t *testing.T) {
|
||||
runner := &ActionRunner{AgentLabels: []string{"self-hosted", "Linux", "X64"}}
|
||||
assert.True(t, runner.CanMatchLabels([]string{"SELF-HOSTED", "linux"}))
|
||||
assert.False(t, runner.CanMatchLabels([]string{"linux", "arm64"}))
|
||||
assert.False(t, runner.CanMatchLabels(nil))
|
||||
}
|
||||
|
||||
@@ -83,17 +83,23 @@ func GetActivityStats(ctx context.Context, repo *repo_model.Repository, timeFrom
|
||||
|
||||
// GetActivityStatsTopAuthors returns top author stats for git commits for all branches
|
||||
func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository, timeFrom time.Time, count int) ([]*ActivityAuthorData, error) {
|
||||
authors, err := git.GetCodeActivityAuthors(ctx, repo, timeFrom)
|
||||
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("OpenRepository: %w", err)
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
code, err := gitRepo.GetCodeActivityStats(ctx, timeFrom, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FillFromGit: %w", err)
|
||||
}
|
||||
if authors == nil {
|
||||
if code.Authors == nil {
|
||||
return nil, nil
|
||||
}
|
||||
users := make(map[int64]*ActivityAuthorData)
|
||||
var unknownUserID int64
|
||||
unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx)
|
||||
for _, v := range authors {
|
||||
for _, v := range code.Authors {
|
||||
if len(v.Email) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/consts"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
@@ -136,7 +135,7 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
||||
defer f.Close()
|
||||
|
||||
// Note: chmod command does not support in Windows.
|
||||
if !consts.IsWindows {
|
||||
if !setting.IsWindows {
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -84,7 +84,6 @@ var (
|
||||
UserTwoFactorRegenerate = define("user:twofactor:regenerate", "Regenerated two-factor authentication secret for user {scope}.")
|
||||
UserTwoFactorDisable = define("user:twofactor:disable", "Disabled two-factor authentication for user {scope}.")
|
||||
UserWebAuthAdd = define("user:webauth:add", "Added WebAuthn key {credential} for user {scope}.")
|
||||
UserWebAuthRename = define("user:webauth:rename", "Renamed WebAuthn key {previous_credential} of user {scope} to {credential}.")
|
||||
UserWebAuthRemove = define("user:webauth:remove", "Removed WebAuthn key {credential} from user {scope}.")
|
||||
UserExternalLoginAdd = define("user:externallogin:add", "Added external login {external_id} for user {scope} using provider {provider}.")
|
||||
UserExternalLoginRemove = define("user:externallogin:remove", "Removed external login from authentication source {auth_source_id} for user {scope}.")
|
||||
|
||||
+72
-29
@@ -5,6 +5,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/timeutil"
|
||||
@@ -12,46 +13,49 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Session represents a session compatible for go-chi session
|
||||
type Session struct {
|
||||
Key string `xorm:"pk CHAR(16)"` // the limit is from legacy go-chi/session
|
||||
Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb
|
||||
LastAccessTime timeutil.TimeStamp `xorm:"expiry"` // last access time, the field name is from legacy go-chi/session, we don't want to change it at the moment
|
||||
Key string `xorm:"pk CHAR(16)"` // has to be Key to match with go-chi/session
|
||||
Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb - this may need to be increased
|
||||
Expiry timeutil.TimeStamp // has to be Expiry to match with go-chi/session
|
||||
}
|
||||
|
||||
const DbSessionLastAccessTime = "expiry" // maybe we can make a deeper clean up in the future, just keep this PR focused
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Session))
|
||||
}
|
||||
|
||||
// UpdateSession stores the data of the session with provided id, creating the session only if create is set
|
||||
func UpdateSession(ctx context.Context, key string, data []byte, create bool) error {
|
||||
session := &Session{Key: key, Data: data, LastAccessTime: timeutil.TimeStampNow()}
|
||||
update := func() (int64, error) {
|
||||
return db.GetEngine(ctx).ID(key).Cols("data", DbSessionLastAccessTime).Update(session)
|
||||
}
|
||||
if updated, err := update(); err != nil || updated > 0 || !create {
|
||||
return err
|
||||
}
|
||||
insertErr := db.Insert(ctx, session)
|
||||
if insertErr == nil {
|
||||
return nil
|
||||
}
|
||||
// the row exists if a concurrent request inserted it, or if MySQL reported an unchanged row as not updated
|
||||
if exist, err := db.Exist[Session](ctx, builder.Eq{"`key`": key}); err != nil || !exist {
|
||||
return insertErr
|
||||
}
|
||||
_, err := update()
|
||||
// UpdateSession updates the session with provided id
|
||||
func UpdateSession(ctx context.Context, key string, data []byte) error {
|
||||
_, err := db.GetEngine(ctx).ID(key).Update(&Session{
|
||||
Data: data,
|
||||
Expiry: timeutil.TimeStampNow(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateSessionLastAccessTime(ctx context.Context, key string) error {
|
||||
_, err := db.GetEngine(ctx).ID(key).Cols(DbSessionLastAccessTime).Update(&Session{LastAccessTime: timeutil.TimeStampNow()})
|
||||
return err
|
||||
// ReadSession reads the data for the provided session
|
||||
func ReadSession(ctx context.Context, key string) (*Session, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
|
||||
session, exist, err := db.Get[Session](ctx, builder.Eq{"`key`": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
session = &Session{
|
||||
Key: key,
|
||||
Expiry: timeutil.TimeStampNow(),
|
||||
}
|
||||
if err := db.Insert(ctx, session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return session, nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetSession(ctx context.Context, key string) (*Session, bool, error) {
|
||||
return db.Get[Session](ctx, builder.Eq{"`key`": key})
|
||||
// ExistSession checks if a session exists
|
||||
func ExistSession(ctx context.Context, key string) (bool, error) {
|
||||
return db.Exist[Session](ctx, builder.Eq{"`key`": key})
|
||||
}
|
||||
|
||||
// DestroySession destroys a session
|
||||
@@ -62,8 +66,47 @@ func DestroySession(ctx context.Context, key string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// RegenerateSession regenerates a session from the old id
|
||||
func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
|
||||
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": newKey}); err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
return nil, fmt.Errorf("session Key: %s already exists", newKey)
|
||||
}
|
||||
|
||||
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": oldKey}); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
if err := db.Insert(ctx, &Session{
|
||||
Key: oldKey,
|
||||
Expiry: timeutil.TimeStampNow(),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.Exec(ctx, "UPDATE `session` SET `key` = ? WHERE `key`=?", newKey, oldKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, _, err := db.Get[Session](ctx, builder.Eq{"`key`": newKey})
|
||||
if err != nil {
|
||||
// is not exist, it should be impossible
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
})
|
||||
}
|
||||
|
||||
// CountSessions returns the number of sessions
|
||||
func CountSessions(ctx context.Context) (int64, error) {
|
||||
return db.GetEngine(ctx).Count(&Session{})
|
||||
}
|
||||
|
||||
// CleanupSessions cleans up expired sessions
|
||||
func CleanupSessions(ctx context.Context, maxLifetime int64) error {
|
||||
_, err := db.GetEngine(ctx).Where(DbSessionLastAccessTime+" <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
|
||||
_, err := db.GetEngine(ctx).Where("expiry <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
|
||||
return err
|
||||
}
|
||||
|
||||
+4
-22
@@ -150,9 +150,9 @@ func GetWebAuthnCredentialByName(ctx context.Context, uid int64, name string) (*
|
||||
}
|
||||
|
||||
// GetWebAuthnCredentialByID returns WebAuthn credential by id
|
||||
func GetWebAuthnCredentialByID(ctx context.Context, uid, id int64) (*WebAuthnCredential, error) {
|
||||
func GetWebAuthnCredentialByID(ctx context.Context, id int64) (*WebAuthnCredential, error) {
|
||||
cred := new(WebAuthnCredential)
|
||||
if found, err := db.GetEngine(ctx).Where("user_id = ?", uid).ID(id).Get(cred); err != nil {
|
||||
if found, err := db.GetEngine(ctx).ID(id).Get(cred); err != nil {
|
||||
return nil, err
|
||||
} else if !found {
|
||||
return nil, ErrWebAuthnCredentialNotExist{ID: id}
|
||||
@@ -195,26 +195,8 @@ func CreateCredential(ctx context.Context, userID int64, name string, cred *weba
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// RenameCredential renames the user's WebAuthnCredential, names are unique per user regardless of letter case
|
||||
func RenameCredential(ctx context.Context, uid, id int64, name string) (bool, error) {
|
||||
used, err := db.GetEngine(ctx).Where("user_id = ? AND lower_name = ? AND id != ?", uid, strings.ToLower(name), id).Exist(&WebAuthnCredential{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if used {
|
||||
return false, util.ErrorWrapTranslatable(
|
||||
util.NewAlreadyExistErrorf("WebAuthn credential name already exists [uid: %d, name: %s]", uid, name),
|
||||
"settings.webauthn_nickname_been_used",
|
||||
)
|
||||
}
|
||||
updated, err := db.GetEngine(ctx).ID(id).Where("user_id=? AND `name`<>?", uid, name).Cols("name", "lower_name").Update(&WebAuthnCredential{
|
||||
Name: name,
|
||||
LowerName: strings.ToLower(name),
|
||||
})
|
||||
return updated > 0, err
|
||||
}
|
||||
|
||||
// DeleteCredential will delete WebAuthnCredential
|
||||
func DeleteCredential(ctx context.Context, uid, id int64) (bool, error) {
|
||||
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", uid).Delete(&WebAuthnCredential{})
|
||||
func DeleteCredential(ctx context.Context, id, userID int64) (bool, error) {
|
||||
had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{})
|
||||
return had > 0, err
|
||||
}
|
||||
|
||||
@@ -16,15 +16,11 @@ import (
|
||||
func TestGetWebAuthnCredentialByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
res, err := auth_model.GetWebAuthnCredentialByID(t.Context(), 32, 1)
|
||||
res, err := auth_model.GetWebAuthnCredentialByID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "WebAuthn credential", res.Name)
|
||||
|
||||
_, err = auth_model.GetWebAuthnCredentialByID(t.Context(), 99999, 1)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, auth_model.IsErrWebAuthnCredentialNotExist(err))
|
||||
|
||||
_, err = auth_model.GetWebAuthnCredentialByID(t.Context(), 32, 99999)
|
||||
_, err = auth_model.GetWebAuthnCredentialByID(t.Context(), 342432)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, auth_model.IsErrWebAuthnCredentialNotExist(err))
|
||||
}
|
||||
|
||||
@@ -433,13 +433,12 @@ func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listO
|
||||
case "reversealphabetically":
|
||||
sess.Desc("name")
|
||||
case "leastissues":
|
||||
sess.OrderBy("num_issues - num_closed_issues ASC")
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.OrderBy("num_issues - num_closed_issues DESC")
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("name")
|
||||
}
|
||||
sess.Asc("id")
|
||||
|
||||
if listOptions.Page > 0 {
|
||||
db.SetSessionPagination(sess, &listOptions)
|
||||
@@ -509,13 +508,12 @@ func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOpt
|
||||
case "reversealphabetically":
|
||||
sess.Desc("name")
|
||||
case "leastissues":
|
||||
sess.OrderBy("num_issues - num_closed_issues ASC")
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.OrderBy("num_issues - num_closed_issues DESC")
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("name")
|
||||
}
|
||||
sess.Asc("id")
|
||||
|
||||
if listOptions.Page > 0 {
|
||||
db.SetSessionPagination(sess, &listOptions)
|
||||
|
||||
@@ -191,8 +191,6 @@ func TestGetLabelsInRepoByIDs(t *testing.T) {
|
||||
|
||||
func TestGetLabelsByRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
_, err := db.GetEngine(t.Context()).ID(2).Cols("num_issues", "num_closed_issues").Update(&issues_model.Label{NumIssues: 3, NumClosedIssues: 3})
|
||||
assert.NoError(t, err)
|
||||
testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) {
|
||||
labels, err := issues_model.GetLabelsByRepoID(t.Context(), repoID, sortType, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -260,8 +258,6 @@ func TestGetLabelsInOrgByIDs(t *testing.T) {
|
||||
|
||||
func TestGetLabelsByOrgID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
_, err := db.GetEngine(t.Context()).ID(3).Cols("num_issues", "num_closed_issues").Update(&issues_model.Label{NumIssues: 3, NumClosedIssues: 3})
|
||||
assert.NoError(t, err)
|
||||
testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) {
|
||||
labels, err := issues_model.GetLabelsByOrgID(t.Context(), orgID, sortType, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -275,7 +271,7 @@ func TestGetLabelsByOrgID(t *testing.T) {
|
||||
testSuccess(3, "reversealphabetically", []int64{4, 3})
|
||||
testSuccess(3, "default", []int64{3, 4})
|
||||
|
||||
_, err = issues_model.GetLabelsByOrgID(t.Context(), 0, "leastissues", db.ListOptions{})
|
||||
_, err := issues_model.GetLabelsByOrgID(t.Context(), 0, "leastissues", db.ListOptions{})
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelsByOrgID(t.Context(), -1, "leastissues", db.ListOptions{})
|
||||
|
||||
@@ -50,7 +50,7 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool {
|
||||
c.gitRepo, c.gitRepoCloser = r, closer
|
||||
}
|
||||
|
||||
exist = c.gitRepo.IsReferenceExist(c.ctx, commitID)
|
||||
exist = c.gitRepo.IsReferenceExist(c.ctx, commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition.
|
||||
c.commitCache[commitID] = exist
|
||||
return exist
|
||||
}
|
||||
|
||||
@@ -78,13 +78,13 @@ func (m *PushMirror) GetRemoteName() string {
|
||||
return m.RemoteName
|
||||
}
|
||||
|
||||
// UpdatePushMirrorSyncStatus updates the sync status (last update time and last error) of the push-mirror
|
||||
func UpdatePushMirrorSyncStatus(ctx context.Context, m *PushMirror) error {
|
||||
_, err := db.GetEngine(ctx).ID(m.ID).Cols("last_update", "last_error").Update(m)
|
||||
// UpdatePushMirror updates the push-mirror
|
||||
func UpdatePushMirror(ctx context.Context, m *PushMirror) error {
|
||||
_, err := db.GetEngine(ctx).ID(m.ID).AllCols().Update(m)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdatePushMirrorInterval updates the sync interval of the push-mirror
|
||||
// UpdatePushMirrorInterval updates the push-mirror
|
||||
func UpdatePushMirrorInterval(ctx context.Context, m *PushMirror) error {
|
||||
_, err := db.GetEngine(ctx).ID(m.ID).Cols("interval").Update(m)
|
||||
return err
|
||||
|
||||
+1
-10
@@ -616,15 +616,6 @@ func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string {
|
||||
|
||||
// ComposeSSHCloneURL returns SSH clone URL based on the given owner and repository name.
|
||||
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
|
||||
return composeSSHCloneURL(doer, ownerName, repoName, setting.Repository.UseCompatSSHURI)
|
||||
}
|
||||
|
||||
// ComposeSSHCloneURI is like ComposeSSHCloneURL but always returns the "ssh://" form, because "go get" rejects scp-style addresses
|
||||
func ComposeSSHCloneURI(doer *user_model.User, ownerName, repoName string) string {
|
||||
return composeSSHCloneURL(doer, ownerName, repoName, true)
|
||||
}
|
||||
|
||||
func composeSSHCloneURL(doer *user_model.User, ownerName, repoName string, useURI bool) string {
|
||||
sshUser := setting.SSH.User
|
||||
sshDomain := setting.SSH.Domain
|
||||
|
||||
@@ -651,7 +642,7 @@ func composeSSHCloneURL(doer *user_model.User, ownerName, repoName string, useUR
|
||||
if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
|
||||
sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
|
||||
}
|
||||
if useURI {
|
||||
if setting.Repository.UseCompatSSHURI {
|
||||
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
}
|
||||
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
|
||||
@@ -185,8 +185,6 @@ func TestComposeSSHCloneURL(t *testing.T) {
|
||||
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
||||
setting.Repository.UseCompatSSHURI = true
|
||||
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
||||
setting.Repository.UseCompatSSHURI = false
|
||||
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURI(nil, "user", "repo"))
|
||||
// test SSH_DOMAIN while use non-standard SSH port
|
||||
setting.SSH.Port = 123
|
||||
setting.Repository.UseCompatSSHURI = false
|
||||
|
||||
@@ -60,6 +60,7 @@ func SyncDirs(srcPath, destPath string) error {
|
||||
}
|
||||
|
||||
// the keep file is used to keep the directory in a git repository, it doesn't need to be synced
|
||||
// and go-git doesn't work with the ".keep" file (it would report errors like "ref is empty")
|
||||
const keepFile = ".keep"
|
||||
|
||||
// find and delete all untracked files
|
||||
|
||||
@@ -151,28 +151,16 @@ func TestListEmails(t *testing.T) {
|
||||
|
||||
func TestEmailAddressValidate(t *testing.T) {
|
||||
cases := map[string]bool{
|
||||
"": false,
|
||||
"@a": false,
|
||||
|
||||
// "_" shouldn't appear in domain but can appear in hostname, since we can't stop site admins from doing so, just accept it
|
||||
"root@local_host": true,
|
||||
"root@localhost": true,
|
||||
"root@LOCALHOST": true,
|
||||
|
||||
"user@[192.168.1.2]": true,
|
||||
"user@[IPv6:FFff::1]": true,
|
||||
|
||||
"abc@gmail.com": true,
|
||||
"abc@gmail.com.": false,
|
||||
"abc@gmail.com-": false,
|
||||
"abc@gmail.com\n": false,
|
||||
"abc@gmail..com": false,
|
||||
"abc@gmail com": false,
|
||||
"abc@gmail*com": false,
|
||||
|
||||
"": false,
|
||||
"root@localhost": true,
|
||||
"user@[192.168.1.2]": true,
|
||||
"@a": false,
|
||||
"abc@gmail.com": true,
|
||||
"abc@gmail.com\n": false,
|
||||
"Foo <foo@bar.com>": false,
|
||||
"abc@gmail.com (x)": false,
|
||||
"jürgen@example.com": false, // utf8 address is not supported yet
|
||||
"jürgen@example.com": false,
|
||||
"a@foo_bar.com": false,
|
||||
}
|
||||
for tc, isValid := range cases {
|
||||
t.Run(tc, func(t *testing.T) {
|
||||
|
||||
@@ -40,8 +40,8 @@ type SearchUserOptions struct {
|
||||
Keyword string
|
||||
Types []UserType
|
||||
UID int64
|
||||
LoginName string // this option should be used only for admin user
|
||||
SourceID optional.Option[int64] // this option should be used only for admin user, Some(0) means local users
|
||||
LoginName string // this option should be used only for admin user
|
||||
SourceID int64 // this option should be used only for admin user
|
||||
OrderBy db.SearchOrderBy
|
||||
Visible []structs.VisibleType
|
||||
Actor *User // The user doing the search
|
||||
@@ -106,8 +106,8 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) db.Session
|
||||
cond = cond.And(builder.Eq{"id": opts.UID})
|
||||
}
|
||||
|
||||
if opts.SourceID.Has() {
|
||||
cond = cond.And(builder.Eq{"login_source": opts.SourceID.Value()})
|
||||
if opts.SourceID > 0 {
|
||||
cond = cond.And(builder.Eq{"login_source": opts.SourceID})
|
||||
}
|
||||
if opts.LoginName != "" {
|
||||
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/actionslib/pkg/expreval"
|
||||
@@ -127,7 +128,7 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
|
||||
}
|
||||
}
|
||||
// Keep accepting empty exclude mappings for workflow compatibility, although GitHub rejects them.
|
||||
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).MatrixCombinations()
|
||||
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).GetMatrixes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getMatrixes: %w", err)
|
||||
}
|
||||
@@ -168,7 +169,7 @@ func ExpandMatrixWithNeeds(jobID string, job *Job, gitCtx *model.GithubContext,
|
||||
if err := job.Strategy.resolve(expreval.New(NewInterpeter(jobID, nil, nil, gitCtx, results, vars, inputs).Evaluate)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).MatrixCombinations()
|
||||
matrixes, err := (&model.Job{Strategy: job.Strategy.actStrategy()}).GetMatrixes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getMatrixes: %w", err)
|
||||
}
|
||||
@@ -223,24 +224,34 @@ func replaceScalars(node *yaml.Node, replace func(string) string) {
|
||||
|
||||
// buildMatrixCombos builds one Job per matrix combination from src, baking the combination into the
|
||||
// strategy and interpolating the name, runs-on and continue-on-error with it.
|
||||
func buildMatrixCombos(jobID string, src *Job, matrixes []model.MatrixCombination, gitCtx *model.GithubContext, results map[string]*JobResult, vars map[string]string, inputs map[string]any) ([]*Job, error) {
|
||||
func buildMatrixCombos(jobID string, src *Job, matrixes []map[string]any, gitCtx *model.GithubContext, results map[string]*JobResult, vars map[string]string, inputs map[string]any) ([]*Job, error) {
|
||||
srcRunsOn := model.RunsOnFromNode(src.RawRunsOn)
|
||||
order, names := make([]int, len(matrixes)), make([]string, len(matrixes))
|
||||
for index, matrix := range matrixes {
|
||||
order[index], names[index] = index, matrixName(matrix.NameValues)
|
||||
order[index], names[index] = index, matrixName(matrix)
|
||||
}
|
||||
slices.SortStableFunc(order, func(a, b int) int { return strings.Compare(names[a], names[b]) })
|
||||
combos := make([]*Job, 0, len(matrixes))
|
||||
var err error
|
||||
for _, index := range order {
|
||||
matrix := matrixes[index].Values
|
||||
matrix := matrixes[index]
|
||||
combo := src.Clone()
|
||||
if combo.Name == "" {
|
||||
combo.Name = jobID
|
||||
}
|
||||
combo.Strategy.RawMatrix = encodeMatrix(matrix)
|
||||
replaceScalars(&combo.Strategy.RawMatrix, escapeExpressions)
|
||||
if src.Strategy.RawMatrix.Kind != 0 {
|
||||
combo.Strategy.JobIndex, combo.Strategy.JobTotal = index, len(matrixes)
|
||||
}
|
||||
evaluator := expreval.New(NewInterpeter(jobID, &combo.Strategy, matrix, gitCtx, results, vars, inputs).Evaluate)
|
||||
if combo.Name, err = jobName(combo.Name, jobID, names[index], evaluator, len(matrix) > 0 || gitCtx != nil); err != nil {
|
||||
if len(matrix) == 0 && gitCtx != nil {
|
||||
combo.Name, err = evaluator.Interpolate(combo.Name)
|
||||
combo.Name = escapeExpressions(combo.Name)
|
||||
} else {
|
||||
combo.Name, err = nameWithMatrix(combo.Name, matrix, evaluator)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("interpolate name for job %q: %w", jobID, err)
|
||||
}
|
||||
if gitCtx != nil { // callers without one don't read runs-on
|
||||
@@ -248,15 +259,14 @@ func buildMatrixCombos(jobID string, src *Job, matrixes []model.MatrixCombinatio
|
||||
if err := evaluator.EvaluateYamlNode(&rawRunsOn); err != nil {
|
||||
return nil, fmt.Errorf("interpolate runs-on for job %q: %w", jobID, err)
|
||||
}
|
||||
if rawRunsOn.Kind != 0 && runsOnProblem(&rawRunsOn) != "" {
|
||||
combo.RawRunsOn = rawRunsOn
|
||||
} else {
|
||||
runsOn := model.RunsOnFromNode(rawRunsOn)
|
||||
for i := range runsOn {
|
||||
runsOn[i] = escapeExpressions(runsOn[i])
|
||||
}
|
||||
combo.RawRunsOn = model.RunsOnNode(runsOn, "")
|
||||
runsOn := model.RunsOnFromNode(rawRunsOn)
|
||||
if len(runsOn) == 0 && len(srcRunsOn) > 0 { // match no runner rather than every runner
|
||||
runsOn = []string{""}
|
||||
}
|
||||
for i := range runsOn {
|
||||
runsOn[i] = escapeExpressions(runsOn[i])
|
||||
}
|
||||
combo.RawRunsOn = model.RunsOnNode(runsOn, "")
|
||||
}
|
||||
if err := evaluator.EvaluateYamlNode(&combo.RawContinueOnError); err != nil {
|
||||
return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", jobID, err)
|
||||
@@ -314,36 +324,29 @@ func encodeMatrix(matrix map[string]any) yaml.Node {
|
||||
return node
|
||||
}
|
||||
|
||||
// jobName trims names, gives plain text and lone string literals the suffix, and blank names the job ID
|
||||
func jobName(name, jobID, suffix string, evaluator expreval.Evaluator, evaluate bool) (string, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if literal, ok := expreval.Literal(name); ok {
|
||||
if literal == "" {
|
||||
literal = jobID
|
||||
}
|
||||
return escapeExpressions(literal + suffix), nil
|
||||
}
|
||||
if !evaluate {
|
||||
func nameWithMatrix(name string, m map[string]any, evaluator expreval.Evaluator) (string, error) {
|
||||
if len(m) == 0 {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
name, err := evaluator.Interpolate(name)
|
||||
if name = strings.TrimSpace(name); name == "" {
|
||||
name = jobID
|
||||
if !strings.Contains(name, "${{") || !strings.Contains(name, "}}") {
|
||||
return escapeExpressions(name + " " + matrixName(m)), nil
|
||||
}
|
||||
|
||||
name, err := evaluator.Interpolate(name)
|
||||
return escapeExpressions(name), err
|
||||
}
|
||||
|
||||
// matrixName formats the name suffix, skipping null and empty values
|
||||
func matrixName(values []any) string {
|
||||
var names []string
|
||||
for _, value := range values {
|
||||
if name := exprparser.CoerceToString(value); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
func matrixName(m map[string]any) string {
|
||||
ks := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return ""
|
||||
sort.Strings(ks)
|
||||
vs := make([]string, 0, len(m))
|
||||
for _, v := range ks {
|
||||
vs = append(vs, fmt.Sprint(m[v]))
|
||||
}
|
||||
return " (" + strings.Join(names, ", ") + ")"
|
||||
|
||||
return fmt.Sprintf("(%s)", strings.Join(vs, ", "))
|
||||
}
|
||||
|
||||
@@ -290,68 +290,17 @@ func TestParseInterpolatesRunName(t *testing.T) {
|
||||
assert.Empty(t, result[0].RunName)
|
||||
}
|
||||
|
||||
func TestParseRunsOnFromJSONKeepsWhatGitHubRejectsForTheJobToFail(t *testing.T) {
|
||||
func TestParseRunsOnFromJSONArray(t *testing.T) {
|
||||
content := []byte("on: push\njobs:\n build:\n runs-on: ${{ fromJSON(vars.RUNNER) }}\n steps: [{run: echo}]\n")
|
||||
_, err := Parse(content)
|
||||
require.NoError(t, err)
|
||||
for runner, want := range map[string][]string{`["self-hosted", "linux"]`: {"self-hosted", "linux"}, "[]": {}, "{}": {}} {
|
||||
for runner, want := range map[string][]string{`["self-hosted", "linux"]`: {"self-hosted", "linux"}, "[]": {""}} {
|
||||
result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
_, job := result[0].Job()
|
||||
assert.Equal(t, want, job.RunsOn(), runner)
|
||||
}
|
||||
for runner, problem := range map[string]string{`["a"]`: "", `""`: "Unexpected value ''", `[["a"]]`: "A sequence was not expected"} {
|
||||
result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner}))
|
||||
require.NoError(t, err)
|
||||
payload, err := result[0].Marshal()
|
||||
require.NoError(t, err)
|
||||
_, job, err := ParseRawSingleWorkflow(payload)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, problem, job.RunsOnProblem(), runner)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJobNames(t *testing.T) {
|
||||
result, err := Parse([]byte(`on: push
|
||||
jobs:
|
||||
scalars:
|
||||
strategy: {matrix: {value: [[1.0, true, false, 0, 1000000000000000, '', null, {os: linux}], ['', null]]}}
|
||||
steps: [{run: echo}]
|
||||
trimmed:
|
||||
name: ' Trimmed '
|
||||
strategy: {matrix: {v: [a]}}
|
||||
steps: [{run: echo}]
|
||||
blank:
|
||||
name: ' '
|
||||
steps: [{run: echo}]
|
||||
folded:
|
||||
name: "${{ ' Folded' }}"
|
||||
strategy: {matrix: {v: [a]}}
|
||||
steps: [{run: echo}]
|
||||
computed:
|
||||
name: ${{ format(' {0} ', matrix.missing) }}
|
||||
strategy: {matrix: {v: [a]}}
|
||||
steps: [{run: echo}]
|
||||
padded:
|
||||
name: ${{ format(' {0} ', matrix.v) }}
|
||||
strategy: {matrix: {v: [a]}}
|
||||
steps: [{run: echo}]
|
||||
`), WithGitContext(&model.GithubContext{}))
|
||||
require.NoError(t, err)
|
||||
names := map[string][]string{}
|
||||
for _, parsed := range result {
|
||||
id, job := parsed.Job()
|
||||
names[id] = append(names[id], job.DisplayName())
|
||||
}
|
||||
assert.Equal(t, map[string][]string{
|
||||
"scalars": {"scalars", "scalars (1, true, false, 0, 1E+15, linux)"},
|
||||
"trimmed": {"Trimmed (a)"},
|
||||
"blank": {"blank"},
|
||||
"folded": {" Folded (a)"},
|
||||
"computed": {"computed"},
|
||||
"padded": {"a"},
|
||||
}, names)
|
||||
}
|
||||
|
||||
func TestJobFieldsWithoutMatrix(t *testing.T) {
|
||||
@@ -509,37 +458,6 @@ func TestReadWorkflowJobConditionContexts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWorkflowStaticJobKindAndRunsOnLikeGitHub(t *testing.T) {
|
||||
for job, want := range map[string]string{
|
||||
"{runs-on: x, steps: [{run: echo}]}": "",
|
||||
"{uses: o/r/.gitea/workflows/c.yml@main}": "",
|
||||
"{runs-on: []}": "",
|
||||
"{runs-on: {}}": "",
|
||||
"{runs-on: {group: org/g, labels: [a, 1]}}": "",
|
||||
"{runs-on: {group: '${{ vars.G }}'}}": "",
|
||||
"{steps: [{run: echo}]}": "Required property is missing: runs-on",
|
||||
"{with: {}}": "Required property is missing: uses",
|
||||
"{runs-on: x, uses: o/r/.gitea/workflows/c.yml@main}": "Unexpected value 'uses'",
|
||||
"{Runs-On: x}": "Unexpected value 'Runs-On'",
|
||||
"{runs-on: ~}": "runs-on: Unexpected value ''",
|
||||
"{runs-on: ['']}": "runs-on: Unexpected value ''",
|
||||
"{runs-on: [[a]]}": "runs-on: A sequence was not expected",
|
||||
"{runs-on: {labels: {a: b}}}": "runs-on: A mapping was not expected",
|
||||
"{runs-on: {foo: x}}": "runs-on: Unexpected value 'foo'",
|
||||
"{runs-on: {group: org/}}": "runs-on: Invalid runs-on group name 'org/'.",
|
||||
"{runs-on: {group: a/b/c}}": "runs-on: Invalid runs-on group name 'a/b/c'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.",
|
||||
"{if: true}": "There's not enough info to determine what you meant. Add one of these properties: " +
|
||||
"cancel-timeout-minutes, container, continue-on-error, defaults, env, environment, outputs, runs-on, secrets, services, snapshot, steps, timeout-minutes, uses, with",
|
||||
} {
|
||||
_, err := ValidateWorkflowStatic([]byte("on: push\njobs:\n build: " + job + "\n"))
|
||||
if want == "" {
|
||||
assert.NoError(t, err, job)
|
||||
} else {
|
||||
assert.EqualError(t, err, "job build: "+want, job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsUnevaluatedMatrixFilters(t *testing.T) {
|
||||
for _, filter := range []string{"include", "exclude"} {
|
||||
t.Run(filter, func(t *testing.T) {
|
||||
|
||||
@@ -174,14 +174,6 @@ func (j *Job) EraseNeeds() *Job {
|
||||
return j
|
||||
}
|
||||
|
||||
// RunsOnProblem returns github.com's error for the job's runs-on, "" if valid.
|
||||
func (j *Job) RunsOnProblem() string {
|
||||
if j.RawRunsOn.Kind == 0 {
|
||||
return ""
|
||||
}
|
||||
return runsOnProblem(&j.RawRunsOn)
|
||||
}
|
||||
|
||||
// RunsOn returns the labels Gitea matches runners against, unescaped like DisplayName.
|
||||
func (j *Job) RunsOn() []string {
|
||||
runsOn := model.RunsOnFromNode(j.RawRunsOn)
|
||||
|
||||
@@ -7,13 +7,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/actionslib/pkg/expreval"
|
||||
"gitea.dev/actionslib/pkg/exprparser"
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// jobConditionContexts are what github.com gives `jobs.<job_id>.if`, which it decides before the matrix, plus the `gitea` alias.
|
||||
@@ -24,7 +21,7 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Keep unknown and case-distinct keys outside of jobs accepted for existing Gitea workflows.
|
||||
// Keep unknown and case-distinct keys accepted for existing Gitea workflows.
|
||||
workflow, err := readWorkflowDoc(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -36,9 +33,6 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
|
||||
if err := validateWorkflowStructure(workflow); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateJobKinds(doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var header struct {
|
||||
RunName string `yaml:"run-name"`
|
||||
}
|
||||
@@ -82,6 +76,7 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
|
||||
if job == nil {
|
||||
return fmt.Errorf("job %q has no configuration", id)
|
||||
}
|
||||
// a job without runs-on is accepted and runs on any runner, github.com rejects it
|
||||
for _, dependency := range job.Needs() {
|
||||
if _, ok := workflow.Jobs[dependency]; !ok {
|
||||
return fmt.Errorf("job %q needs unknown job %q", id, dependency)
|
||||
@@ -115,116 +110,3 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// job keys of github.com's workflow schema, by the kind of job allowing them
|
||||
var (
|
||||
stepsJobKeys = []string{"cancel-timeout-minutes", "container", "continue-on-error", "defaults", "env", "environment", "outputs", "runs-on", "services", "snapshot", "steps", "timeout-minutes"}
|
||||
callerJobKeys = []string{"secrets", "uses", "with"}
|
||||
sharedJobKeys = []string{"concurrency", "if", "name", "needs", "permissions", "strategy"}
|
||||
)
|
||||
|
||||
// validateJobKinds applies github.com's job kinds, decided by the first kind-specific key.
|
||||
func validateJobKinds(doc *yaml.Node) error {
|
||||
jobs := mappingValue(doc.Content[0], "jobs")
|
||||
for i := 0; i+1 < len(jobs.Content); i += 2 {
|
||||
id, job := jobs.Content[i].Value, jobs.Content[i+1]
|
||||
var required string
|
||||
for j := 0; j+1 < len(job.Content); j += 2 {
|
||||
key := job.Content[j].Value
|
||||
isStepsKey, isCallerKey := slices.Contains(stepsJobKeys, key), slices.Contains(callerJobKeys, key)
|
||||
switch {
|
||||
case required == "runs-on" && isCallerKey, required == "uses" && isStepsKey, !isStepsKey && !isCallerKey && !slices.Contains(sharedJobKeys, key):
|
||||
return fmt.Errorf("job %s: Unexpected value '%s'", id, key)
|
||||
case required == "" && isStepsKey:
|
||||
required = "runs-on"
|
||||
case required == "" && isCallerKey:
|
||||
required = "uses"
|
||||
}
|
||||
}
|
||||
if required == "" {
|
||||
keys := slices.Concat(stepsJobKeys, callerJobKeys)
|
||||
slices.Sort(keys)
|
||||
return fmt.Errorf("job %s: There's not enough info to determine what you meant. Add one of these properties: %s", id, strings.Join(keys, ", "))
|
||||
}
|
||||
value := mappingValue(job, required)
|
||||
if value == nil {
|
||||
return fmt.Errorf("job %s: Required property is missing: %s", id, required)
|
||||
}
|
||||
if required != "runs-on" {
|
||||
continue
|
||||
}
|
||||
if problem := runsOnProblem(value); problem != "" {
|
||||
return fmt.Errorf("job %s: runs-on: %s", id, problem)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runsOnProblem returns github.com's schema error for a runs-on, "" if valid.
|
||||
func runsOnProblem(node *yaml.Node) string {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return runsOnLabelsProblem(node)
|
||||
}
|
||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||
var problem string
|
||||
switch key := node.Content[i].Value; key {
|
||||
case "labels":
|
||||
problem = runsOnLabelsProblem(node.Content[i+1])
|
||||
case "group":
|
||||
problem = runsOnGroupProblem(node.Content[i+1])
|
||||
default:
|
||||
problem = fmt.Sprintf("Unexpected value '%s'", key)
|
||||
}
|
||||
if problem != "" {
|
||||
return problem
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func runsOnLabelsProblem(node *yaml.Node) string {
|
||||
if node.Kind != yaml.SequenceNode {
|
||||
return nonEmptyStringProblem(node)
|
||||
}
|
||||
for _, label := range node.Content {
|
||||
if problem := nonEmptyStringProblem(label); problem != "" {
|
||||
return problem
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func runsOnGroupProblem(node *yaml.Node) string {
|
||||
if problem := nonEmptyStringProblem(node); problem != "" || hasExpression(node.Value) {
|
||||
return problem
|
||||
}
|
||||
switch prefix, name, found := strings.Cut(node.Value, "/"); {
|
||||
case found && name == "":
|
||||
return fmt.Sprintf("Invalid runs-on group name '%s'.", node.Value)
|
||||
case found && (strings.Contains(name, "/") || !slices.Contains([]string{"org", "organization", "ent", "enterprise"}, prefix)):
|
||||
return fmt.Sprintf("Invalid runs-on group name '%s'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.", node.Value)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// nonEmptyStringProblem mirrors github.com's non-empty-string, which also accepts non-string scalars.
|
||||
func nonEmptyStringProblem(node *yaml.Node) string {
|
||||
switch {
|
||||
case node.Kind == yaml.SequenceNode:
|
||||
return "A sequence was not expected"
|
||||
case node.Kind == yaml.MappingNode:
|
||||
return "A mapping was not expected"
|
||||
case node.Value == "" || node.ShortTag() == "!!null":
|
||||
return "Unexpected value ''"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func mappingValue(node *yaml.Node, key string) *yaml.Node {
|
||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||
if node.Content[i].Value == key {
|
||||
return node.Content[i+1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -30,23 +30,18 @@ jobs:
|
||||
|
||||
func TestReadWorkflowEventsStaticErrors(t *testing.T) {
|
||||
for content, static := range map[string]bool{
|
||||
"on: push\njobs: {}": true,
|
||||
"on: push\njobs: {test: {runs-on: x, needs: absent}}": true,
|
||||
"on: push\njobs: {one: {runs-on: x, needs: two}, two: {runs-on: x, needs: one}}": true,
|
||||
"on: push\njobs: {test: {runs-on: x, strategy: {matrix: {os: []}}}}": true,
|
||||
"on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {runs-on: x}}": true,
|
||||
"on: push\njobs: {test: {steps: [{run: echo}]}}": true,
|
||||
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {runs-on: x, steps: [{run: echo}]}}": false,
|
||||
"on: push\njobs: {}": true,
|
||||
"on: push\njobs: {test: {needs: absent}}": true,
|
||||
"on: push\njobs: {one: {needs: two}, two: {needs: one}}": true,
|
||||
"on: push\njobs: {test: {strategy: {matrix: {os: []}}}}": true,
|
||||
"on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {}}": true,
|
||||
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {steps: [{run: echo}]}}": false,
|
||||
} {
|
||||
_, gotStatic, err := readWorkflowEvents([]byte(content))
|
||||
require.Error(t, err, content)
|
||||
assert.Equal(t, static, gotStatic, content)
|
||||
}
|
||||
for _, content := range []string{
|
||||
"on: push\njobs: {test: {runs-on: x, steps: [{run: echo}]}}",
|
||||
"on: push\nrun-name: ${{ github.ref }}\njobs: {test: {runs-on: x}}",
|
||||
"on: push\njobs: {call: {uses: ./.gitea/workflows/called.yml}}",
|
||||
} {
|
||||
for _, content := range []string{"on: push\njobs: {test: {steps: [{run: echo}]}}", "on: push\nrun-name: ${{ github.ref }}\njobs: {test: {}}"} {
|
||||
_, _, err := readWorkflowEvents([]byte(content))
|
||||
assert.NoError(t, err, content)
|
||||
}
|
||||
|
||||
@@ -102,11 +102,11 @@ func NewEmbeddedFS(data []byte) fs.ReadDirFS {
|
||||
efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)}
|
||||
efs.meta = sync.OnceValue(func() *EmbeddedMeta {
|
||||
var meta EmbeddedMeta
|
||||
_, metaJSON, ok := bytes.CutLast(data, []byte{'\n'})
|
||||
if !ok {
|
||||
p := bytes.LastIndexByte(data, '\n')
|
||||
if p < 0 {
|
||||
return &meta
|
||||
}
|
||||
if err := json.Unmarshal(metaJSON, &meta); err != nil {
|
||||
if err := json.Unmarshal(data[p+1:], &meta); err != nil {
|
||||
panic("embedded file is not valid")
|
||||
}
|
||||
return &meta
|
||||
|
||||
Vendored
+3
-1
@@ -13,6 +13,8 @@ import (
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
_ "gitea.com/go-chi/cache/memcache" //nolint:depguard // memcache plugin for cache, it is required for config "ADAPTER=memcache"
|
||||
)
|
||||
|
||||
var defaultCache StringCache
|
||||
@@ -81,7 +83,7 @@ func GetCache() StringCache {
|
||||
|
||||
// GetString returns the key value from cache with callback when no key exists in cache
|
||||
func GetString(key string, getFunc func() (string, error)) (string, error) {
|
||||
if defaultCache == nil || setting.CacheService.TTL <= 0 {
|
||||
if defaultCache == nil || setting.CacheService.TTL == 0 {
|
||||
return getFunc()
|
||||
}
|
||||
cached, exist := defaultCache.Get(key)
|
||||
|
||||
Vendored
-70
@@ -1,70 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
)
|
||||
|
||||
const memcacheMaxRelativeTTL = 30 * 24 * 60 * 60
|
||||
|
||||
type memcacheCache struct {
|
||||
client *memcache.Client
|
||||
}
|
||||
|
||||
func newMemcacheCache(conn string) (backend, error) {
|
||||
if conn == "" {
|
||||
return nil, errors.New("cache adapter memcache requires [cache] HOST, e.g. 127.0.0.1:11211")
|
||||
}
|
||||
servers := &memcache.ServerList{}
|
||||
if err := servers.SetServers(strings.Split(conn, ";")...); err != nil {
|
||||
return nil, fmt.Errorf("invalid memcache HOST %q: %w", conn, err)
|
||||
}
|
||||
return &memcacheCache{client: memcache.NewFromSelector(servers)}, nil
|
||||
}
|
||||
|
||||
// memcacheExpiration converts a TTL beyond memcached's 30-day relative limit into an absolute unix time
|
||||
func memcacheExpiration(ttl int64) int32 {
|
||||
if ttl > memcacheMaxRelativeTTL {
|
||||
ttl += timeNow().Unix()
|
||||
}
|
||||
return int32(min(ttl, math.MaxInt32))
|
||||
}
|
||||
|
||||
func (c *memcacheCache) Get(key string) (string, bool) {
|
||||
item, err := c.client.Get(key)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(item.Value), true
|
||||
}
|
||||
|
||||
func (c *memcacheCache) Put(key, value string, ttl int64) error {
|
||||
return c.client.Set(&memcache.Item{Key: key, Value: []byte(value), Expiration: memcacheExpiration(ttl)})
|
||||
}
|
||||
|
||||
func (c *memcacheCache) Delete(key string) error {
|
||||
if err := c.client.Delete(key); err != nil && !errors.Is(err, memcache.ErrCacheMiss) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *memcacheCache) IsExist(key string) bool {
|
||||
_, err := c.client.Get(key)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (c *memcacheCache) Ping() error {
|
||||
const key = "__gitea_cache_ping"
|
||||
if err := c.Put(key, "ping", 10); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Delete(key)
|
||||
}
|
||||
Vendored
+134
-19
@@ -4,44 +4,159 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/nosql"
|
||||
|
||||
"gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type redisCache struct {
|
||||
client redis.UniversalClient
|
||||
prefix string
|
||||
// RedisCacher represents a redis cache adapter implementation.
|
||||
type RedisCacher struct {
|
||||
c redis.UniversalClient
|
||||
prefix string
|
||||
hsetName string
|
||||
occupyMode bool
|
||||
}
|
||||
|
||||
func newRedisCache(conn string) backend {
|
||||
uri := nosql.ToRedisURI(conn)
|
||||
return &redisCache{
|
||||
client: nosql.GetManager().GetRedisClient(uri.String()),
|
||||
prefix: uri.Query().Get("prefix"),
|
||||
// toStr convert string/int/int64 interface to string. it's only used by the RedisCacher.Put internally
|
||||
func toStr(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []byte:
|
||||
return string(v)
|
||||
case int:
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10)
|
||||
default:
|
||||
return fmt.Sprint(v) // as what the old com.ToStr does in most cases
|
||||
}
|
||||
}
|
||||
|
||||
func (c *redisCache) Get(key string) (string, bool) {
|
||||
value, err := c.client.Get(graceful.GetManager().HammerContext(), c.prefix+key).Result()
|
||||
return value, err == nil
|
||||
// Put puts value (string type) into cache with key and expire time.
|
||||
// If expired is 0, it lives forever.
|
||||
func (c *RedisCacher) Put(key string, val any, expire int64) error {
|
||||
// this function is not well-designed, it only puts string values into cache
|
||||
key = c.prefix + key
|
||||
if expire == 0 {
|
||||
if err := c.c.Set(graceful.GetManager().HammerContext(), key, toStr(val), 0).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
dur := time.Duration(expire) * time.Second
|
||||
if err := c.c.Set(graceful.GetManager().HammerContext(), key, toStr(val), dur).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.occupyMode {
|
||||
return nil
|
||||
}
|
||||
return c.c.HSet(graceful.GetManager().HammerContext(), c.hsetName, key, "0").Err()
|
||||
}
|
||||
|
||||
func (c *redisCache) Put(key, value string, ttl int64) error {
|
||||
return c.client.Set(graceful.GetManager().HammerContext(), c.prefix+key, value, time.Duration(ttl)*time.Second).Err()
|
||||
// Get gets cached value by given key.
|
||||
func (c *RedisCacher) Get(key string) any {
|
||||
val, err := c.c.Get(graceful.GetManager().HammerContext(), c.prefix+key).Result()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (c *redisCache) Delete(key string) error {
|
||||
return c.client.Del(graceful.GetManager().HammerContext(), c.prefix+key).Err()
|
||||
// Delete deletes cached value by given key.
|
||||
func (c *RedisCacher) Delete(key string) error {
|
||||
key = c.prefix + key
|
||||
if err := c.c.Del(graceful.GetManager().HammerContext(), key).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.occupyMode {
|
||||
return nil
|
||||
}
|
||||
return c.c.HDel(graceful.GetManager().HammerContext(), c.hsetName, key).Err()
|
||||
}
|
||||
|
||||
func (c *redisCache) IsExist(key string) bool {
|
||||
return c.client.Exists(graceful.GetManager().HammerContext(), c.prefix+key).Val() == 1
|
||||
// Incr increases cached int-type value by given key as a counter.
|
||||
func (c *RedisCacher) Incr(key string) error {
|
||||
if !c.IsExist(key) {
|
||||
return fmt.Errorf("key '%s' not exist", key)
|
||||
}
|
||||
return c.c.Incr(graceful.GetManager().HammerContext(), c.prefix+key).Err()
|
||||
}
|
||||
|
||||
func (c *redisCache) Ping() error {
|
||||
return c.client.Ping(graceful.GetManager().HammerContext()).Err()
|
||||
// Decr decreases cached int-type value by given key as a counter.
|
||||
func (c *RedisCacher) Decr(key string) error {
|
||||
if !c.IsExist(key) {
|
||||
return fmt.Errorf("key '%s' not exist", key)
|
||||
}
|
||||
return c.c.Decr(graceful.GetManager().HammerContext(), c.prefix+key).Err()
|
||||
}
|
||||
|
||||
// IsExist returns true if cached value exists.
|
||||
func (c *RedisCacher) IsExist(key string) bool {
|
||||
if c.c.Exists(graceful.GetManager().HammerContext(), c.prefix+key).Val() == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
if !c.occupyMode {
|
||||
c.c.HDel(graceful.GetManager().HammerContext(), c.hsetName, c.prefix+key)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Flush deletes all cached data.
|
||||
func (c *RedisCacher) Flush() error {
|
||||
if c.occupyMode {
|
||||
return c.c.FlushDB(graceful.GetManager().HammerContext()).Err()
|
||||
}
|
||||
|
||||
keys, err := c.c.HKeys(graceful.GetManager().HammerContext(), c.hsetName).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.c.Del(graceful.GetManager().HammerContext(), keys...).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.c.Del(graceful.GetManager().HammerContext(), c.hsetName).Err()
|
||||
}
|
||||
|
||||
// StartAndGC starts GC routine based on config string settings.
|
||||
// AdapterConfig: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180,hset_name=MacaronCache,prefix=cache:
|
||||
func (c *RedisCacher) StartAndGC(opts cache.Options) error {
|
||||
c.hsetName = "MacaronCache"
|
||||
c.occupyMode = opts.OccupyMode
|
||||
|
||||
uri := nosql.ToRedisURI(opts.AdapterConfig)
|
||||
|
||||
c.c = nosql.GetManager().GetRedisClient(uri.String())
|
||||
|
||||
for k, v := range uri.Query() {
|
||||
switch k {
|
||||
case "hset_name":
|
||||
c.hsetName = v[0]
|
||||
case "prefix":
|
||||
c.prefix = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
return c.c.Ping(graceful.GetManager().HammerContext()).Err()
|
||||
}
|
||||
|
||||
// Ping tests if the cache is alive.
|
||||
func (c *RedisCacher) Ping() error {
|
||||
return c.c.Ping(graceful.GetManager().HammerContext()).Err()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cache.Register("redis", &RedisCacher{})
|
||||
}
|
||||
|
||||
Vendored
-95
@@ -8,12 +8,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/nosql"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createTestCache() {
|
||||
@@ -35,98 +32,6 @@ func TestNewContext(t *testing.T) {
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, con)
|
||||
|
||||
_, err = NewStringCache(setting.Cache{Adapter: "memcache"})
|
||||
assert.ErrorContains(t, err, "requires [cache] HOST")
|
||||
}
|
||||
|
||||
func TestStringCacheAdapters(t *testing.T) {
|
||||
now := time.Now()
|
||||
defer test.MockVariableValue(&timeNow, func() time.Time { return now })()
|
||||
cases := []struct {
|
||||
adapter string
|
||||
conns func(t *testing.T) (string, string)
|
||||
}{
|
||||
{adapter: "memory", conns: func(*testing.T) (string, string) { return "", "" }},
|
||||
{adapter: "twoqueue", conns: func(*testing.T) (string, string) { return "100", `{"size":100}` }},
|
||||
{adapter: "redis", conns: func(t *testing.T) (string, string) {
|
||||
conn := test.PrepareTestRedis(t) + "?prefix=gitea-test-cache-"
|
||||
return conn + "first:", conn + "second:"
|
||||
}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.adapter, func(t *testing.T) {
|
||||
firstConn, secondConn := tc.conns(t)
|
||||
first, err := NewStringCache(setting.Cache{Adapter: tc.adapter, Conn: firstConn, Interval: -1})
|
||||
require.NoError(t, err)
|
||||
second, err := NewStringCache(setting.Cache{Adapter: tc.adapter, Conn: secondConn, Interval: -1})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, first.Ping())
|
||||
|
||||
require.NoError(t, first.Put("key", "value", 0))
|
||||
value, ok := first.Get("key")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "value", value)
|
||||
assert.True(t, first.IsExist("key"))
|
||||
assert.False(t, second.IsExist("key"))
|
||||
|
||||
require.NoError(t, first.Delete("key"))
|
||||
_, ok = first.Get("key")
|
||||
assert.False(t, ok)
|
||||
assert.False(t, first.IsExist("key"))
|
||||
require.NoError(t, first.Delete("key"))
|
||||
|
||||
require.NoError(t, first.Put("expiring", "value", 10))
|
||||
if tc.adapter == "redis" {
|
||||
uri := nosql.ToRedisURI(firstConn)
|
||||
ttl := nosql.GetManager().GetRedisClient(uri.String()).TTL(t.Context(), uri.Query().Get("prefix")+"expiring").Val()
|
||||
assert.Positive(t, ttl)
|
||||
assert.LessOrEqual(t, ttl, 10*time.Second)
|
||||
require.NoError(t, first.Delete("expiring"))
|
||||
return
|
||||
}
|
||||
now = now.Add(9 * time.Second)
|
||||
assert.True(t, first.IsExist("expiring"))
|
||||
now = now.Add(time.Second)
|
||||
assert.False(t, first.IsExist("expiring"))
|
||||
_, ok = first.Get("expiring")
|
||||
assert.False(t, ok)
|
||||
require.NoError(t, first.Put("expiring", "renewed", 0))
|
||||
value, ok = first.Get("expiring")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "renewed", value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNegativeItemTTLDisablesOnlyItemTTLCaching(t *testing.T) {
|
||||
createTestCache()
|
||||
defer test.MockVariableValue(&setting.CacheService.TTL, -1)()
|
||||
|
||||
require.NoError(t, defaultCache.Put("key", "stale", 0))
|
||||
require.NoError(t, defaultCache.Put("key", "value", setting.CacheService.TTLSeconds()))
|
||||
assert.False(t, defaultCache.IsExist("key"))
|
||||
|
||||
calls := 0
|
||||
for range 2 {
|
||||
data, err := GetString("key", func() (string, error) {
|
||||
calls++
|
||||
return "value", nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value", data)
|
||||
}
|
||||
assert.Equal(t, 2, calls)
|
||||
assert.False(t, defaultCache.IsExist("key"))
|
||||
|
||||
require.NoError(t, defaultCache.Put("captcha", "value", 600))
|
||||
assert.True(t, defaultCache.IsExist("captcha"))
|
||||
}
|
||||
|
||||
func TestMemcacheExpiration(t *testing.T) {
|
||||
defer test.MockVariableValue(&timeNow, func() time.Time { return time.Unix(1000, 0) })()
|
||||
assert.EqualValues(t, memcacheMaxRelativeTTL, memcacheExpiration(memcacheMaxRelativeTTL))
|
||||
assert.EqualValues(t, 1000+memcacheMaxRelativeTTL+1, memcacheExpiration(memcacheMaxRelativeTTL+1))
|
||||
}
|
||||
|
||||
func TestTest(t *testing.T) {
|
||||
|
||||
Vendored
+169
-87
@@ -4,123 +4,205 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
|
||||
mc "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
const twoQueueDefaultSize = 50000
|
||||
|
||||
type twoQueueCache struct {
|
||||
cache *lru.TwoQueueCache[string, memoryItem] // wrap the existing thread-safe 2Q (two-queue) cache directly
|
||||
// TwoQueueCache represents a LRU 2Q cache adapter implementation
|
||||
type TwoQueueCache struct {
|
||||
lock sync.Mutex
|
||||
cache *lru.TwoQueueCache[string, any]
|
||||
interval int
|
||||
}
|
||||
|
||||
type twoQueueCacheConfig struct {
|
||||
Size int `json:"size"`
|
||||
RecentRatio float64 `json:"recent_ratio"`
|
||||
GhostRatio float64 `json:"ghost_ratio"`
|
||||
// TwoQueueCacheConfig describes the configuration for TwoQueueCache
|
||||
type TwoQueueCacheConfig struct {
|
||||
Size int `ini:"SIZE" json:"size"`
|
||||
RecentRatio float64 `ini:"RECENT_RATIO" json:"recent_ratio"`
|
||||
GhostRatio float64 `ini:"GHOST_RATIO" json:"ghost_ratio"`
|
||||
}
|
||||
|
||||
func newTwoQueueCache(conn string, gcInterval time.Duration) (backend, error) {
|
||||
lruCache, err := newTwoQueueLRU(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache := &twoQueueCache{cache: lruCache}
|
||||
startGC(gcInterval, cache.deleteExpired)
|
||||
return cache, nil
|
||||
// MemoryItem represents a memory cache item.
|
||||
type MemoryItem struct {
|
||||
Val any
|
||||
Created int64
|
||||
Timeout int64
|
||||
}
|
||||
|
||||
func newTwoQueueLRU(conn string) (*lru.TwoQueueCache[string, memoryItem], error) {
|
||||
if conn == "" {
|
||||
return lru.New2Q[string, memoryItem](twoQueueDefaultSize)
|
||||
}
|
||||
if size, err := strconv.Atoi(conn); err == nil {
|
||||
return lru.New2Q[string, memoryItem](size)
|
||||
}
|
||||
if !json.Valid([]byte(conn)) {
|
||||
return nil, fmt.Errorf("invalid two-queue cache HOST %q, expected a size or a JSON config", conn)
|
||||
}
|
||||
config := twoQueueCacheConfig{
|
||||
Size: twoQueueDefaultSize,
|
||||
RecentRatio: lru.Default2QRecentRatio,
|
||||
GhostRatio: lru.Default2QGhostEntries,
|
||||
}
|
||||
_ = json.Unmarshal([]byte(conn), &config)
|
||||
return lru.New2QParams[string, memoryItem](config.Size, config.RecentRatio, config.GhostRatio)
|
||||
func (item *MemoryItem) hasExpired() bool {
|
||||
return item.Timeout > 0 &&
|
||||
(time.Now().Unix()-item.Created) >= item.Timeout
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) Get(key string) (string, bool) {
|
||||
item, ok := c.cache.Get(key)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if item.expired(timeNow()) {
|
||||
c.cache.Remove(key)
|
||||
return "", false
|
||||
}
|
||||
return item.value, true
|
||||
}
|
||||
var _ mc.Cache = &TwoQueueCache{}
|
||||
|
||||
func (c *twoQueueCache) Put(key, value string, ttl int64) error {
|
||||
item := newMemoryItem(value, ttl)
|
||||
// Put puts value into cache with key and expire time.
|
||||
func (c *TwoQueueCache) Put(key string, val any, timeout int64) error {
|
||||
item := &MemoryItem{
|
||||
Val: val,
|
||||
Created: time.Now().Unix(),
|
||||
Timeout: timeout,
|
||||
}
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.cache.Add(key, item)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) Delete(key string) error {
|
||||
// Get gets cached value by given key.
|
||||
func (c *TwoQueueCache) Get(key string) any {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item, ok := cached.(*MemoryItem)
|
||||
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
return item.Val
|
||||
}
|
||||
|
||||
// Delete deletes cached value by given key.
|
||||
func (c *TwoQueueCache) Delete(key string) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.cache.Remove(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) IsExist(key string) bool {
|
||||
item, ok := c.cache.Peek(key)
|
||||
return ok && !item.expired(timeNow())
|
||||
// Incr increases cached int-type value by given key as a counter.
|
||||
func (c *TwoQueueCache) Incr(key string) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item, ok := cached.(*MemoryItem)
|
||||
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
item.Val, err = mc.Incr(item.Val)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) Ping() error {
|
||||
// Decr decreases cached int-type value by given key as a counter.
|
||||
func (c *TwoQueueCache) Decr(key string) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item, ok := cached.(*MemoryItem)
|
||||
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
item.Val, err = mc.Decr(item.Val)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsExist returns true if cached value exists.
|
||||
func (c *TwoQueueCache) IsExist(key string) bool {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Peek(key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
item, ok := cached.(*MemoryItem)
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Flush deletes all cached data.
|
||||
func (c *TwoQueueCache) Flush() error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.cache.Purge()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *twoQueueCache) deleteExpired() {
|
||||
now := timeNow()
|
||||
for _, key := range c.cache.Keys() {
|
||||
if item, ok := c.cache.Peek(key); ok && item.expired(now) {
|
||||
// here might be a slight data-race: the item might have been removed or updated by another goroutine between the Peek and Remove calls,
|
||||
// but it's acceptable since the cache is not guaranteed to be 100% accurate and any item can be evicted at any time
|
||||
c.cache.Remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type memoryItem struct {
|
||||
value string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func newMemoryItem(value string, ttl int64) memoryItem {
|
||||
item := memoryItem{value: value}
|
||||
if ttl > 0 {
|
||||
item.expiresAt = timeNow().Add(time.Duration(ttl) * time.Second)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func (item memoryItem) expired(now time.Time) bool {
|
||||
return !item.expiresAt.IsZero() && !now.Before(item.expiresAt)
|
||||
}
|
||||
|
||||
func startGC(interval time.Duration, deleteExpired func()) {
|
||||
if interval <= 0 {
|
||||
func (c *TwoQueueCache) checkAndInvalidate(key string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cached, ok := c.cache.Peek(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for range time.Tick(interval) {
|
||||
deleteExpired()
|
||||
}
|
||||
}()
|
||||
item, ok := cached.(*MemoryItem)
|
||||
if !ok || item.hasExpired() {
|
||||
c.cache.Remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TwoQueueCache) startGC() {
|
||||
if c.interval < 0 {
|
||||
return
|
||||
}
|
||||
for _, key := range c.cache.Keys() {
|
||||
c.checkAndInvalidate(key)
|
||||
}
|
||||
time.AfterFunc(time.Duration(c.interval)*time.Second, c.startGC)
|
||||
}
|
||||
|
||||
// StartAndGC starts GC routine based on config string settings.
|
||||
func (c *TwoQueueCache) StartAndGC(opts mc.Options) error {
|
||||
var err error
|
||||
size := 50000
|
||||
if opts.AdapterConfig != "" {
|
||||
size, err = strconv.Atoi(opts.AdapterConfig)
|
||||
}
|
||||
if err != nil {
|
||||
if !json.Valid([]byte(opts.AdapterConfig)) {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := &TwoQueueCacheConfig{
|
||||
Size: 50000,
|
||||
RecentRatio: lru.Default2QRecentRatio,
|
||||
GhostRatio: lru.Default2QGhostEntries,
|
||||
}
|
||||
_ = json.Unmarshal([]byte(opts.AdapterConfig), cfg)
|
||||
c.cache, err = lru.New2QParams[string, any](cfg.Size, cfg.RecentRatio, cfg.GhostRatio)
|
||||
} else {
|
||||
c.cache, err = lru.New2Q[string, any](size)
|
||||
}
|
||||
c.interval = opts.Interval
|
||||
if c.interval > 0 {
|
||||
go c.startGC()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Ping tests if the cache is alive.
|
||||
func (c *TwoQueueCache) Ping() error {
|
||||
return mc.GenericPing(c)
|
||||
}
|
||||
|
||||
func init() {
|
||||
mc.Register("twoqueue", &TwoQueueCache{})
|
||||
}
|
||||
|
||||
Vendored
+37
-34
@@ -5,14 +5,13 @@ package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
chi_cache "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
|
||||
)
|
||||
|
||||
type GetJSONError struct {
|
||||
@@ -31,57 +30,57 @@ type StringCache interface {
|
||||
Ping() error
|
||||
|
||||
Get(key string) (string, bool)
|
||||
Put(key, value string, ttl int64) error // ttl in seconds, 0 never expires, negative removes the key
|
||||
Put(key, value string, ttl int64) error
|
||||
Delete(key string) error
|
||||
IsExist(key string) bool
|
||||
|
||||
PutJSON(key string, v any, ttl int64) error
|
||||
GetJSON(key string, ptr any) (exist bool, err *GetJSONError)
|
||||
}
|
||||
|
||||
type backend interface {
|
||||
Get(key string) (string, bool)
|
||||
Put(key, value string, ttl int64) error
|
||||
Delete(key string) error
|
||||
IsExist(key string) bool
|
||||
Ping() error
|
||||
ChiCache() chi_cache.Cache
|
||||
}
|
||||
|
||||
type stringCache struct {
|
||||
backend
|
||||
chiCache chi_cache.Cache
|
||||
}
|
||||
|
||||
func NewStringCache(cacheConfig setting.Cache) (StringCache, error) {
|
||||
cacheBackend, err := newBackend(cacheConfig)
|
||||
adapter := util.IfZero(cacheConfig.Adapter, "memory")
|
||||
interval := util.IfZero(cacheConfig.Interval, 60)
|
||||
cc, err := chi_cache.NewCacher(chi_cache.Options{
|
||||
Adapter: adapter,
|
||||
AdapterConfig: cacheConfig.Conn,
|
||||
Interval: interval,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stringCache{backend: cacheBackend}, nil
|
||||
return &stringCache{chiCache: cc}, nil
|
||||
}
|
||||
|
||||
func newBackend(cacheConfig setting.Cache) (backend, error) {
|
||||
gcInterval := time.Duration(util.IfZero(cacheConfig.Interval, 60)) * time.Second
|
||||
switch adapter := util.IfZero(cacheConfig.Adapter, "memory"); adapter {
|
||||
case "memory":
|
||||
// the old "memory" adapter doesn't have a limit, which can lead to OOM
|
||||
// now, use two-queue cache for in-memory cache with items limit, at most a few GB of memory will be used
|
||||
return newTwoQueueCache(strconv.FormatInt(10*1024*1024, 10), gcInterval)
|
||||
case "twoqueue":
|
||||
return newTwoQueueCache(cacheConfig.Conn, gcInterval)
|
||||
case "redis":
|
||||
return newRedisCache(cacheConfig.Conn), nil
|
||||
case "memcache":
|
||||
return newMemcacheCache(cacheConfig.Conn)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown cache adapter %q", adapter)
|
||||
func (sc *stringCache) Ping() error {
|
||||
return sc.chiCache.Ping()
|
||||
}
|
||||
|
||||
func (sc *stringCache) Get(key string) (string, bool) {
|
||||
v := sc.chiCache.Get(key)
|
||||
if v == nil {
|
||||
return "", false
|
||||
}
|
||||
s, ok := v.(string)
|
||||
return s, ok
|
||||
}
|
||||
|
||||
func (sc *stringCache) Put(key, value string, ttl int64) error {
|
||||
if ttl < 0 {
|
||||
return sc.backend.Delete(key)
|
||||
}
|
||||
return sc.backend.Put(key, value, ttl)
|
||||
return sc.chiCache.Put(key, value, ttl)
|
||||
}
|
||||
|
||||
func (sc *stringCache) Delete(key string) error {
|
||||
return sc.chiCache.Delete(key)
|
||||
}
|
||||
|
||||
func (sc *stringCache) IsExist(key string) bool {
|
||||
return sc.chiCache.IsExist(key)
|
||||
}
|
||||
|
||||
const cachedErrorPrefix = "<CACHED-ERROR>:"
|
||||
@@ -98,7 +97,7 @@ func (sc *stringCache) PutJSON(key string, v any, ttl int64) error {
|
||||
}
|
||||
s = util.UnsafeBytesToString(b)
|
||||
}
|
||||
return sc.Put(key, s, ttl)
|
||||
return sc.chiCache.Put(key, s, ttl)
|
||||
}
|
||||
|
||||
func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSONError) {
|
||||
@@ -115,3 +114,7 @@ func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSON
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (sc *stringCache) ChiCache() chi_cache.Cache {
|
||||
return sc.chiCache
|
||||
}
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors.
|
||||
// Copyright 2018-2024 The Ruby Citation File Format Developers. Licensed under the Apache License, Version 2.0
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package citation formats CITATION.cff files, ported from the formatters of ruby-cff 1.3.0
|
||||
package citation
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"maps"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
type date struct{ time.Time }
|
||||
|
||||
var dateSeparators = strings.NewReplacer("/", "-", ". ", " ", ".", "-", ",", "")
|
||||
|
||||
func (d *date) UnmarshalYAML(node *yaml.Node) error {
|
||||
value := dateSeparators.Replace(node.Value)
|
||||
for _, layout := range []string{"2006-1-2", "2-1-2006", "2 Jan 2006", "2 January 2006", "Jan 2 2006", "January 2 2006"} {
|
||||
if parsed, err := time.Parse(layout, value); err == nil {
|
||||
d.Time = parsed
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type license string
|
||||
|
||||
func (l *license) UnmarshalYAML(node *yaml.Node) error {
|
||||
*l = license(node.Value)
|
||||
if node.Kind != yaml.ScalarNode {
|
||||
*l = license(inspectNode(node))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type actor struct {
|
||||
Name string `yaml:"name"`
|
||||
Alias string `yaml:"alias"`
|
||||
FamilyNames string `yaml:"family-names"`
|
||||
GivenNames string `yaml:"given-names"`
|
||||
NameParticle string `yaml:"name-particle"`
|
||||
NameSuffix string `yaml:"name-suffix"`
|
||||
Affiliation string `yaml:"affiliation"`
|
||||
City string `yaml:"city"`
|
||||
Region string `yaml:"region"`
|
||||
Country string `yaml:"country"`
|
||||
DateStart date `yaml:"date-start"`
|
||||
DateEnd date `yaml:"date-end"`
|
||||
}
|
||||
|
||||
func (a *actor) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.ScalarNode:
|
||||
a.Name = node.Value
|
||||
return nil
|
||||
case yaml.SequenceNode:
|
||||
return nil
|
||||
}
|
||||
type plainActor actor
|
||||
return node.Load((*plainActor)(a), yaml.WithUniqueKeys(false))
|
||||
}
|
||||
|
||||
type metadata struct {
|
||||
Type string `yaml:"type"`
|
||||
Title string `yaml:"title"`
|
||||
Authors []actor `yaml:"authors"`
|
||||
Version string `yaml:"version"`
|
||||
DOI string `yaml:"doi"`
|
||||
URL string `yaml:"url"`
|
||||
RepositoryCode string `yaml:"repository-code"`
|
||||
License license `yaml:"license"`
|
||||
DateReleased date `yaml:"date-released"`
|
||||
}
|
||||
|
||||
type reference struct {
|
||||
metadata `yaml:",inline"`
|
||||
isTopLevel bool
|
||||
Editors []actor `yaml:"editors"`
|
||||
EditorsSeries []actor `yaml:"editors-series"`
|
||||
DatePublished date `yaml:"date-published"`
|
||||
Year string `yaml:"year"`
|
||||
Month string `yaml:"month"`
|
||||
Status string `yaml:"status"`
|
||||
Journal string `yaml:"journal"`
|
||||
Volume string `yaml:"volume"`
|
||||
Issue string `yaml:"issue"`
|
||||
Start string `yaml:"start"`
|
||||
End string `yaml:"end"`
|
||||
ISBN string `yaml:"isbn"`
|
||||
Notes string `yaml:"notes"`
|
||||
CollectionTitle string `yaml:"collection-title"`
|
||||
ThesisType string `yaml:"thesis-type"`
|
||||
Publisher actor `yaml:"publisher"`
|
||||
Institution *actor `yaml:"institution"`
|
||||
Conference actor `yaml:"conference"`
|
||||
}
|
||||
|
||||
const (
|
||||
MaxContentSize = 256 * 1024 // parsing takes up to ~1000x the input, largest real-world file found is 80 KiB
|
||||
maxAliasExpansion = 64 * 1024 // nodes plus value bytes aliases may add
|
||||
)
|
||||
|
||||
// FormatCFF returns the APA and BibTeX citations of a CITATION.cff file, both empty if it has no title or authors
|
||||
func FormatCFF(content string) (apa, bibtex string) {
|
||||
var node yaml.Node
|
||||
// the parser copies %TAG prefixes into every node
|
||||
if len(content) > MaxContentSize || strings.Contains(content, "%TAG") || yaml.Unmarshal([]byte(content), &node) != nil || aliasExpansion(&node) > maxAliasExpansion {
|
||||
return "", ""
|
||||
}
|
||||
retagTimestamps(&node)
|
||||
var file struct {
|
||||
TopLevel metadata `yaml:",inline"`
|
||||
PreferredCitation *reference `yaml:"preferred-citation"`
|
||||
}
|
||||
if node.Load(&file, yaml.WithUniqueKeys(false)) != nil {
|
||||
return "", ""
|
||||
}
|
||||
ref := file.PreferredCitation
|
||||
if ref == nil {
|
||||
ref = &reference{metadata: file.TopLevel, isTopLevel: true}
|
||||
}
|
||||
if ref.Title == "" || len(ref.Authors) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
return ref.formatAPA(), ref.formatBibTeX()
|
||||
}
|
||||
|
||||
func retagTimestamps(node *yaml.Node) {
|
||||
if node.ShortTag() == "!!timestamp" {
|
||||
node.Tag = "!!str"
|
||||
}
|
||||
for _, child := range node.Content {
|
||||
retagTimestamps(child)
|
||||
}
|
||||
}
|
||||
|
||||
func aliasExpansion(root *yaml.Node) int {
|
||||
anchors := map[*yaml.Node]int{}
|
||||
added := 0
|
||||
var expandedSize func(node, parent *yaml.Node) int
|
||||
expandedSize = func(node, parent *yaml.Node) int {
|
||||
if node.Kind == yaml.AliasNode {
|
||||
size, walked := anchors[node.Alias]
|
||||
if !walked && (node.Alias != parent || parent.Kind != yaml.SequenceNode) { // decoders never expand a sequence listing itself
|
||||
size = maxAliasExpansion + 1
|
||||
}
|
||||
added = min(added+size, maxAliasExpansion+1)
|
||||
return size
|
||||
}
|
||||
size := 1 + len(node.Value)
|
||||
for _, child := range node.Content {
|
||||
size = min(size+expandedSize(child, node), maxAliasExpansion+1)
|
||||
}
|
||||
if node.Anchor != "" {
|
||||
anchors[node] = size
|
||||
}
|
||||
return size
|
||||
}
|
||||
expandedSize(root, nil)
|
||||
return added
|
||||
}
|
||||
|
||||
func inspectNode(node *yaml.Node) string {
|
||||
var parts []string
|
||||
switch node.Kind {
|
||||
case yaml.AliasNode:
|
||||
if node.Alias.Kind == yaml.ScalarNode { // collection aliases can be recursive
|
||||
return inspectNode(node.Alias)
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
for _, child := range node.Content {
|
||||
parts = append(parts, inspectNode(child))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case yaml.MappingNode:
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
parts = append(parts, inspectNode(node.Content[i])+" => "+inspectNode(node.Content[i+1]))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
}
|
||||
if node.ShortTag() == "!!null" {
|
||||
return "nil"
|
||||
}
|
||||
return strconv.Quote(node.Value)
|
||||
}
|
||||
|
||||
var statusNotes = map[string]string{
|
||||
"advance-online": "Advance online publication",
|
||||
"in-preparation": "Manuscript in preparation.",
|
||||
"submitted": "Manuscript submitted for publication.",
|
||||
}
|
||||
|
||||
func joinNonEmpty(sep string, parts ...string) string {
|
||||
return strings.Join(util.SliceRemoveAll(parts, ""), sep)
|
||||
}
|
||||
|
||||
func (r *reference) conferenceDates() (start, end date) {
|
||||
if r.Type == "conference-paper" {
|
||||
return r.Conference.DateStart, r.Conference.DateEnd
|
||||
}
|
||||
return date{}, date{}
|
||||
}
|
||||
|
||||
func (r *reference) monthAndYear() (month, year string) {
|
||||
when, _ := r.conferenceDates()
|
||||
if when.IsZero() {
|
||||
if r.Status == "in-press" {
|
||||
return "", "in press"
|
||||
}
|
||||
if r.Year != "" {
|
||||
return r.Month, r.Year
|
||||
}
|
||||
when = cmp.Or(r.DateReleased, r.DatePublished)
|
||||
}
|
||||
if when.IsZero() {
|
||||
return "", ""
|
||||
}
|
||||
return strconv.Itoa(int(when.Month())), strconv.Itoa(when.Year())
|
||||
}
|
||||
|
||||
func (r *reference) pages(dash string) string {
|
||||
if r.Start == "" || r.End == "" || r.Start == r.End {
|
||||
return r.Start
|
||||
}
|
||||
return r.Start + dash + r.End
|
||||
}
|
||||
|
||||
func (r *reference) volume() string {
|
||||
if r.Volume == "" || r.Issue == "" {
|
||||
return r.Volume
|
||||
}
|
||||
return r.Volume + "(" + r.Issue + ")"
|
||||
}
|
||||
|
||||
func (r *reference) institution() string {
|
||||
if r.Institution == nil {
|
||||
return r.Authors[0].Affiliation
|
||||
}
|
||||
return r.Institution.Name
|
||||
}
|
||||
|
||||
func (r *reference) formatAPA() string {
|
||||
authors := make([]string, 0, len(r.Authors))
|
||||
for _, author := range r.Authors {
|
||||
authors = append(authors, apaAuthor(author))
|
||||
}
|
||||
date := r.apaDate()
|
||||
if date != "" {
|
||||
date = "(" + date + ")"
|
||||
}
|
||||
version := ""
|
||||
if r.Version != "" {
|
||||
version = " (Version " + r.Version + ")"
|
||||
}
|
||||
url := cmp.Or(r.RepositoryCode, r.URL)
|
||||
if r.DOI != "" {
|
||||
url = "https://doi.org/" + r.DOI
|
||||
}
|
||||
return joinNonEmpty(". ", combineAuthors(authors), date, r.Title+version+r.apaTypeLabel(), r.apaPublicationData(), url)
|
||||
}
|
||||
|
||||
func apaAuthor(author actor) string {
|
||||
if author.Name != "" {
|
||||
return author.Name
|
||||
}
|
||||
name := cmp.Or(author.FamilyNames, author.GivenNames, author.Alias)
|
||||
if author.FamilyNames != "" && author.GivenNames != "" {
|
||||
name += ", " + initials(author.GivenNames) + "."
|
||||
}
|
||||
if author.NameParticle != "" {
|
||||
name = author.NameParticle + " " + name
|
||||
}
|
||||
if author.NameSuffix != "" {
|
||||
name += ", " + author.NameSuffix
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func initials(names string) string {
|
||||
parts := splitWords(names)
|
||||
for i, part := range parts {
|
||||
first, _ := utf8.DecodeRuneInString(part)
|
||||
parts[i] = util.ToTitleCase(string(first))
|
||||
}
|
||||
return strings.Join(parts, ". ")
|
||||
}
|
||||
|
||||
func splitWords(text string) []string {
|
||||
return strings.FieldsFunc(text, func(char rune) bool { return char <= unicode.MaxASCII && unicode.IsSpace(char) })
|
||||
}
|
||||
|
||||
func combineAuthors(authors []string) string {
|
||||
if len(authors) == 1 {
|
||||
return strings.TrimSuffix(authors[0], ".")
|
||||
}
|
||||
return strings.TrimSuffix(strings.Join(authors[:len(authors)-1], ", ")+", & "+authors[len(authors)-1], ".")
|
||||
}
|
||||
|
||||
func (r *reference) apaDate() string {
|
||||
start, end := r.conferenceDates()
|
||||
if start.IsZero() || end.IsZero() || !start.Before(end.Time) {
|
||||
_, year := r.monthAndYear()
|
||||
return year
|
||||
}
|
||||
endLayout := "2"
|
||||
if end.Month() != start.Month() {
|
||||
endLayout = "January 2"
|
||||
}
|
||||
if end.Year() != start.Year() {
|
||||
endLayout = "2006, " + endLayout
|
||||
}
|
||||
return start.Format("2006, January 2") + "–" + end.Format(endLayout)
|
||||
}
|
||||
|
||||
func (r *reference) apaTypeLabel() string {
|
||||
switch {
|
||||
case strings.Contains(r.Type, "data"):
|
||||
return " [Data set]"
|
||||
case strings.Contains(r.Type, "conference"):
|
||||
return " [Conference paper]"
|
||||
case !r.isTopLevel && !strings.Contains(r.Type, "software"):
|
||||
return ""
|
||||
}
|
||||
return " [Computer software]"
|
||||
}
|
||||
|
||||
func (r *reference) apaPublicationData() string {
|
||||
switch r.Type {
|
||||
case "article":
|
||||
return joinNonEmpty(", ", r.Journal, r.volume(), r.pages("–"), statusNotes[r.Status])
|
||||
case "book":
|
||||
return r.Publisher.Name
|
||||
case "conference-paper":
|
||||
return joinNonEmpty(", ", r.CollectionTitle, r.volume(), r.pages("–"))
|
||||
case "report":
|
||||
return r.institution()
|
||||
case "phdthesis":
|
||||
return "[" + cmp.Or(r.ThesisType, "Doctoral dissertation") + ", " + r.institution() + "]"
|
||||
case "mastersthesis":
|
||||
return "[" + cmp.Or(r.ThesisType, "Master's thesis") + ", " + r.institution() + "]"
|
||||
case "unpublished":
|
||||
return statusNotes[r.Status]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var bibtexTypeFields = map[string][]string{
|
||||
"article": {"journal", "note", "number", "pages", "volume"},
|
||||
"book": {"address", "editor", "isbn", "number", "pages", "publisher", "volume"},
|
||||
"booklet": {"address"},
|
||||
"inproceedings": {"address", "booktitle", "editor", "pages", "publisher", "series"},
|
||||
"manual": {"address"},
|
||||
"mastersthesis": {"address", "school", "type"},
|
||||
"misc": {"pages"},
|
||||
"phdthesis": {"address", "school", "type"},
|
||||
"proceedings": {"address", "booktitle", "editor", "pages", "publisher", "series"},
|
||||
"software": {"license", "version"},
|
||||
"techreport": {"address", "institution", "number"},
|
||||
"unpublished": {"note"},
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
bibtexEscaper *strings.Replacer
|
||||
keyLetters *strings.Replacer
|
||||
keyUnsafeChars *regexp.Regexp
|
||||
bibtexPattern *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
ret.bibtexEscaper = strings.NewReplacer("&", `\&`, "%", `\%`, "$", `\$`, "#", `\#`, "_", `\_`, "{", `\{`, "}", `\}`)
|
||||
ret.keyLetters = strings.NewReplacer(
|
||||
"Æ", "AE", "æ", "ae", "Ð", "D", "ð", "d", "Ø", "O", "ø", "o", "Þ", "Th", "þ", "th", "ß", "ss", "×", "x",
|
||||
"Đ", "D", "đ", "d", "Ħ", "H", "ħ", "h", "ı", "i", "IJ", "IJ", "ij", "ij", "ĸ", "k", "Ŀ", "L", "ŀ", "l",
|
||||
"Ł", "L", "ł", "l", "ʼn", "'n", "Ŋ", "NG", "ŋ", "ng", "Œ", "OE", "œ", "oe", "Ŧ", "T", "ŧ", "t",
|
||||
)
|
||||
ret.keyUnsafeChars = regexp.MustCompile(`[^a-zA-Z0-9-]+`)
|
||||
|
||||
// https://www.acm.org/publications/authors/bibtex-formatting
|
||||
ret.bibtexPattern = regexp.MustCompile(`(?m)^\s*@?\w+\s*{`) // a simple and quick check, no need to be strict
|
||||
return ret
|
||||
})
|
||||
|
||||
func keyToASCII() transform.Transformer {
|
||||
return transform.Chain(
|
||||
runes.Remove(runes.Predicate(func(char rune) bool {
|
||||
return char > unicode.MaxASCII && (char < 'À' || char > 'ž') && char != 'ệ'
|
||||
})),
|
||||
norm.NFD,
|
||||
runes.Remove(runes.Predicate(func(char rune) bool { return char > unicode.MaxASCII })),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *reference) formatBibTeX() string {
|
||||
place := r.Publisher
|
||||
if r.Type == "conference-paper" {
|
||||
place = r.Conference
|
||||
}
|
||||
editors := r.Editors
|
||||
if len(editors) == 0 {
|
||||
editors = r.EditorsSeries
|
||||
}
|
||||
bibtexEscaper := globalVars().bibtexEscaper
|
||||
typeFields := map[string]string{
|
||||
"address": joinNonEmpty(", ", place.City, place.Region, place.Country),
|
||||
"booktitle": bibtexEscaper.Replace(r.CollectionTitle),
|
||||
"editor": bibtexActors(editors),
|
||||
"institution": bibtexEscaper.Replace(r.institution()),
|
||||
"isbn": bibtexEscaper.Replace(r.ISBN),
|
||||
"journal": bibtexEscaper.Replace(r.Journal),
|
||||
"license": bibtexEscaper.Replace(string(r.License)),
|
||||
"note": statusNotes[r.Status],
|
||||
"number": r.Issue,
|
||||
"pages": r.pages("--"),
|
||||
"publisher": bibtexEscaper.Replace(r.Publisher.Name),
|
||||
"school": bibtexEscaper.Replace(r.institution()),
|
||||
"series": bibtexEscaper.Replace(r.Conference.Name),
|
||||
"type": r.ThesisType,
|
||||
"version": bibtexEscaper.Replace(r.Version),
|
||||
"volume": bibtexEscaper.Replace(r.Volume),
|
||||
}
|
||||
entryType := bibtexType(r.Type)
|
||||
fields := map[string]string{
|
||||
"author": bibtexActors(r.Authors),
|
||||
"title": "{" + bibtexEscaper.Replace(r.Title) + "}",
|
||||
"doi": bibtexEscaper.Replace(r.DOI),
|
||||
}
|
||||
for _, name := range bibtexTypeFields[entryType] {
|
||||
fields[name] = typeFields[name]
|
||||
}
|
||||
month, year := r.monthAndYear()
|
||||
if num, _ := strconv.Atoi(month); num >= 1 && num <= 12 {
|
||||
fields["month"] = strings.ToLower(time.Month(num).String()[:3])
|
||||
}
|
||||
fields["year"] = year
|
||||
fields["url"] = cmp.Or(r.RepositoryCode, r.URL)
|
||||
fields["note"] = cmp.Or(fields["note"], r.Notes)
|
||||
maps.DeleteFunc(fields, func(_, value string) bool { return value == "" })
|
||||
|
||||
lines := []string{bibtexKey(fields)}
|
||||
for _, name := range slices.Sorted(maps.Keys(fields)) {
|
||||
value := fields[name]
|
||||
if name != "month" {
|
||||
value = "{" + value + "}"
|
||||
}
|
||||
lines = append(lines, name+" = "+value)
|
||||
}
|
||||
return "@" + entryType + "{" + strings.Join(lines, ",\n") + "\n}"
|
||||
}
|
||||
|
||||
func bibtexType(cffType string) string {
|
||||
if cffType == "" || strings.Contains(cffType, "software") {
|
||||
return "software"
|
||||
}
|
||||
switch cffType {
|
||||
case "article", "book", "manual", "unpublished", "phdthesis", "mastersthesis":
|
||||
return cffType
|
||||
case "conference", "proceedings":
|
||||
return "proceedings"
|
||||
case "conference-paper":
|
||||
return "inproceedings"
|
||||
case "magazine-article", "newspaper-article":
|
||||
return "article"
|
||||
case "pamphlet":
|
||||
return "booklet"
|
||||
case "report":
|
||||
return "techreport"
|
||||
}
|
||||
return "misc"
|
||||
}
|
||||
|
||||
func bibtexActors(actors []actor) string {
|
||||
bibtexEscaper := globalVars().bibtexEscaper
|
||||
names := make([]string, 0, len(actors))
|
||||
for _, entry := range actors {
|
||||
switch {
|
||||
case entry.Name != "":
|
||||
names = append(names, "{"+bibtexEscaper.Replace(entry.Name)+"}")
|
||||
case entry.FamilyNames == "" && entry.GivenNames == "":
|
||||
names = append(names, bibtexEscaper.Replace(entry.Alias))
|
||||
default:
|
||||
family := entry.FamilyNames
|
||||
if entry.NameParticle != "" {
|
||||
family = entry.NameParticle + " " + family
|
||||
}
|
||||
names = append(names, joinNonEmpty(", ", family, entry.NameSuffix, entry.GivenNames))
|
||||
}
|
||||
}
|
||||
return strings.Join(names, " and ")
|
||||
}
|
||||
|
||||
func bibtexKey(fields map[string]string) string {
|
||||
author, _, _ := strings.Cut(fields["author"], ",")
|
||||
titleWords := splitWords(fields["title"])
|
||||
key := joinNonEmpty("_", author, strings.Join(titleWords[:min(3, len(titleWords))], "_"), fields["year"])
|
||||
key, _, _ = transform.String(keyToASCII(), globalVars().keyLetters.Replace(key))
|
||||
return strings.Trim(globalVars().keyUnsafeChars.ReplaceAllString(key, "_"), "_")
|
||||
}
|
||||
|
||||
func IsLikelyBibTeX(content string) bool {
|
||||
return globalVars().bibtexPattern.MatchString(content)
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package citation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFormatCFF(t *testing.T) {
|
||||
cases := []struct{ cff, apa, bibtex string }{
|
||||
{
|
||||
cff: `title: Overridden
|
||||
message: &mit MIT
|
||||
authors: [{family-names: Haines, given-names: Robert}, {name: "The {curly_braces} Collective"}]
|
||||
title: "Software that uses the following symbols: &, %, $, #"
|
||||
version: 2024-01-16
|
||||
license: [*mit, Apache-2.0]
|
||||
date-released: 2024-01-16
|
||||
`,
|
||||
apa: "Haines, R., & The {curly_braces} Collective. (2024). Software that uses the following symbols: &, %, $, # (Version 2024-01-16) [Computer software]",
|
||||
bibtex: `@software{Haines_Software_that_uses_2024,
|
||||
author = {Haines, Robert and {The \{curly\_braces\} Collective}},
|
||||
license = {["MIT", "Apache-2.0"]},
|
||||
month = jan,
|
||||
title = {{Software that uses the following symbols: \&, \%, \$, \#}},
|
||||
version = {2024-01-16},
|
||||
year = {2024}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
cff: `authors:
|
||||
- family-names: Smith
|
||||
given-names: Arfon M.
|
||||
title: "Software citation principles"
|
||||
license: MIT
|
||||
preferred-citation:
|
||||
authors:
|
||||
- family-names: Smith
|
||||
given-names: A. M.
|
||||
- name: "FORCE11 Software Citation Working Group"
|
||||
doi: "10.7717/peerj-cs.86"
|
||||
journal: "PeerJ Computer Science"
|
||||
month: 9
|
||||
start: e86
|
||||
title: "Software citation principles"
|
||||
type: article
|
||||
volume: 2
|
||||
issue: 123
|
||||
year: 2016
|
||||
publisher:
|
||||
- name: The Open Journal
|
||||
`,
|
||||
apa: "Smith, A. M., & FORCE11 Software Citation Working Group. (2016). Software citation principles. PeerJ Computer Science, 2(123), e86. https://doi.org/10.7717/peerj-cs.86",
|
||||
bibtex: `@article{Smith_Software_citation_principles_2016,
|
||||
author = {Smith, A. M. and {FORCE11 Software Citation Working Group}},
|
||||
doi = {10.7717/peerj-cs.86},
|
||||
journal = {PeerJ Computer Science},
|
||||
month = sep,
|
||||
number = {123},
|
||||
pages = {e86},
|
||||
title = {{Software citation principles}},
|
||||
volume = {2},
|
||||
year = {2016}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
cff: `preferred-citation:
|
||||
type: conference-paper
|
||||
version: 1.10
|
||||
title: "Über tools"
|
||||
authors:
|
||||
- family-names: Ørsted
|
||||
given-names: ǰan christian
|
||||
name-particle: van
|
||||
name-suffix: Jr.
|
||||
collection-title: "Proceedings of X & Y"
|
||||
conference:
|
||||
name: "Conf_2020"
|
||||
city: Berlin
|
||||
country: DE
|
||||
date-start: 30.06.2020
|
||||
date-end: 2020-07-02
|
||||
start: 10
|
||||
end: 20
|
||||
editors:
|
||||
- affiliation: Press
|
||||
editors-series:
|
||||
- family-names: Editor
|
||||
given-names: Eve
|
||||
`,
|
||||
apa: "van Ørsted, J̌. C., Jr. (2020, June 30–July 2). Über tools (Version 1.10) [Conference paper]. Proceedings of X & Y, 10–20",
|
||||
bibtex: `@inproceedings{van_Orsted_Uber_tools_2020,
|
||||
address = {Berlin, DE},
|
||||
author = {van Ørsted, Jr., ǰan christian},
|
||||
booktitle = {Proceedings of X \& Y},
|
||||
month = jun,
|
||||
pages = {10--20},
|
||||
series = {Conf\_2020},
|
||||
title = {{Über tools}},
|
||||
year = {2020}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
cff: `thesis: &thesis {type: phdthesis, title: Thesis}
|
||||
preferred-citation:
|
||||
<<: *thesis
|
||||
authors:
|
||||
- family-names: Nguyễn
|
||||
given-names: Sam
|
||||
affiliation: "Uni_A"
|
||||
institution: {city: Hanoi}
|
||||
license: &loop [*loop]
|
||||
status: in-press
|
||||
notes: A note
|
||||
`,
|
||||
apa: "Nguyễn, S. (in press). Thesis. [Doctoral dissertation, ]",
|
||||
bibtex: `@phdthesis{Nguyn_Thesis_in_press,
|
||||
author = {Nguyễn, Sam},
|
||||
note = {A note},
|
||||
title = {{Thesis}},
|
||||
year = {in press}
|
||||
}`,
|
||||
},
|
||||
{cff: "title: No authors\nauthor:\n - name: Typo\n"},
|
||||
{cff: "title: T\nauthors: [{name: A}]\nmessage: " + strings.Repeat("x", MaxContentSize)},
|
||||
{cff: "title: T\nauthors: [{name: A}]\nmessage: &s " + strings.Repeat("x", maxAliasExpansion/2) + "\nlicense: [*s, *s]\n"},
|
||||
{cff: "%TAG !e! tag:example.com,2000:\n---\ntitle: T\nauthors: [{name: A}]\n"},
|
||||
{cff: "preferred-citation: &m {title: T, name: A, authors: [*m]}\n"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
apa, bibtex := FormatCFF(tc.cff)
|
||||
assert.Equal(t, tc.apa, apa)
|
||||
assert.Equal(t, tc.bibtex, bibtex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLikelyBibTeX(t *testing.T) {
|
||||
assert.True(t, IsLikelyBibTeX("@article{key, title={Title}}"))
|
||||
assert.True(t, IsLikelyBibTeX("Inproceedings\n{\n}\n"))
|
||||
assert.True(t, IsLikelyBibTeX("% comment\n\n@misc{key}\n% comment\n"))
|
||||
assert.False(t, IsLikelyBibTeX("not bib {}"))
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package consts
|
||||
|
||||
import "runtime"
|
||||
|
||||
const IsWindows = runtime.GOOS == "windows"
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/consts"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"github.com/mholt/archives"
|
||||
@@ -184,7 +184,7 @@ func (dumper *Dumper) Close() error {
|
||||
|
||||
func (dumper *Dumper) normalizeFilePath(absPath string) string {
|
||||
absPath = filepath.Clean(absPath)
|
||||
if consts.IsWindows {
|
||||
if setting.IsWindows {
|
||||
absPath = strings.ToLower(absPath)
|
||||
}
|
||||
return absPath
|
||||
|
||||
@@ -15,6 +15,11 @@ func TestReadingBlameOutputSha256(t *testing.T) {
|
||||
setting.AppDataPath = t.TempDir()
|
||||
ctx := t.Context()
|
||||
|
||||
if DefaultFeatures().UsingGogit {
|
||||
t.Skip("Skipping test since gogit does not support sha256")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
|
||||
storage := mockRepository("repo5_pulls_sha256")
|
||||
repo, err := OpenRepository(ctx, storage)
|
||||
|
||||
+2
-96
@@ -12,10 +12,11 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// This file contains common functions between the gogit and !gogit variants for git Blobs
|
||||
|
||||
// Name returns name of the tree entry this blob object was created from (or empty string)
|
||||
func (b *Blob) Name() string {
|
||||
return b.name
|
||||
@@ -113,98 +114,3 @@ loop:
|
||||
_ = encoder.Close()
|
||||
return base64buf.String(), nil
|
||||
}
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID ObjectID
|
||||
|
||||
gotSize bool
|
||||
size int64
|
||||
name string
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||
// Calling the Close function on the result will discard all unread output.
|
||||
func (b *Blob) DataAsync(ctx context.Context) (_ io.ReadCloser, retErr error) {
|
||||
batch, cancel, err := b.repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
// if there was an error, cancel the batch right away,
|
||||
// otherwise let the caller close it
|
||||
if retErr != nil {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
info, contentReader, err := batch.QueryContent(b.ID.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.gotSize = true
|
||||
b.size = info.Size
|
||||
return &blobReader{
|
||||
rd: contentReader,
|
||||
n: info.Size,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Size returns the uncompressed size of the blob
|
||||
func (b *Blob) Size(ctx context.Context) int64 {
|
||||
if b.gotSize {
|
||||
return b.size
|
||||
}
|
||||
|
||||
batch, cancel, err := b.repo.CatFileBatch()
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
|
||||
return 0
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(b.ID.String())
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
|
||||
return 0
|
||||
}
|
||||
b.gotSize = true
|
||||
b.size = info.Size
|
||||
return b.size
|
||||
}
|
||||
|
||||
type blobReader struct {
|
||||
rd BufferedReader
|
||||
n int64
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func (b *blobReader) Read(p []byte) (n int, err error) {
|
||||
if b.n <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if int64(len(p)) > b.n {
|
||||
p = p[0:b.n]
|
||||
}
|
||||
n, err = b.rd.Read(p)
|
||||
b.n -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close implements io.Closer
|
||||
func (b *blobReader) Close() error {
|
||||
if b.rd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer b.cancel()
|
||||
|
||||
if err := DiscardFull(b.rd, b.n+1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.rd = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID ObjectID
|
||||
repo *Repository
|
||||
name string
|
||||
}
|
||||
|
||||
func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
|
||||
return b.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(b.ID.RawValue()))
|
||||
}
|
||||
|
||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||
// Calling the Close function on the result will discard all unread output.
|
||||
func (b *Blob) DataAsync(_ context.Context) (io.ReadCloser, error) {
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.Reader()
|
||||
}
|
||||
|
||||
// Size returns the uncompressed size of the blob
|
||||
func (b *Blob) Size(_ context.Context) int64 {
|
||||
obj, err := b.gogitEncodedObj()
|
||||
if err != nil {
|
||||
log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err)
|
||||
return 0
|
||||
}
|
||||
return obj.Size()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID ObjectID
|
||||
|
||||
gotSize bool
|
||||
size int64
|
||||
name string
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||
// Calling the Close function on the result will discard all unread output.
|
||||
func (b *Blob) DataAsync(ctx context.Context) (_ io.ReadCloser, retErr error) {
|
||||
batch, cancel, err := b.repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
// if there was an error, cancel the batch right away,
|
||||
// otherwise let the caller close it
|
||||
if retErr != nil {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
info, contentReader, err := batch.QueryContent(b.ID.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.gotSize = true
|
||||
b.size = info.Size
|
||||
return &blobReader{
|
||||
rd: contentReader,
|
||||
n: info.Size,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Size returns the uncompressed size of the blob
|
||||
func (b *Blob) Size(ctx context.Context) int64 {
|
||||
if b.gotSize {
|
||||
return b.size
|
||||
}
|
||||
|
||||
batch, cancel, err := b.repo.CatFileBatch()
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
|
||||
return 0
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(b.ID.String())
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.LogString(), err)
|
||||
return 0
|
||||
}
|
||||
b.gotSize = true
|
||||
b.size = info.Size
|
||||
return b.size
|
||||
}
|
||||
|
||||
type blobReader struct {
|
||||
rd BufferedReader
|
||||
n int64
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func (b *blobReader) Read(p []byte) (n int, err error) {
|
||||
if b.n <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if int64(len(p)) > b.n {
|
||||
p = p[0:b.n]
|
||||
}
|
||||
n, err = b.rd.Read(p)
|
||||
b.n -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close implements io.Closer
|
||||
func (b *blobReader) Close() error {
|
||||
if b.rd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer b.cancel()
|
||||
|
||||
if err := DiscardFull(b.rd, b.n+1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.rd = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
func convertPGPSignature(c *object.Commit) *CommitSignature {
|
||||
if c.PGPSignature == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var w strings.Builder
|
||||
var err error
|
||||
|
||||
if _, err = fmt.Fprintf(&w, "tree %s\n", c.TreeHash.String()); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, parent := range c.ParentHashes {
|
||||
if _, err = fmt.Fprintf(&w, "parent %s\n", parent.String()); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprint(&w, "author "); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = c.Author.Encode(&w); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprint(&w, "\ncommitter "); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = c.Committer.Encode(&w); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Encoding != "" && c.Encoding != "UTF-8" {
|
||||
if _, err = fmt.Fprintf(&w, "\nencoding %s\n", c.Encoding); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &CommitSignature{
|
||||
Signature: c.PGPSignature,
|
||||
Payload: w.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertCommit(c *object.Commit) *Commit {
|
||||
return &Commit{
|
||||
ID: ParseGogitHash(c.Hash),
|
||||
TreeID: ParseGogitHash(c.TreeHash),
|
||||
CommitMessage: CommitMessage{MessageRaw: c.Message},
|
||||
Committer: &c.Committer,
|
||||
Author: &c.Author,
|
||||
Signature: convertPGPSignature(c),
|
||||
Parents: ParseGogitHashArray(c.ParentHashes),
|
||||
}
|
||||
}
|
||||
@@ -113,36 +113,3 @@ func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string
|
||||
|
||||
return results, unHitEntryPaths, nil
|
||||
}
|
||||
|
||||
// GetLastCommitForPaths returns last commit information
|
||||
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
|
||||
// We read backwards from the commit to obtain all of the commits
|
||||
revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commitsMap := map[string]*Commit{}
|
||||
commitsMap[commit.ID.String()] = commit
|
||||
|
||||
commitCommits := map[string]*Commit{}
|
||||
for path, commitID := range revs {
|
||||
if len(commitID) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
c, ok := commitsMap[commitID]
|
||||
if ok {
|
||||
commitCommits[path] = c
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := gitRepo.GetCommit(ctx, commitID) // Ensure the commit exists in the repository
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commitCommits[path] = c
|
||||
}
|
||||
|
||||
return commitCommits, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
|
||||
"github.com/emirpasic/gods/trees/binaryheap"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
||||
)
|
||||
|
||||
type commitAndPaths struct {
|
||||
commit cgobject.CommitNode
|
||||
// Paths that are still on the branch represented by commit
|
||||
paths []string
|
||||
// Set of hashes for the paths
|
||||
hashes map[string]plumbing.Hash
|
||||
}
|
||||
|
||||
func getCommitTree(c cgobject.CommitNode, treePath string) (*object.Tree, error) {
|
||||
tree, err := c.Tree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Optimize deep traversals by focusing only on the specific tree
|
||||
if treePath != "" {
|
||||
tree, err = tree.Tree(treePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
|
||||
tree, err := getCommitTree(c, treePath)
|
||||
if err == object.ErrDirectoryNotFound {
|
||||
// The whole tree didn't exist, so return empty map
|
||||
return make(map[string]plumbing.Hash), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hashes := make(map[string]plumbing.Hash)
|
||||
for _, path := range paths {
|
||||
if path != "" {
|
||||
entry, err := tree.FindEntry(path)
|
||||
if err == nil {
|
||||
hashes[path] = entry.Hash
|
||||
}
|
||||
} else {
|
||||
hashes[path] = tree.Hash
|
||||
}
|
||||
}
|
||||
|
||||
return hashes, nil
|
||||
}
|
||||
|
||||
// GetLastCommitForPaths returns last commit information
|
||||
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
|
||||
commitNodeIndex, closer := gitRepo.CommitNodeIndex()
|
||||
defer closer()
|
||||
|
||||
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getLastCommitForPathsByCommitNode(ctx, gitRepo, c, treePath, paths)
|
||||
}
|
||||
|
||||
func getLastCommitForPathsByCommitNode(ctx context.Context, gitRepo *Repository, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) {
|
||||
refSha := c.ID().String()
|
||||
|
||||
// We do a tree traversal with nodes sorted by commit time
|
||||
heap := binaryheap.NewWith(func(a, b any) int {
|
||||
if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) { //nolint:forcetypeassert // this heap only ever holds *commitAndPaths
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
})
|
||||
|
||||
resultNodes := make(map[string]cgobject.CommitNode)
|
||||
initialHashes, err := getFileHashes(c, treePath, paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start search from the root commit and with full set of paths
|
||||
heap.Push(&commitAndPaths{c, paths, initialHashes})
|
||||
heaploop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
break heaploop
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
cIn, ok := heap.Pop()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
current := cIn.(*commitAndPaths) //nolint:forcetypeassert // this heap only ever holds *commitAndPaths
|
||||
|
||||
// Load the parent commits for the one we are currently examining
|
||||
numParents := current.commit.NumParents()
|
||||
var parents []cgobject.CommitNode
|
||||
for i := range numParents {
|
||||
parent, err := current.commit.ParentNode(i)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
parents = append(parents, parent)
|
||||
}
|
||||
|
||||
// Examine the current commit and set of interesting paths
|
||||
pathUnchanged := make([]bool, len(current.paths))
|
||||
parentHashes := make([]map[string]plumbing.Hash, len(parents))
|
||||
for j, parent := range parents {
|
||||
parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
for i, path := range current.paths {
|
||||
if parentHashes[j][path] == current.hashes[path] {
|
||||
pathUnchanged[i] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var remainingPaths []string
|
||||
for i, pth := range current.paths {
|
||||
// The results could already contain some newer change for the same path,
|
||||
// so don't override that and bail out on the file early.
|
||||
if resultNodes[pth] == nil {
|
||||
if pathUnchanged[i] {
|
||||
// The path existed with the same hash in at least one parent so it could
|
||||
// not have been changed in this commit directly.
|
||||
remainingPaths = append(remainingPaths, pth)
|
||||
} else {
|
||||
// There are few possible cases how can we get here:
|
||||
// - The path didn't exist in any parent, so it must have been created by
|
||||
// this commit.
|
||||
// - The path did exist in the parent commit, but the hash of the file has
|
||||
// changed.
|
||||
// - We are looking at a merge commit and the hash of the file doesn't
|
||||
// match any of the hashes being merged. This is more common for directories,
|
||||
// but it can also happen if a file is changed through conflict resolution.
|
||||
resultNodes[pth] = current.commit
|
||||
if err := gitRepo.LastCommitCache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(remainingPaths) > 0 {
|
||||
// Add the parent nodes along with remaining paths to the heap for further
|
||||
// processing.
|
||||
for j, parent := range parents {
|
||||
// Combine remainingPath with paths available on the parent branch
|
||||
// and make union of them
|
||||
remainingPathsForParent := make([]string, 0, len(remainingPaths))
|
||||
newRemainingPaths := make([]string, 0, len(remainingPaths))
|
||||
for _, path := range remainingPaths {
|
||||
if parentHashes[j][path] == current.hashes[path] {
|
||||
remainingPathsForParent = append(remainingPathsForParent, path)
|
||||
} else {
|
||||
newRemainingPaths = append(newRemainingPaths, path)
|
||||
}
|
||||
}
|
||||
|
||||
if remainingPathsForParent != nil {
|
||||
heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
|
||||
}
|
||||
|
||||
if len(newRemainingPaths) == 0 {
|
||||
break
|
||||
}
|
||||
remainingPaths = newRemainingPaths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post-processing
|
||||
result := make(map[string]*Commit)
|
||||
for path, commitNode := range resultNodes {
|
||||
commit, err := commitNode.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[path] = convertCommit(commit)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// GetLastCommitForPaths returns last commit information
|
||||
func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) {
|
||||
// We read backwards from the commit to obtain all of the commits
|
||||
revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commitsMap := map[string]*Commit{}
|
||||
commitsMap[commit.ID.String()] = commit
|
||||
|
||||
commitCommits := map[string]*Commit{}
|
||||
for path, commitID := range revs {
|
||||
if len(commitID) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
c, ok := commitsMap[commitID]
|
||||
if ok {
|
||||
commitCommits[path] = c
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := gitRepo.GetCommit(ctx, commitID) // Ensure the commit exists in the repository
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commitCommits[path] = c
|
||||
}
|
||||
|
||||
return commitCommits, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
|
||||
repoPath, _ := filepath.Abs(filepath.Join(testReposDir, "repo1_bare"))
|
||||
repo, err := OpenRepository(t.Context(), gitrepo.RepositoryManaged("dummy", repoPath))
|
||||
require.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
commit, err := repo.GetCommit(t.Context(), "feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
|
||||
require.NoError(t, err)
|
||||
entries, err := commit.Tree().ListEntries(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
|
||||
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
|
||||
for _, info := range infos {
|
||||
nilCommits += util.Iif(info.Commit == nil, 1, 0)
|
||||
nonNilCommits += util.Iif(info.Commit != nil, 1, 0)
|
||||
}
|
||||
return nilCommits, nonNilCommits
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
|
||||
|
||||
walkGitLogDebugBeforeNext = cancel
|
||||
commitInfos, _, err := entries.GetCommitsInfo(ctx, time.Second, "/any/repo-link", repo, commit, "")
|
||||
assert.NoError(t, err)
|
||||
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
|
||||
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
|
||||
assert.Equal(t, 3, nilCommits)
|
||||
|
||||
walkGitLogDebugBeforeNext = nil
|
||||
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), time.Second, "/any/repo-link", repo, commit, "")
|
||||
assert.NoError(t, err)
|
||||
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
|
||||
assert.Equal(t, 3, nonNilCommits)
|
||||
assert.Equal(t, 0, nilCommits)
|
||||
}
|
||||
@@ -4,14 +4,11 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -167,40 +164,3 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
|
||||
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) {
|
||||
repoPath, _ := filepath.Abs(filepath.Join(testReposDir, "repo1_bare"))
|
||||
repo, err := OpenRepository(t.Context(), gitrepo.RepositoryManaged("dummy", repoPath))
|
||||
require.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
commit, err := repo.GetCommit(t.Context(), "feaf4ba6bc635fec442f46ddd4512416ec43c2c2")
|
||||
require.NoError(t, err)
|
||||
entries, err := commit.Tree().ListEntries(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
|
||||
countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) {
|
||||
for _, info := range infos {
|
||||
nilCommits += util.Iif(info.Commit == nil, 1, 0)
|
||||
nonNilCommits += util.Iif(info.Commit != nil, 1, 0)
|
||||
}
|
||||
return nilCommits, nonNilCommits
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer test.MockVariableValue(&walkGitLogDebugBeforeNext)()
|
||||
|
||||
walkGitLogDebugBeforeNext = cancel
|
||||
commitInfos, _, err := entries.GetCommitsInfo(ctx, time.Second, "/any/repo-link", repo, commit, "")
|
||||
assert.NoError(t, err)
|
||||
nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos)
|
||||
assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context
|
||||
assert.Equal(t, 3, nilCommits)
|
||||
|
||||
walkGitLogDebugBeforeNext = nil
|
||||
commitInfos, _, err = entries.GetCommitsInfo(t.Context(), time.Second, "/any/repo-link", repo, commit, "")
|
||||
assert.NoError(t, err)
|
||||
nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos)
|
||||
assert.Equal(t, 3, nonNilCommits)
|
||||
assert.Equal(t, 0, nilCommits)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
|
||||
+11
-5
@@ -8,9 +8,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/consts"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
@@ -73,9 +73,15 @@ func syncGitConfig(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// set support for AGit flow
|
||||
if err := configAddNonExist(ctx, "receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
if DefaultFeatures().SupportProcReceive {
|
||||
// set support for AGit flow
|
||||
if err := configAddNonExist(ctx, "receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := configUnsetAll(ctx, "receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user.
|
||||
@@ -90,7 +96,7 @@ func syncGitConfig(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if consts.IsWindows {
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := configSet(ctx, "core.longpaths", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func ForceFastImportWithInit(ctx context.Context, repoLocalPath string, commits
|
||||
dirEntries, err := os.ReadDir(repoLocalPath)
|
||||
if os.IsNotExist(err) || (err == nil && len(dirEntries) == 0) {
|
||||
_ = os.MkdirAll(repoLocalPath, 0o755)
|
||||
err := InitRepositoryLocal(ctx, repoLocalPath, initOpt.Bare, util.IfZero(initOpt.ObjectFormat, "sha1"), "")
|
||||
err := InitRepositoryLocal(ctx, repoLocalPath, initOpt.Bare, util.IfZero(initOpt.ObjectFormat, "sha1"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+15
-18
@@ -10,23 +10,20 @@ import (
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
)
|
||||
|
||||
// FetchRemoteTempCommit fetches a specific commit and its related objects from a remote repository
|
||||
// into the managed repository for temporary use (e.g.: compare, check merge base, etc.).
|
||||
// FetchRemoteCommit fetches a specific commit and its related objects from a remote
|
||||
// repository into the managed repository.
|
||||
//
|
||||
// If no reference (branch, tag, or other ref) points to the fetched commit, it will be treated as unreachable
|
||||
// and cleaned up by auto-gc (a future push) or Gitea's "git gc" cronjob after the default prune expiration period.
|
||||
// Ref: https://www.kernel.org/pub/software/scm/git/docs/git-gc.html
|
||||
func FetchRemoteTempCommit(ctx context.Context, repo, remoteRepo RepositoryFacade, commitID string) error {
|
||||
// since it is for temporary use, we don't need to update any ref or lock anything or maintain anything
|
||||
return fetchRemoteCommand(remoteRepo, commitID).WithRepo(repo).Run(ctx)
|
||||
}
|
||||
|
||||
// FetchRemoteCommitUpdateRef fetches a commit from a remote repository and points refName at it
|
||||
func FetchRemoteCommitUpdateRef(ctx context.Context, repo, remoteRepo RepositoryFacade, commitish, refName string) error {
|
||||
return fetchRemoteCommand(remoteRepo, "+"+commitish+":"+refName).WithRepo(repo).RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
func fetchRemoteCommand(remoteRepo RepositoryFacade, refspec string) *gitcmd.Command {
|
||||
return gitcmd.NewCommand("fetch", "--no-tags", "--no-write-commit-graph", "--no-write-fetch-head", "--no-auto-maintenance").
|
||||
AddDynamicArguments(gitrepo.RepoLocalPath(remoteRepo), refspec)
|
||||
// If no reference (branch, tag, or other ref) points to the fetched commit, it will
|
||||
// be treated as unreachable and cleaned up by `git gc` after the default prune
|
||||
// expiration period (2 weeks). Ref: https://www.kernel.org/pub/software/scm/git/docs/git-gc.html
|
||||
//
|
||||
// This behavior is sufficient for temporary operations, such as determining the
|
||||
// merge base between commits.
|
||||
func FetchRemoteCommit(ctx context.Context, repo, remoteRepo RepositoryFacade, commitID string) error {
|
||||
return LockWriteAndDo(ctx, repo, func(ctx context.Context) error {
|
||||
return gitcmd.NewCommand("fetch", "--no-tags").
|
||||
AddDynamicArguments(gitrepo.RepoLocalPath(remoteRepo)).
|
||||
AddDynamicArguments(commitID).
|
||||
WithRepo(repo).Run(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
+19
-3
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/cache"
|
||||
@@ -23,11 +24,13 @@ import (
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
const RequiredVersion = "2.34.0" // the minimum Git version required
|
||||
const RequiredVersion = "2.25.0" // the minimum Git version required
|
||||
|
||||
type Features struct {
|
||||
gitVersion *version.Version
|
||||
|
||||
UsingGogit bool
|
||||
SupportProcReceive bool // >= 2.29
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
SupportedObjectFormats []ObjectFormat // sha1, sha256
|
||||
SupportCheckAttrOnBare bool // >= 2.40
|
||||
@@ -76,8 +79,9 @@ func loadGitVersionFeatures() (*Features, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
features := &Features{gitVersion: ver}
|
||||
features.SupportHashSha256 = features.CheckVersionAtLeast("2.42")
|
||||
features := &Features{gitVersion: ver, UsingGogit: isGogit}
|
||||
features.SupportProcReceive = features.CheckVersionAtLeast("2.29")
|
||||
features.SupportHashSha256 = features.CheckVersionAtLeast("2.42") && !isGogit
|
||||
features.SupportedObjectFormats = []ObjectFormat{Sha1ObjectFormat}
|
||||
if features.SupportHashSha256 {
|
||||
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
|
||||
@@ -125,8 +129,16 @@ func checkGitVersionCompatibility(gitVer *version.Version) error {
|
||||
func ensureGitVersion() error {
|
||||
if !DefaultFeatures().CheckVersionAtLeast(RequiredVersion) {
|
||||
moreHint := "get git: https://git-scm.com/downloads"
|
||||
if runtime.GOOS == "linux" {
|
||||
// there are a lot of CentOS/RHEL users using old git, so we add a special hint for them
|
||||
if _, err := os.Stat("/etc/redhat-release"); err == nil {
|
||||
// ius.io is the recommended official(git-scm.com) method to install git
|
||||
moreHint = "get git: https://git-scm.com/downloads/linux and https://ius.io"
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", DefaultFeatures().gitVersion.Original(), RequiredVersion, moreHint)
|
||||
}
|
||||
|
||||
if err := checkGitVersionCompatibility(DefaultFeatures().gitVersion); err != nil {
|
||||
return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", DefaultFeatures().gitVersion.String(), err)
|
||||
}
|
||||
@@ -197,3 +209,7 @@ func runGitTests(m interface{ Run() int }) int {
|
||||
func LockConfigAndDo(ctx context.Context, repo RepositoryFacade, fn func(ctx context.Context) error) error {
|
||||
return globallock.LockAndDo(ctx, "repo-config:"+repo.GitRepoManagedID(), fn)
|
||||
}
|
||||
|
||||
func LockWriteAndDo(ctx context.Context, repo RepositoryFacade, fn func(ctx context.Context) error) error {
|
||||
return globallock.LockAndDo(ctx, "repo-write:"+repo.GitRepoManagedID(), fn)
|
||||
}
|
||||
|
||||
@@ -94,7 +94,8 @@ const (
|
||||
StderrBadRevision StderrPrefix = "fatal: bad revision"
|
||||
StderrNoSuchPath StderrPrefix = "fatal: no such path"
|
||||
|
||||
StderrNoSuchRemote StderrPrefix = "error: no such remote"
|
||||
StderrNoSuchRemote1 StderrPrefix = "fatal: no such remote" // git < 2.30, exit status 128
|
||||
StderrNoSuchRemote2 StderrPrefix = "error: no such remote" // git >= 2.30. exit status 2
|
||||
|
||||
StderrAuthenticationFailed StderrPrefix = "fatal: Authentication failed for"
|
||||
StderrCouldNotReadUsername StderrPrefix = "fatal: could not read Username"
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"gitea.dev/modules/consts"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
@@ -156,7 +156,7 @@ func createDelegateHooks(hookDir string) (err error) {
|
||||
|
||||
func checkExecutable(filename string) bool {
|
||||
// windows has no concept of a executable bit
|
||||
if consts.IsWindows {
|
||||
if runtime.GOOS == "windows" {
|
||||
return true
|
||||
}
|
||||
fileInfo, err := os.Stat(filename)
|
||||
|
||||
@@ -25,20 +25,14 @@ type LastCommitCache struct {
|
||||
|
||||
// Put puts the last commit id with commit and entry path
|
||||
func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
|
||||
if c.ttlFn() < 0 {
|
||||
return nil
|
||||
}
|
||||
log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
|
||||
return c.cache.Put(getCacheKey(c.repo, ref, entryPath), commitID, c.ttlFn())
|
||||
}
|
||||
|
||||
// Get gets the last commit information by commit id and entry path
|
||||
func (c *LastCommitCache) Get(ctx context.Context, ref, entryPath string) (*Commit, error) {
|
||||
var lastCommitID string
|
||||
if c.ttlFn() >= 0 {
|
||||
lastCommitID, _ = c.cache.Get(getCacheKey(c.repo, ref, entryPath))
|
||||
}
|
||||
if lastCommitID == "" {
|
||||
lastCommitID, ok := c.cache.Get(getCacheKey(c.repo, ref, entryPath))
|
||||
if !ok || lastCommitID == "" {
|
||||
return nil, nil //nolint:nilnil // return nil when cache miss
|
||||
}
|
||||
|
||||
@@ -78,43 +72,3 @@ func (c *LastCommitCache) GetCommitByPath(ctx context.Context, entryCommitID Obj
|
||||
|
||||
return lastCommit, nil
|
||||
}
|
||||
|
||||
// CacheCommit will cache the commit from the gitRepository
|
||||
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
|
||||
return c.recursiveCache(ctx, gitRepo, c.Tree(), "", 1)
|
||||
}
|
||||
|
||||
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, tree *Tree, treePath string, level int) error {
|
||||
if level == 0 {
|
||||
return nil
|
||||
}
|
||||
entries, err := tree.ListEntries(ctx, gitRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entryPaths := make([]string, len(entries))
|
||||
for i, entry := range entries {
|
||||
entryPaths[i] = entry.Name()
|
||||
}
|
||||
|
||||
_, err = walkGitLog(ctx, gitRepo, c, treePath, entryPaths...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, treeEntry := range entries {
|
||||
// entryMap won't contain "" therefore skip this.
|
||||
if treeEntry.IsDir() {
|
||||
subTree, err := tree.SubTree(ctx, gitRepo, treeEntry.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.recursiveCache(ctx, gitRepo, subTree, treeEntry.Name(), level-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
||||
)
|
||||
|
||||
// CacheCommit will cache the commit from the gitRepository
|
||||
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
|
||||
commitNodeIndex, closer := gitRepo.CommitNodeIndex()
|
||||
defer closer()
|
||||
|
||||
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.recursiveCache(ctx, gitRepo, index, c.Tree(), "", 1)
|
||||
}
|
||||
|
||||
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, index cgobject.CommitNode, tree *Tree, treePath string, level int) error {
|
||||
if level == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries, err := tree.ListEntries(ctx, gitRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entryPaths := make([]string, len(entries))
|
||||
entryMap := make(map[string]*TreeEntry)
|
||||
for i, entry := range entries {
|
||||
entryPaths[i] = entry.Name()
|
||||
entryMap[entry.Name()] = entry
|
||||
}
|
||||
|
||||
commits, err := getLastCommitForPathsByCommitNode(ctx, gitRepo, index, treePath, entryPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for entry := range commits {
|
||||
if entryMap[entry].IsDir() {
|
||||
subTree, err := tree.SubTree(ctx, gitRepo, entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.recursiveCache(ctx, gitRepo, index, subTree, entry, level-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// CacheCommit will cache the commit from the gitRepository
|
||||
func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error {
|
||||
return c.recursiveCache(ctx, gitRepo, c.Tree(), "", 1)
|
||||
}
|
||||
|
||||
func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, tree *Tree, treePath string, level int) error {
|
||||
if level == 0 {
|
||||
return nil
|
||||
}
|
||||
entries, err := tree.ListEntries(ctx, gitRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entryPaths := make([]string, len(entries))
|
||||
for i, entry := range entries {
|
||||
entryPaths[i] = entry.Name()
|
||||
}
|
||||
|
||||
_, err = walkGitLog(ctx, gitRepo, c, treePath, entryPaths...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, treeEntry := range entries {
|
||||
// entryMap won't contain "" therefore skip this.
|
||||
if treeEntry.IsDir() {
|
||||
subTree, err := tree.SubTree(ctx, gitRepo, treeEntry.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.recursiveCache(ctx, gitRepo, subTree, treeEntry.Name(), level-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -38,8 +38,8 @@ func RenameRepository(ctx context.Context, repo, newRepo RepositoryFacade) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitRepository(ctx context.Context, repo RepositoryFacade, objectFormatName, initialBranch string) error {
|
||||
return InitRepositoryLocal(ctx, gitrepo.RepoLocalPath(repo), true, objectFormatName, initialBranch)
|
||||
func InitRepository(ctx context.Context, repo RepositoryFacade, objectFormatName string) error {
|
||||
return InitRepositoryLocal(ctx, gitrepo.RepoLocalPath(repo), true, objectFormatName)
|
||||
}
|
||||
|
||||
func IsRepoFileExist(ctx context.Context, repo RepositoryFacade, relativeFilePath string) (bool, error) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
@@ -21,17 +23,26 @@ import (
|
||||
// logNameStatusRepo opens git log --raw in the provided repo and returns a parser
|
||||
func logNameStatusRepo(ctx context.Context, repo RepositoryFacade, head, treepath string, paths ...string) *logNameStatusRepoParser {
|
||||
cmd := gitcmd.NewCommand()
|
||||
cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "--no-follow", "-t", "-z").AddDynamicArguments(head)
|
||||
cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head)
|
||||
|
||||
var files []string
|
||||
if treepath != "" {
|
||||
files = append(files, treepath) // matches all entries, adding them would not narrow the log
|
||||
} else if len(paths) < 70 {
|
||||
for _, pth := range paths {
|
||||
if pth != "" {
|
||||
files = append(files, pth)
|
||||
if len(paths) < 70 {
|
||||
if treepath != "" {
|
||||
files = append(files, treepath)
|
||||
for _, pth := range paths {
|
||||
if pth != "" {
|
||||
files = append(files, path.Join(treepath, pth))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, pth := range paths {
|
||||
if pth != "" {
|
||||
files = append(files, pth)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if treepath != "" {
|
||||
files = append(files, treepath)
|
||||
}
|
||||
// Use the :(literal) pathspec magic to handle edge cases with files named like ":file.txt" or "*.jpg"
|
||||
for i, file := range files {
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||
)
|
||||
|
||||
func ParseGogitHash(h plumbing.Hash) ObjectID {
|
||||
switch hash.Size {
|
||||
case 20:
|
||||
return Sha1ObjectFormat.MustID(h[:])
|
||||
case 32:
|
||||
return Sha256ObjectFormat.MustID(h[:])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseGogitHashArray(objectIDs []plumbing.Hash) []ObjectID {
|
||||
ret := make([]ObjectID, len(objectIDs))
|
||||
for i, h := range objectIDs {
|
||||
ret[i] = ParseGogitHash(h)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -43,6 +43,8 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
var _ = catBatchParseTreeEntries // bypass "unused" lint because it is only used by "nogogit"
|
||||
|
||||
func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd BufferedReader, sz int64) ([]*TreeEntry, error) {
|
||||
entries := make([]*TreeEntry, 0, 10)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func fillResultNameRev(ctx context.Context, repo git.RepositoryFacade, results []*LFSResult) error {
|
||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||
wg := errgroup.Group{}
|
||||
cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithRepo(repo)
|
||||
stdin, stdinClose := cmd.MakeStdinPipe()
|
||||
|
||||
@@ -66,7 +66,7 @@ func (err *ErrInvalidCloneAddr) Unwrap() error {
|
||||
|
||||
// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist.
|
||||
func IsRemoteNotExistError(err error) bool {
|
||||
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote)
|
||||
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote1, gitcmd.StderrNoSuchRemote2)
|
||||
}
|
||||
|
||||
// ParseRemoteAddr checks if given remote address is valid,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsRemoteNotExistError(t *testing.T) {
|
||||
_, err := GetRemoteAddress(t.Context(), mockRepository("repo1_bare"), "no-such-remote")
|
||||
assert.True(t, IsRemoteNotExistError(err))
|
||||
}
|
||||
+9
-5
@@ -118,7 +118,7 @@ func IsRepoURLAccessible(ctx context.Context, url string) bool {
|
||||
}
|
||||
|
||||
// InitRepositoryLocal initializes a new Git repository.
|
||||
func InitRepositoryLocal(ctx context.Context, localRepoPath string, bare bool, objectFormatName, initialBranch string) error {
|
||||
func InitRepositoryLocal(ctx context.Context, localRepoPath string, bare bool, objectFormatName string) error {
|
||||
err := os.MkdirAll(localRepoPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -132,9 +132,6 @@ func InitRepositoryLocal(ctx context.Context, localRepoPath string, bare bool, o
|
||||
if DefaultFeatures().SupportHashSha256 {
|
||||
cmd.AddOptionValues("--object-format", objectFormatName)
|
||||
}
|
||||
if initialBranch != "" {
|
||||
cmd.AddOptionValues("--initial-branch", initialBranch)
|
||||
}
|
||||
|
||||
if bare {
|
||||
cmd.AddArguments("--bare")
|
||||
@@ -226,6 +223,7 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
|
||||
// PushOptions options when push to remote
|
||||
type PushOptions struct {
|
||||
Remote string
|
||||
LocalRefName string
|
||||
Branch string
|
||||
Force bool
|
||||
ForceWithLease string
|
||||
@@ -247,7 +245,13 @@ func Push(ctx context.Context, localRepoPath string, opts PushOptions) error {
|
||||
}
|
||||
remoteBranchArgs := []string{opts.Remote}
|
||||
if len(opts.Branch) > 0 {
|
||||
remoteBranchArgs = append(remoteBranchArgs, opts.Branch)
|
||||
var refspec string
|
||||
if opts.LocalRefName != "" {
|
||||
refspec = fmt.Sprintf("%s:%s", opts.LocalRefName, opts.Branch)
|
||||
} else {
|
||||
refspec = opts.Branch
|
||||
}
|
||||
remoteBranchArgs = append(remoteBranchArgs, refspec)
|
||||
}
|
||||
cmd.AddDashesAndList(remoteBranchArgs...)
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/cache"
|
||||
"github.com/go-git/go-git/v5/storage/filesystem"
|
||||
"github.com/go-git/go-git/v5/storage/filesystem/dotgit"
|
||||
)
|
||||
|
||||
const isGogit = true
|
||||
|
||||
type Repository struct {
|
||||
RepositoryBase
|
||||
|
||||
gogitRepo *gogit.Repository
|
||||
gogitStorage *reindexingStorage
|
||||
}
|
||||
|
||||
// reindexingStorage reloads the pack index when git added or removed packs after go-git loaded it
|
||||
// https://github.com/go-git/go-git/issues/2439 https://github.com/go-git/go-git/issues/1623
|
||||
// FIXME: gogit workaround, remove with the gogit build
|
||||
type reindexingStorage struct {
|
||||
*filesystem.Storage
|
||||
packs []plumbing.Hash
|
||||
}
|
||||
|
||||
func isRepackError(err error) bool {
|
||||
return errors.Is(err, plumbing.ErrObjectNotFound) || errors.Is(err, dotgit.ErrPackfileNotFound) || errors.Is(err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
// retry reruns fn while a concurrent repack keeps changing the packs
|
||||
func (s *reindexingStorage) retry(fn func() error) error {
|
||||
for {
|
||||
err := fn()
|
||||
if !isRepackError(err) {
|
||||
return err
|
||||
}
|
||||
packs, _ := s.ObjectPacks()
|
||||
if slices.Equal(packs, s.packs) {
|
||||
return err
|
||||
}
|
||||
s.packs = packs
|
||||
s.Reindex()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *reindexingStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (obj plumbing.EncodedObject, err error) {
|
||||
err = s.retry(func() (err error) {
|
||||
obj, err = s.Storage.EncodedObject(t, h)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := obj.(*plumbing.MemoryObject); ok {
|
||||
return obj, nil
|
||||
}
|
||||
return &lazyObject{EncodedObject: obj, storage: s}, nil
|
||||
}
|
||||
|
||||
// lazyObject looks up a large object again when its file got removed before Reader reopened it
|
||||
// FIXME: gogit workaround, remove with the gogit build
|
||||
type lazyObject struct {
|
||||
plumbing.EncodedObject
|
||||
storage *reindexingStorage
|
||||
}
|
||||
|
||||
func (o *lazyObject) Reader() (rc io.ReadCloser, err error) {
|
||||
rc, err = o.EncodedObject.Reader()
|
||||
if !isRepackError(err) {
|
||||
return rc, err
|
||||
}
|
||||
err = o.storage.retry(func() error {
|
||||
obj, err := o.storage.Storage.EncodedObject(o.Type(), o.Hash())
|
||||
if err == nil {
|
||||
o.EncodedObject = obj
|
||||
rc, err = obj.Reader()
|
||||
}
|
||||
return err
|
||||
})
|
||||
return rc, err
|
||||
}
|
||||
|
||||
// packIdxFS lists packs like git, only while their .idx exists
|
||||
// FIXME: gogit workaround, remove with the gogit build
|
||||
type packIdxFS struct {
|
||||
billy.Filesystem
|
||||
}
|
||||
|
||||
func (f packIdxFS) ReadDir(dir string) ([]os.FileInfo, error) {
|
||||
if dir != f.Join("objects", "pack") {
|
||||
return f.Filesystem.ReadDir(dir)
|
||||
}
|
||||
dirFile, err := os.Open(filepath.Join(f.Root(), dir))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dirFile.Close()
|
||||
infos, err := dirFile.Readdir(-1) // skips files removed before their lstat, unlike billy's ReadDir
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := make(container.Set[string], len(infos))
|
||||
for _, info := range infos {
|
||||
names.Add(info.Name())
|
||||
}
|
||||
return slices.DeleteFunc(infos, func(info os.FileInfo) bool {
|
||||
base, isPack := strings.CutSuffix(info.Name(), ".pack")
|
||||
return isPack && !names.Contains(base+".idx")
|
||||
}), nil
|
||||
}
|
||||
|
||||
func openRepositoryInternal(gitRepo *Repository) error {
|
||||
repoPath := gitrepo.RepoLocalPath(gitRepo)
|
||||
fs := osfs.New(repoPath)
|
||||
_, err := fs.Stat(".git")
|
||||
if err == nil {
|
||||
fs, err = fs.Chroot(".git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// the "clone --shared" repo doesn't work well with go-git AlternativeFS, https://github.com/go-git/go-git/issues/1006
|
||||
// so use "/" for AlternatesFS, I guess it is the same behavior as current nogogit (no limitation or check for the "objects/info/alternates" paths), trust the "clone" command executed by the server.
|
||||
var altFs billy.Filesystem
|
||||
if setting.IsWindows {
|
||||
altFs = osfs.New(filepath.VolumeName(setting.RepoRootPath) + "\\") // TODO: does it really work for Windows? Need some time to check.
|
||||
} else {
|
||||
altFs = osfs.New("/")
|
||||
}
|
||||
gitRepo.objectFormatCache = ParseGogitHash(plumbing.ZeroHash).Type()
|
||||
storage := filesystem.NewStorageWithOptions(packIdxFS{fs}, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true, LargeObjectThreshold: setting.Git.LargeObjectThreshold, AlternatesFS: altFs})
|
||||
packs, _ := storage.ObjectPacks()
|
||||
gitRepo.gogitStorage = &reindexingStorage{Storage: storage, packs: packs}
|
||||
gitRepo.gogitRepo, err = gogit.Open(gitRepo.gogitStorage, fs)
|
||||
if err != nil {
|
||||
_ = gitRepo.gogitStorage.Close()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *Repository) closeInternal() error {
|
||||
if repo.gogitStorage == nil {
|
||||
return nil
|
||||
}
|
||||
err := repo.gogitStorage.Close()
|
||||
repo.gogitStorage = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// GoGitRepo gets the go-git repo representation
|
||||
func (repo *Repository) GoGitRepo() *gogit.Repository {
|
||||
return repo.gogitRepo
|
||||
}
|
||||
@@ -2,8 +2,12 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
const isGogit = false
|
||||
|
||||
type Repository struct {
|
||||
RepositoryBase
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
@@ -5,13 +5,9 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// BranchPrefix base dir of the branch information file store on git
|
||||
@@ -28,170 +24,3 @@ func (repo *Repository) AddRemote(ctx context.Context, name, url string, fetch b
|
||||
RunStdString(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsObjectExist returns true if the given object exists in the repository.
|
||||
// FIXME: this function doesn't seem right, it is only used by GarbageCollectLFSMetaObjectsForRepo
|
||||
func (repo *Repository) IsObjectExist(ctx context.Context, name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
log.Debug("Error opening CatFileBatch %v", err)
|
||||
return false
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(name)
|
||||
if err != nil {
|
||||
log.Debug("Error checking object info %v", err)
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(info.ID, name) // FIXME: this logic doesn't seem right, why "HasPrefix"
|
||||
}
|
||||
|
||||
// IsReferenceExist returns true if given reference exists in the repository.
|
||||
func (repo *Repository) IsReferenceExist(ctx context.Context, name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
log.Error("Error opening CatFileBatch %v", err)
|
||||
return false
|
||||
}
|
||||
defer cancel()
|
||||
_, err = batch.QueryInfo(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsBranchExist returns true if given branch exists in current repository.
|
||||
func (repo *Repository) IsBranchExist(ctx context.Context, name string) bool {
|
||||
if repo == nil || name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return repo.IsReferenceExist(ctx, BranchPrefix+name)
|
||||
}
|
||||
|
||||
// GetBranchNames returns branches from the repository, skipping "skip" initial branches and
|
||||
// returning at most "limit" branches, or all branches if "limit" is 0.
|
||||
func (repo *Repository) GetBranchNames(ctx context.Context, skip, limit int) ([]string, int, error) {
|
||||
return callShowRef(ctx, repo, BranchPrefix, gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}, skip, limit)
|
||||
}
|
||||
|
||||
// WalkReferences walks all the references from the repository
|
||||
// refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty.
|
||||
func (repo *Repository) WalkReferences(ctx context.Context, refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
|
||||
var args gitcmd.TrustedCmdArgs
|
||||
switch refType {
|
||||
case ObjectTag:
|
||||
args = gitcmd.TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}
|
||||
case ObjectBranch:
|
||||
args = gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}
|
||||
}
|
||||
|
||||
return WalkShowRef(ctx, repo, args, skip, limit, walkfn)
|
||||
}
|
||||
|
||||
// callShowRef return refs, if limit = 0 it will not limit
|
||||
func callShowRef(ctx context.Context, repo RepositoryFacade, trimPrefix string, extraArgs gitcmd.TrustedCmdArgs, skip, limit int) (branchNames []string, countAll int, err error) {
|
||||
countAll, err = WalkShowRef(ctx, repo, extraArgs, skip, limit, func(_, branchName string) error {
|
||||
branchName = strings.TrimPrefix(branchName, trimPrefix)
|
||||
branchNames = append(branchNames, branchName)
|
||||
|
||||
return nil
|
||||
})
|
||||
return branchNames, countAll, err
|
||||
}
|
||||
|
||||
func WalkShowRef(ctx context.Context, repo RepositoryFacade, extraArgs gitcmd.TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
|
||||
i := 0
|
||||
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
|
||||
args = append(args, extraArgs...)
|
||||
cmd := gitcmd.NewCommand(args...)
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
cmd.WithRepo(repo).
|
||||
WithPipelineFunc(func(gitcmd.Context) error {
|
||||
bufReader := bufio.NewReader(stdoutReader)
|
||||
for i < skip {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
}
|
||||
for limit == 0 || i < skip+limit {
|
||||
// The output of show-ref is simply a list:
|
||||
// <sha> SP <ref> LF
|
||||
sha, err := bufReader.ReadString(' ')
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
branchName, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
// This shouldn't happen... but we'll tolerate it for the sake of peace
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(branchName) > 0 {
|
||||
branchName = branchName[:len(branchName)-1]
|
||||
}
|
||||
|
||||
if len(sha) > 0 {
|
||||
sha = sha[:len(sha)-1]
|
||||
}
|
||||
|
||||
err = walkfn(sha, branchName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i++
|
||||
}
|
||||
// count all refs
|
||||
for limit != 0 {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
err = cmd.RunWithStderr(ctx)
|
||||
if errPipeline, ok := gitcmd.UnwrapPipelineError(err); ok {
|
||||
return i, errPipeline // keep the old behavior: return pipeline error directly
|
||||
}
|
||||
return i, err
|
||||
}
|
||||
|
||||
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
|
||||
func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) {
|
||||
var revList []string
|
||||
_, err := WalkShowRef(ctx, repo, nil, 0, 0, func(walkSha, refname string) error {
|
||||
if walkSha == sha && strings.HasPrefix(refname, prefix) {
|
||||
revList = append(revList, refname)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return revList, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/storer"
|
||||
)
|
||||
|
||||
// IsObjectExist returns true if the given object exists in the repository.
|
||||
// FIXME: Inconsistent behavior with nogogit edition
|
||||
// Unlike the implementation of IsObjectExist in nogogit edition, it does not support short hashes here.
|
||||
// For example, IsObjectExist("153f451") will return false, but it will return true in nogogit edition.
|
||||
// To fix this, the solution could be adding support for short hashes in gogit edition if it's really needed.
|
||||
func (repo *Repository) IsObjectExist(_ context.Context, name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err := repo.gogitRepo.Object(plumbing.AnyObject, plumbing.NewHash(name))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsReferenceExist returns true if given reference exists in the repository.
|
||||
// FIXME: Inconsistent behavior with nogogit edition
|
||||
// Unlike the implementation of IsObjectExist in nogogit edition, it does not support blob hashes here.
|
||||
// For example, IsObjectExist([existing_blob_hash]) will return false, but it will return true in nogogit edition.
|
||||
// To fix this, the solution could be refusing to support blob hashes in nogogit edition since a blob hash is not a reference.
|
||||
func (repo *Repository) IsReferenceExist(_ context.Context, name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err := repo.gogitRepo.ResolveRevision(plumbing.Revision(name))
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsBranchExist returns true if given branch exists in current repository.
|
||||
func (repo *Repository) IsBranchExist(_ context.Context, name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
reference, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return reference.Type() != plumbing.InvalidReference
|
||||
}
|
||||
|
||||
// GetBranches returns branches from the repository, skipping "skip" initial branches and
|
||||
// returning at most "limit" branches, or all branches if "limit" is 0.
|
||||
// Branches are returned with sort of `-committerdate` as the nogogit
|
||||
// implementation. This requires full fetch, sort and then the
|
||||
// skip/limit applies later as gogit returns in undefined order.
|
||||
func (repo *Repository) GetBranchNames(_ context.Context, skip, limit int) ([]string, int, error) {
|
||||
type BranchData struct {
|
||||
name string
|
||||
committerDate int64
|
||||
}
|
||||
var branchData []BranchData
|
||||
|
||||
branchIter, err := repo.gogitRepo.Branches()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_ = branchIter.ForEach(func(branch *plumbing.Reference) error {
|
||||
obj, err := repo.gogitRepo.CommitObject(branch.Hash())
|
||||
if err != nil {
|
||||
// skip branch if can't find commit
|
||||
return nil
|
||||
}
|
||||
|
||||
branchData = append(branchData, BranchData{strings.TrimPrefix(branch.Name().String(), BranchPrefix), obj.Committer.When.Unix()})
|
||||
return nil
|
||||
})
|
||||
|
||||
sort.Slice(branchData, func(i, j int) bool {
|
||||
return !(branchData[i].committerDate < branchData[j].committerDate)
|
||||
})
|
||||
|
||||
var branchNames []string
|
||||
maxPos := len(branchData)
|
||||
if limit > 0 {
|
||||
maxPos = min(skip+limit, maxPos)
|
||||
}
|
||||
for i := skip; i < maxPos; i++ {
|
||||
branchNames = append(branchNames, branchData[i].name)
|
||||
}
|
||||
|
||||
return branchNames, len(branchData), nil
|
||||
}
|
||||
|
||||
// WalkReferences walks all the references from the repository
|
||||
func (repo *Repository) WalkReferences(ctx context.Context, arg ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
|
||||
i := 0
|
||||
var iter storer.ReferenceIter
|
||||
var err error
|
||||
switch arg {
|
||||
case ObjectTag:
|
||||
iter, err = repo.gogitRepo.Tags()
|
||||
case ObjectBranch:
|
||||
iter, err = repo.gogitRepo.Branches()
|
||||
default:
|
||||
iter, err = repo.gogitRepo.References()
|
||||
}
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
err = iter.ForEach(func(ref *plumbing.Reference) error {
|
||||
if i < skip {
|
||||
i++
|
||||
return nil
|
||||
}
|
||||
err := walkfn(ref.Hash().String(), string(ref.Name()))
|
||||
i++
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if limit != 0 && i >= skip+limit {
|
||||
return storer.ErrStop
|
||||
}
|
||||
return ctx.Err()
|
||||
})
|
||||
return i, err
|
||||
}
|
||||
|
||||
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
|
||||
func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) {
|
||||
var revList []string
|
||||
iter, err := repo.gogitRepo.References()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = iter.ForEach(func(ref *plumbing.Reference) error {
|
||||
if ref.Hash().String() == sha && strings.HasPrefix(string(ref.Name()), prefix) {
|
||||
revList = append(revList, string(ref.Name()))
|
||||
}
|
||||
return ctx.Err()
|
||||
})
|
||||
return revList, err
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/log"
|
||||
)
|
||||
|
||||
// IsObjectExist returns true if the given object exists in the repository.
|
||||
// FIXME: this function doesn't seem right, it is only used by GarbageCollectLFSMetaObjectsForRepo
|
||||
func (repo *Repository) IsObjectExist(ctx context.Context, name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
log.Debug("Error opening CatFileBatch %v", err)
|
||||
return false
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(name)
|
||||
if err != nil {
|
||||
log.Debug("Error checking object info %v", err)
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(info.ID, name) // FIXME: this logic doesn't seem right, why "HasPrefix"
|
||||
}
|
||||
|
||||
// IsReferenceExist returns true if given reference exists in the repository.
|
||||
func (repo *Repository) IsReferenceExist(ctx context.Context, name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
log.Error("Error opening CatFileBatch %v", err)
|
||||
return false
|
||||
}
|
||||
defer cancel()
|
||||
_, err = batch.QueryInfo(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsBranchExist returns true if given branch exists in current repository.
|
||||
func (repo *Repository) IsBranchExist(ctx context.Context, name string) bool {
|
||||
if repo == nil || name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return repo.IsReferenceExist(ctx, BranchPrefix+name)
|
||||
}
|
||||
|
||||
// GetBranchNames returns branches from the repository, skipping "skip" initial branches and
|
||||
// returning at most "limit" branches, or all branches if "limit" is 0.
|
||||
func (repo *Repository) GetBranchNames(ctx context.Context, skip, limit int) ([]string, int, error) {
|
||||
return callShowRef(ctx, repo, BranchPrefix, gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}, skip, limit)
|
||||
}
|
||||
|
||||
// WalkReferences walks all the references from the repository
|
||||
// refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty.
|
||||
func (repo *Repository) WalkReferences(ctx context.Context, refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) {
|
||||
var args gitcmd.TrustedCmdArgs
|
||||
switch refType {
|
||||
case ObjectTag:
|
||||
args = gitcmd.TrustedCmdArgs{TagPrefix, "--sort=-taggerdate"}
|
||||
case ObjectBranch:
|
||||
args = gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}
|
||||
}
|
||||
|
||||
return WalkShowRef(ctx, repo, args, skip, limit, walkfn)
|
||||
}
|
||||
|
||||
// callShowRef return refs, if limit = 0 it will not limit
|
||||
func callShowRef(ctx context.Context, repo RepositoryFacade, trimPrefix string, extraArgs gitcmd.TrustedCmdArgs, skip, limit int) (branchNames []string, countAll int, err error) {
|
||||
countAll, err = WalkShowRef(ctx, repo, extraArgs, skip, limit, func(_, branchName string) error {
|
||||
branchName = strings.TrimPrefix(branchName, trimPrefix)
|
||||
branchNames = append(branchNames, branchName)
|
||||
|
||||
return nil
|
||||
})
|
||||
return branchNames, countAll, err
|
||||
}
|
||||
|
||||
func WalkShowRef(ctx context.Context, repo RepositoryFacade, extraArgs gitcmd.TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
|
||||
i := 0
|
||||
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
|
||||
args = append(args, extraArgs...)
|
||||
cmd := gitcmd.NewCommand(args...)
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
cmd.WithRepo(repo).
|
||||
WithPipelineFunc(func(gitcmd.Context) error {
|
||||
bufReader := bufio.NewReader(stdoutReader)
|
||||
for i < skip {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
}
|
||||
for limit == 0 || i < skip+limit {
|
||||
// The output of show-ref is simply a list:
|
||||
// <sha> SP <ref> LF
|
||||
sha, err := bufReader.ReadString(' ')
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
branchName, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
// This shouldn't happen... but we'll tolerate it for the sake of peace
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(branchName) > 0 {
|
||||
branchName = branchName[:len(branchName)-1]
|
||||
}
|
||||
|
||||
if len(sha) > 0 {
|
||||
sha = sha[:len(sha)-1]
|
||||
}
|
||||
|
||||
err = walkfn(sha, branchName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i++
|
||||
}
|
||||
// count all refs
|
||||
for limit != 0 {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
err = cmd.RunWithStderr(ctx)
|
||||
if errPipeline, ok := gitcmd.UnwrapPipelineError(err); ok {
|
||||
return i, errPipeline // keep the old behavior: return pipeline error directly
|
||||
}
|
||||
return i, err
|
||||
}
|
||||
|
||||
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
|
||||
func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) {
|
||||
var revList []string
|
||||
_, err := WalkShowRef(ctx, repo, nil, 0, 0, func(walkSha, refname string) error {
|
||||
if walkSha == sha && strings.HasPrefix(refname, prefix) {
|
||||
revList = append(revList, refname)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return revList, err
|
||||
}
|
||||
@@ -4,9 +4,15 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -39,6 +45,41 @@ func TestRepository_GetBranches(t *testing.T) {
|
||||
assert.ElementsMatch(t, []string{}, branches)
|
||||
}
|
||||
|
||||
// FIXME: covers the gogit workarounds in repo_base_gogit.go, remove with the gogit build
|
||||
func TestReadsAfterConcurrentRepack(t *testing.T) {
|
||||
repoDir := t.TempDir()
|
||||
require.NoError(t, gitcmd.NewCommand("init", "--bare").AddDynamicArguments(repoDir).Run(t.Context()))
|
||||
content := strings.Repeat("a", int(setting.Git.LargeObjectThreshold)+1)
|
||||
for _, from := range []string{"", "from refs/heads/main^0\n"} {
|
||||
stdin := fmt.Sprintf("commit refs/heads/main\ncommitter a <a@a> 0 +0000\ndata 0\n%sM 100644 inline f\ndata %d\n%s\n", from, len(content), content)
|
||||
require.NoError(t, gitcmd.NewCommand("fast-import").WithDir(repoDir).WithStdinBytes([]byte(stdin)).Run(t.Context()))
|
||||
require.NoError(t, gitcmd.NewCommand("repack", "-d").WithDir(repoDir).Run(t.Context()))
|
||||
}
|
||||
|
||||
repo, err := OpenRepositoryLocal(t.Context(), repoDir)
|
||||
require.NoError(t, err)
|
||||
defer repo.Close()
|
||||
require.False(t, repo.IsObjectExist(t.Context(), "0000000000000000000000000000000000000001"))
|
||||
blobRepo, err := OpenRepositoryLocal(t.Context(), repoDir)
|
||||
require.NoError(t, err)
|
||||
defer blobRepo.Close()
|
||||
commit, err := blobRepo.GetBranchCommit(t.Context(), "main")
|
||||
require.NoError(t, err)
|
||||
readBlob := func() string {
|
||||
data, err := commit.GetFileContent(t.Context(), blobRepo, "f", len(content))
|
||||
require.NoError(t, err)
|
||||
return data
|
||||
}
|
||||
require.Equal(t, content, readBlob())
|
||||
require.NoError(t, gitcmd.NewCommand("repack", "-a", "-d").WithDir(repoDir).Run(t.Context()))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(repoDir, "objects", "pack", "pack-"+strings.Repeat("1", 40)+".pack"), nil, 0o644))
|
||||
|
||||
branches, _, err := repo.GetBranchNames(t.Context(), 0, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"main"}, branches)
|
||||
assert.Equal(t, content, readBlob())
|
||||
}
|
||||
|
||||
func BenchmarkRepository_GetBranches(b *testing.B) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||
bareRepo1, err := OpenRepositoryLocal(b.Context(), bareRepo1Path)
|
||||
@@ -102,6 +143,10 @@ func TestRepository_IsObjectExist(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
// FIXME: Inconsistent behavior between gogit and nogogit editions
|
||||
// See the comment of IsObjectExist in gogit edition for more details.
|
||||
supportShortHash := !isGogit
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
arg string
|
||||
@@ -125,7 +170,7 @@ func TestRepository_IsObjectExist(t *testing.T) {
|
||||
{
|
||||
name: "short commit hash",
|
||||
arg: "ce06481",
|
||||
want: true,
|
||||
want: supportShortHash,
|
||||
},
|
||||
{
|
||||
name: "blob hash",
|
||||
@@ -135,7 +180,7 @@ func TestRepository_IsObjectExist(t *testing.T) {
|
||||
{
|
||||
name: "short blob hash",
|
||||
arg: "153f451",
|
||||
want: true,
|
||||
want: supportShortHash,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -151,6 +196,10 @@ func TestRepository_IsReferenceExist(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer repo.Close()
|
||||
|
||||
// FIXME: Inconsistent behavior between gogit and nogogit editions
|
||||
// See the comment of IsReferenceExist in gogit edition for more details.
|
||||
supportBlobHash := !isGogit
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
arg string
|
||||
@@ -179,12 +228,12 @@ func TestRepository_IsReferenceExist(t *testing.T) {
|
||||
{
|
||||
name: "blob hash",
|
||||
arg: "153f451b9ee7fa1da317ab17a127e9fd9d384310",
|
||||
want: true,
|
||||
want: supportBlobHash,
|
||||
},
|
||||
{
|
||||
name: "short blob hash",
|
||||
arg: "153f451",
|
||||
want: true,
|
||||
want: supportBlobHash,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -7,7 +7,6 @@ package git
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -459,124 +458,3 @@ func (repo *Repository) GetCommitBranchStart(ctx context.Context, env []string,
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
||||
func (repo *Repository) GetRefCommitID(ctx context.Context, name string) (string, error) {
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(name)
|
||||
if IsErrNotExist(err) {
|
||||
return "", ErrNotExist{name, ""}
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return info.ID, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) getCommit(_ context.Context, id ObjectID) (*Commit, error) {
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
return repo.getCommitWithBatch(batch, id)
|
||||
}
|
||||
|
||||
func limitDiscardReader(rd BufferedReader, full, limit int64) (io.Reader, func() error) {
|
||||
return io.LimitReader(rd, min(full, limit)), func() error {
|
||||
if full > limit {
|
||||
return DiscardFull(rd, full-limit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Commit, error) {
|
||||
info, rd, err := batch.QueryContent(id.String())
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
|
||||
return nil, ErrNotExist{ID: id.String()}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch info.Type {
|
||||
case "missing":
|
||||
return nil, ErrNotExist{ID: id.String()}
|
||||
case "tag":
|
||||
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
|
||||
data, err := io.ReadAll(limitReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = limitDiscard(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = rd.Discard(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag, err := parseTagData(id.Type(), data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.getCommitWithBatch(batch, tag.Object)
|
||||
case "commit":
|
||||
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
|
||||
commit, err := CommitFromReader(id, limitReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = limitDiscard(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = rd.Discard(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return commit, nil
|
||||
default:
|
||||
if info.Type != "blob" && info.Type != "tree" {
|
||||
setting.PanicInDevOrTesting("Unknown cat-file object type %s for object %s in repo %s", info.Type, id.String(), repo.LogString())
|
||||
}
|
||||
if err := DiscardFull(rd, info.Size+1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, ErrNotExist{
|
||||
ID: id.String(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToGitID returns a git object ID from the git ref, it doesn't guarantee the returned ID really exists
|
||||
func (repo *Repository) ConvertToGitID(ctx context.Context, ref string) (ObjectID, error) {
|
||||
objectFormat, err := repo.GetObjectFormat(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if IsStringValidObjectID(objectFormat, ref) {
|
||||
id, err := NewIDFromString(ref)
|
||||
if err == nil {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(ref)
|
||||
if err != nil {
|
||||
if IsErrNotExist(err) {
|
||||
return nil, ErrNotExist{ref, ""}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return MustIDFromString(info.ID), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
// GetRefCommitID returns the last commit ID string of given reference.
|
||||
func (repo *Repository) GetRefCommitID(_ context.Context, name string) (string, error) {
|
||||
if plumbing.IsHash(name) {
|
||||
return name, nil
|
||||
}
|
||||
refName := plumbing.ReferenceName(name)
|
||||
if err := refName.Validate(); err != nil {
|
||||
// Match the nogogit behavior: an unresolvable/invalid ref name
|
||||
// is reported as not-existing rather than a generic validation error,
|
||||
// so callers can rely on IsErrNotExist regardless of build tag.
|
||||
return "", ErrNotExist{ID: name}
|
||||
}
|
||||
ref, err := repo.gogitRepo.Reference(refName, true)
|
||||
if err != nil {
|
||||
if err == plumbing.ErrReferenceNotFound {
|
||||
return "", ErrNotExist{
|
||||
ID: name,
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ref.Hash().String(), nil
|
||||
}
|
||||
|
||||
// ConvertToHash returns a Hash object from a potential ID string
|
||||
func (repo *Repository) ConvertToGitID(ctx context.Context, commitID string) (ObjectID, error) {
|
||||
objectFormat, err := repo.GetObjectFormat(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if IsStringValidObjectID(objectFormat, commitID) {
|
||||
ID, err := NewIDFromString(commitID)
|
||||
if err == nil {
|
||||
return ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
actualCommitID, _, err := gitcmd.NewCommand("rev-parse", "--verify").
|
||||
AddDynamicArguments(commitID).
|
||||
WithRepo(repo).
|
||||
RunStdString(ctx)
|
||||
actualCommitID = strings.TrimSpace(actualCommitID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "unknown revision or path") ||
|
||||
strings.Contains(err.Error(), "fatal: Needed a single revision") {
|
||||
return objectFormat.EmptyObjectID(), ErrNotExist{commitID, ""}
|
||||
}
|
||||
return objectFormat.EmptyObjectID(), err
|
||||
}
|
||||
|
||||
return NewIDFromString(actualCommitID)
|
||||
}
|
||||
|
||||
func (repo *Repository) getCommit(_ context.Context, id ObjectID) (*Commit, error) {
|
||||
var tagObject *object.Tag
|
||||
|
||||
commitID := plumbing.Hash(id.RawValue())
|
||||
gogitCommit, err := repo.gogitRepo.CommitObject(commitID)
|
||||
if err == plumbing.ErrObjectNotFound {
|
||||
tagObject, err = repo.gogitRepo.TagObject(commitID)
|
||||
if err == plumbing.ErrObjectNotFound {
|
||||
return nil, ErrNotExist{
|
||||
ID: id.String(),
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
gogitCommit, err = repo.gogitRepo.CommitObject(tagObject.Target)
|
||||
}
|
||||
// if we get a plumbing.ErrObjectNotFound here then the repository is broken and it should be 500
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commit := convertCommit(gogitCommit)
|
||||
|
||||
tree, err := gogitCommit.Tree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commit.TreeID = ParseGogitHash(tree.Hash)
|
||||
commit.Tree().resolvedGogitTreeObject = tree
|
||||
|
||||
return commit, nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
||||
func (repo *Repository) GetRefCommitID(ctx context.Context, name string) (string, error) {
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(name)
|
||||
if IsErrNotExist(err) {
|
||||
return "", ErrNotExist{name, ""}
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return info.ID, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) getCommit(ctx context.Context, id ObjectID) (*Commit, error) {
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
return repo.getCommitWithBatch(batch, id)
|
||||
}
|
||||
|
||||
func limitDiscardReader(rd BufferedReader, full, limit int64) (io.Reader, func() error) {
|
||||
return io.LimitReader(rd, min(full, limit)), func() error {
|
||||
if full > limit {
|
||||
return DiscardFull(rd, full-limit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Commit, error) {
|
||||
info, rd, err := batch.QueryContent(id.String())
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
|
||||
return nil, ErrNotExist{ID: id.String()}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch info.Type {
|
||||
case "missing":
|
||||
return nil, ErrNotExist{ID: id.String()}
|
||||
case "tag":
|
||||
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
|
||||
data, err := io.ReadAll(limitReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = limitDiscard(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = rd.Discard(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag, err := parseTagData(id.Type(), data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo.getCommitWithBatch(batch, tag.Object)
|
||||
case "commit":
|
||||
limitReader, limitDiscard := limitDiscardReader(rd, info.Size, MaxGitObjectSize)
|
||||
commit, err := CommitFromReader(id, limitReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = limitDiscard(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = rd.Discard(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return commit, nil
|
||||
default:
|
||||
if info.Type != "blob" && info.Type != "tree" {
|
||||
setting.PanicInDevOrTesting("Unknown cat-file object type %s for object %s in repo %s", info.Type, id.String(), repo.LogString())
|
||||
}
|
||||
if err := DiscardFull(rd, info.Size+1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, ErrNotExist{
|
||||
ID: id.String(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToGitID returns a git object ID from the git ref, it doesn't guarantee the returned ID really exists
|
||||
func (repo *Repository) ConvertToGitID(ctx context.Context, ref string) (ObjectID, error) {
|
||||
objectFormat, err := repo.GetObjectFormat(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if IsStringValidObjectID(objectFormat, ref) {
|
||||
id, err := NewIDFromString(ref)
|
||||
if err == nil {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
|
||||
batch, cancel, err := repo.CatFileBatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
info, err := batch.QueryInfo(ref)
|
||||
if err != nil {
|
||||
if IsErrNotExist(err) {
|
||||
return nil, ErrNotExist{ref, ""}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return MustIDFromString(info.ID), nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2019 The Gitea Authors.
|
||||
// All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
|
||||
commitgraph "github.com/go-git/go-git/v5/plumbing/format/commitgraph/v2"
|
||||
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
||||
)
|
||||
|
||||
// CommitNodeIndex returns the index for walking commit graph
|
||||
func (repo *Repository) CommitNodeIndex() (_ cgobject.CommitNodeIndex, closer func()) {
|
||||
indexPath := filepath.Join(gitrepo.RepoLocalPath(repo), "objects", "info", "commit-graph")
|
||||
file, err := os.Open(indexPath)
|
||||
if err == nil {
|
||||
var index commitgraph.Index
|
||||
index, err = commitgraph.OpenFileIndex(file)
|
||||
if err == nil {
|
||||
return cgobject.NewGraphCommitNodeIndex(index, repo.gogitRepo.Storer), func() { _ = file.Close() }
|
||||
}
|
||||
_ = file.Close()
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
log.Warn("Unable to read commit-graph for %s: %v", repo.LogString(), err)
|
||||
}
|
||||
|
||||
return cgobject.NewObjectCommitNodeIndex(repo.gogitRepo.Storer), func() {}
|
||||
}
|
||||
@@ -4,9 +4,7 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
@@ -98,64 +96,3 @@ func (repo *Repository) UnstableGuessRefByShortName(ctx context.Context, shortNa
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
||||
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
|
||||
refs := make([]*Reference, 0)
|
||||
cmd := gitcmd.NewCommand("for-each-ref")
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := cmd.WithRepo(repo).
|
||||
WithPipelineFunc(func(context gitcmd.Context) error {
|
||||
bufReader := bufio.NewReader(stdoutReader)
|
||||
for {
|
||||
// The output of for-each-ref is simply a list:
|
||||
// <sha> SP <type> TAB <ref> LF
|
||||
sha, err := bufReader.ReadString(' ')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sha = sha[:len(sha)-1]
|
||||
|
||||
typ, err := bufReader.ReadString('\t')
|
||||
if err == io.EOF {
|
||||
// This should not happen, but we'll tolerate it
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
typ = typ[:len(typ)-1]
|
||||
|
||||
refName, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
// This should not happen, but we'll tolerate it
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refName = refName[:len(refName)-1]
|
||||
|
||||
// refName cannot be HEAD but can be remotes or stash
|
||||
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
|
||||
continue
|
||||
}
|
||||
|
||||
if pattern == "" || strings.HasPrefix(refName, pattern) {
|
||||
r := &Reference{
|
||||
Name: refName,
|
||||
Object: MustIDFromString(sha),
|
||||
Type: typ,
|
||||
repo: repo,
|
||||
}
|
||||
refs = append(refs, r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}).RunWithStderr(ctx)
|
||||
return refs, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
||||
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
|
||||
r := repo.gogitRepo
|
||||
|
||||
refsIter, err := r.References()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refs := make([]*Reference, 0)
|
||||
if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
|
||||
if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
|
||||
(pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) {
|
||||
refType := string(ObjectCommit)
|
||||
if ref.Name().IsTag() {
|
||||
// tags can be of type `commit` (lightweight) or `tag` (annotated)
|
||||
if tagType, _ := repo.GetTagType(ctx, ParseGogitHash(ref.Hash())); err == nil {
|
||||
refType = tagType
|
||||
}
|
||||
}
|
||||
r := &Reference{
|
||||
Name: ref.Name().String(),
|
||||
Object: ParseGogitHash(ref.Hash()),
|
||||
Type: refType,
|
||||
repo: repo,
|
||||
}
|
||||
refs = append(refs, r)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
// GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with.
|
||||
func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) {
|
||||
refs := make([]*Reference, 0)
|
||||
cmd := gitcmd.NewCommand("for-each-ref")
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := cmd.WithRepo(repo).
|
||||
WithPipelineFunc(func(context gitcmd.Context) error {
|
||||
bufReader := bufio.NewReader(stdoutReader)
|
||||
for {
|
||||
// The output of for-each-ref is simply a list:
|
||||
// <sha> SP <type> TAB <ref> LF
|
||||
sha, err := bufReader.ReadString(' ')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sha = sha[:len(sha)-1]
|
||||
|
||||
typ, err := bufReader.ReadString('\t')
|
||||
if err == io.EOF {
|
||||
// This should not happen, but we'll tolerate it
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
typ = typ[:len(typ)-1]
|
||||
|
||||
refName, err := bufReader.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
// This should not happen, but we'll tolerate it
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refName = refName[:len(refName)-1]
|
||||
|
||||
// refName cannot be HEAD but can be remotes or stash
|
||||
if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" {
|
||||
continue
|
||||
}
|
||||
|
||||
if pattern == "" || strings.HasPrefix(refName, pattern) {
|
||||
r := &Reference{
|
||||
Name: refName,
|
||||
Object: MustIDFromString(sha),
|
||||
Type: typ,
|
||||
repo: repo,
|
||||
}
|
||||
refs = append(refs, r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}).RunWithStderr(ctx)
|
||||
return refs, err
|
||||
}
|
||||
@@ -61,28 +61,10 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time.
|
||||
} else {
|
||||
gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch)
|
||||
}
|
||||
if err := parseCodeActivityLog(ctx, repo, gitCmd, stats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetCodeActivityAuthors returns the authors of non-merge commits in all branches since fromTime, sorted by commit count
|
||||
func GetCodeActivityAuthors(ctx context.Context, repo RepositoryFacade, fromTime time.Time) ([]*CodeActivityAuthor, error) {
|
||||
stats := &CodeActivityStats{}
|
||||
gitCmd := gitcmd.NewCommand("log", "--no-merges", "--pretty=format:---%n%H%n%aN%n%aE%n", "--branches=*").
|
||||
AddOptionFormat("--since=%s", fromTime.Format(time.RFC3339))
|
||||
if err := parseCodeActivityLog(ctx, repo, gitCmd, stats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stats.Authors, nil
|
||||
}
|
||||
|
||||
func parseCodeActivityLog(ctx context.Context, repo RepositoryFacade, gitCmd *gitcmd.Command, stats *CodeActivityStats) error {
|
||||
stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := gitCmd.
|
||||
err = gitCmd.
|
||||
WithRepo(repo).
|
||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
scanner := bufio.NewScanner(stdoutReader)
|
||||
@@ -134,8 +116,8 @@ func parseCodeActivityLog(ctx context.Context, repo RepositoryFacade, gitCmd *gi
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("parseCodeActivityLog scan: %w", err)
|
||||
if err = scanner.Err(); err != nil {
|
||||
return fmt.Errorf("GetCodeActivityStats scan: %w", err)
|
||||
}
|
||||
a := make([]*CodeActivityAuthor, 0, len(authors))
|
||||
for _, v := range authors {
|
||||
@@ -152,7 +134,8 @@ func parseCodeActivityLog(ctx context.Context, repo RepositoryFacade, gitCmd *gi
|
||||
}).
|
||||
RunWithStderr(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parseCodeActivityLog: %w", err)
|
||||
return nil, fmt.Errorf("GetCodeActivityStats: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@@ -33,8 +33,4 @@ func TestRepository_GetCodeActivityStats(t *testing.T) {
|
||||
assert.Equal(t, "tris.git@shoddynet.org", code.Authors[1].Email)
|
||||
assert.EqualValues(t, 3, code.Authors[1].Commits)
|
||||
assert.EqualValues(t, 5, code.Authors[0].Commits)
|
||||
|
||||
authors, err := GetCodeActivityAuthors(t.Context(), bareRepo1, timeFrom)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, code.Authors, authors)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user