mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-04 07:33:44 +09:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7641fc3a8c | |||
| cca466caae | |||
| 516a4883fa | |||
| f65e01226a | |||
| ab1979bcfb | |||
| ad38b60983 | |||
| 3932624947 | |||
| e0e18fc286 | |||
| 457510fa09 | |||
| 2baa5a16f8 | |||
| fb067e1115 | |||
| daaed0d7f4 | |||
| 99573bde0e | |||
| 986b0bcae0 | |||
| 6dad3ad43d | |||
| cf89ecd887 | |||
| bea6fcaa84 | |||
| 873efed5a6 | |||
| e6ffbea888 | |||
| 2f01ec38bd | |||
| fc81f2832a | |||
| 075ff8f516 | |||
| 2f5cdbd5c1 | |||
| aae0a218c3 | |||
| 9b5c87a6b6 | |||
| 51b93d1d27 | |||
| f3aed8b81d | |||
| fc68608603 | |||
| fe31237fd8 | |||
| a71c5c94c5 | |||
| f81a2ab69a | |||
| b0d6cc1d22 | |||
| 8936303510 | |||
| 3d085dbaf1 | |||
| 9b2a3c267b | |||
| 590d2984d9 | |||
| 61e0343580 | |||
| 0b43bde974 | |||
| cc95f141f8 | |||
| a25fbd43c4 | |||
| f365a6b9c8 | |||
| e0095af8c3 |
@@ -34,7 +34,7 @@ runs:
|
|||||||
env:
|
env:
|
||||||
# pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
|
# 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
|
GOTEST_FLAGS: -race -timeout=40m
|
||||||
TAGS: bindata gogit
|
TAGS: bindata
|
||||||
TEST_LDAP: 1
|
TEST_LDAP: 1
|
||||||
TEST_SHARD: ${{ inputs.shard }}
|
TEST_SHARD: ${{ inputs.shard }}
|
||||||
TEST_TOTAL_SHARDS: ${{ inputs.total-shards }}
|
TEST_TOTAL_SHARDS: ${{ inputs.total-shards }}
|
||||||
|
|||||||
@@ -37,22 +37,15 @@ jobs:
|
|||||||
- uses: ./.github/actions/go-setup
|
- uses: ./.github/actions/go-setup
|
||||||
- run: make deps-backend deps-tools
|
- run: make deps-backend deps-tools
|
||||||
- run: TAGS="bindata" make backend
|
- run: TAGS="bindata" make backend
|
||||||
- run: TAGS="bindata gogit" make backend
|
|
||||||
- name: warm test compile cache (bindata)
|
- name: warm test compile cache (bindata)
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
GOTEST_FLAGS: -race -list=^$$ -count=1
|
GOTEST_FLAGS: -race -list=^$$ -count=1
|
||||||
run: make test-backend
|
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
|
- name: warm integration compile cache
|
||||||
run: |
|
run: |
|
||||||
TAGS="bindata" make test-integration-compile
|
TAGS="bindata" make test-integration-compile
|
||||||
TAGS="bindata gogit" make test-integration-compile
|
TAGS="bindata" GOTEST_FLAGS="-race" make test-integration-compile
|
||||||
TAGS="bindata gogit" GOTEST_FLAGS="-race" make test-integration-compile
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: go-gitea/giteabot@4c9d4d3fd913b5c35f59dcc0b004a3d11d5b22bf # v1.0.7
|
- uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||||
gitea_fork: giteabot/gitea
|
gitea_fork: giteabot/gitea
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: giteabot-review
|
||||||
|
|
||||||
|
# Relays PR reviews to giteabot.yml through its workflow_run trigger, because review
|
||||||
|
# runs on fork PRs get no secrets and a read-only token. The job itself does nothing.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_review:
|
||||||
|
types:
|
||||||
|
- submitted
|
||||||
|
- edited
|
||||||
|
- dismissed
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
relay:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: "true"
|
||||||
@@ -20,13 +20,12 @@ on:
|
|||||||
- closed
|
- closed
|
||||||
- review_requested
|
- review_requested
|
||||||
- review_request_removed
|
- review_request_removed
|
||||||
# Review events keep review-derived state such as lgtm labels and status checks
|
# Reviews arrive through giteabot-review because fork PR review runs get no secrets
|
||||||
# in sync after approvals, edits, or dismissals.
|
workflow_run:
|
||||||
pull_request_review:
|
workflows:
|
||||||
|
- giteabot-review
|
||||||
types:
|
types:
|
||||||
- submitted
|
- requested
|
||||||
- edited
|
|
||||||
- dismissed
|
|
||||||
# Periodic maintenance is still useful as a backstop for queue cleanup and
|
# Periodic maintenance is still useful as a backstop for queue cleanup and
|
||||||
# other housekeeping, even though main pushes now trigger it promptly.
|
# other housekeeping, even though main pushes now trigger it promptly.
|
||||||
schedule:
|
schedule:
|
||||||
@@ -43,12 +42,12 @@ on:
|
|||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ format('{0}-{1}', github.workflow, (github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review') && format('pr-{0}', github.event.pull_request.number) || 'maintenance') }}
|
group: ${{ format('{0}-{1}', github.workflow, github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_run' && format('review-{0}', github.event.workflow_run.head_sha) || 'maintenance') }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
giteabot:
|
giteabot:
|
||||||
if: github.repository == 'go-gitea/gitea'
|
if: github.repository == 'go-gitea/gitea' && (github.event_name != 'workflow_run' || github.event.workflow_run.event == 'pull_request_review')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
permissions:
|
permissions:
|
||||||
@@ -57,9 +56,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
statuses: write
|
statuses: write
|
||||||
steps:
|
steps:
|
||||||
# pull_request_review runs without repository secrets on fork PRs, so fall
|
- uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
|
||||||
# back to the workflow token for the non-backport checks handled here.
|
|
||||||
- uses: go-gitea/giteabot@4c9d4d3fd913b5c35f59dcc0b004a3d11d5b22bf # v1.0.7
|
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITEABOT_TOKEN || github.token }}
|
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||||
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||||
|
|||||||
@@ -93,13 +93,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
GOARCH: arm64
|
GOARCH: arm64
|
||||||
TAGS: bindata gogit
|
TAGS: bindata
|
||||||
- name: build-backend-windows
|
- name: build-backend-windows
|
||||||
run: go build -ldflags '-s -w' -o gitea-windows
|
run: go build -ldflags '-s -w' -o gitea-windows
|
||||||
env:
|
env:
|
||||||
GOOS: windows
|
GOOS: windows
|
||||||
GOARCH: amd64
|
GOARCH: amd64
|
||||||
TAGS: bindata gogit
|
TAGS: bindata
|
||||||
- name: build-backend-386
|
- name: build-backend-386
|
||||||
run: go build -ldflags '-s -w' -o gitea-linux-386
|
run: go build -ldflags '-s -w' -o gitea-linux-386
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -95,17 +95,17 @@ jobs:
|
|||||||
- run: make deps-backend
|
- run: make deps-backend
|
||||||
- run: make backend
|
- run: make backend
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit
|
TAGS: bindata
|
||||||
- run: GITEA_TEST_DATABASE=sqlite make test-migration
|
- run: GITEA_TEST_DATABASE=sqlite make test-migration
|
||||||
env:
|
env:
|
||||||
TAGS: bindata gogit
|
TAGS: bindata
|
||||||
- name: run tests
|
- name: run tests
|
||||||
run: GITEA_TEST_DATABASE=sqlite make test-integration
|
run: GITEA_TEST_DATABASE=sqlite make test-integration
|
||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
env:
|
env:
|
||||||
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
|
# sqlite driver can contain large amount of Golang code, so don't use race detector for it, otherwise, extremely slow
|
||||||
GOTEST_FLAGS: -timeout=40m
|
GOTEST_FLAGS: -timeout=40m
|
||||||
TAGS: bindata gogit
|
TAGS: bindata
|
||||||
|
|
||||||
test-unit:
|
test-unit:
|
||||||
if: needs.files-changed.outputs.backend == 'true'
|
if: needs.files-changed.outputs.backend == 'true'
|
||||||
@@ -163,13 +163,6 @@ jobs:
|
|||||||
GOTEST_FLAGS: -race -timeout=20m
|
GOTEST_FLAGS: -race -timeout=20m
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
|
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
|
- run: make test-check
|
||||||
|
|
||||||
test-mysql:
|
test-mysql:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
- In `options/locale`, only edit `locale_en-US.json`, other locales are synced automatically
|
- 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 TS, use `!` instead of `?.`/`??` when a value always exists
|
||||||
- In Go, prefer to use modern language features wherever possible
|
- 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`
|
- 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`
|
- 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`
|
- 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`
|
||||||
|
|||||||
Generated
-45
File diff suppressed because one or more lines are too long
+2
-8
@@ -13,13 +13,11 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
"gitea.dev/modules/dump"
|
"gitea.dev/modules/dump"
|
||||||
"gitea.dev/modules/json"
|
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/storage"
|
"gitea.dev/modules/storage"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"gitea.com/go-chi/session"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -249,12 +247,8 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
|||||||
log.Info("Packing data directory...%s", setting.AppDataPath)
|
log.Info("Packing data directory...%s", setting.AppDataPath)
|
||||||
|
|
||||||
var excludes []string
|
var excludes []string
|
||||||
if setting.SessionConfig.OriginalProvider == "file" {
|
if setting.SessionConfig.Provider == "file" {
|
||||||
var opts session.Options
|
excludes = append(excludes, setting.SessionConfig.ProviderConfig)
|
||||||
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") {
|
if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
|
||||||
|
|||||||
+21
-26
@@ -155,7 +155,7 @@
|
|||||||
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
|
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
|
||||||
;BUILTIN_SSH_SERVER_USER =
|
;BUILTIN_SSH_SERVER_USER =
|
||||||
;;
|
;;
|
||||||
;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL
|
;; Domain name to be exposed in clone URL, defaults to the domain part of ROOT_URL
|
||||||
;SSH_DOMAIN =
|
;SSH_DOMAIN =
|
||||||
;;
|
;;
|
||||||
;; Port number to be exposed in clone URL.
|
;; 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
|
;; 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
|
;; 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
|
;; relative paths are made absolute relative to the APP_DATA_PATH
|
||||||
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
|
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gitea.ed25519, ssh/gitea.ecdsa, ssh/gogs.rsa
|
||||||
;;
|
;;
|
||||||
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
|
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
|
||||||
;SSH_AUTHORIZED_KEYS_BACKUP = false
|
;SSH_AUTHORIZED_KEYS_BACKUP = false
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
|
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
|
||||||
;;
|
;;
|
||||||
;; Indicate whether to check minimum key size with corresponding type
|
;; Indicate whether to check minimum key size with corresponding type
|
||||||
;MINIMUM_KEY_SIZE_CHECK = false
|
;MINIMUM_KEY_SIZE_CHECK = true
|
||||||
;;
|
;;
|
||||||
;; TLS Settings: Either ACME or manual
|
;; TLS Settings: Either ACME or manual
|
||||||
;; (Other common TLS configuration are found before)
|
;; (Other common TLS configuration are found before)
|
||||||
@@ -459,7 +459,7 @@ INTERNAL_TOKEN =
|
|||||||
;LOGIN_REMEMBER_DAYS = 31
|
;LOGIN_REMEMBER_DAYS = 31
|
||||||
;;
|
;;
|
||||||
;; Name of cookie used to store authentication information.
|
;; Name of cookie used to store authentication information.
|
||||||
;COOKIE_REMEMBER_NAME = gitea_incredible
|
;COOKIE_REMEMBER_NAME = gitea_remember
|
||||||
;;
|
;;
|
||||||
;; URL or path that Gitea should redirect users to *after* performing its own logout.
|
;; 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.
|
;; Use this, if needed, when authentication is handled by a reverse proxy or SSO.
|
||||||
@@ -780,8 +780,6 @@ LEVEL = Info
|
|||||||
;; Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled)
|
;; 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
|
;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
|
;; Set to true to forcibly set core.protectNTFS=false
|
||||||
;DISABLE_CORE_PROTECT_NTFS=false
|
;DISABLE_CORE_PROTECT_NTFS=false
|
||||||
;; Disable the usage of using partial clones for git.
|
;; Disable the usage of using partial clones for git.
|
||||||
@@ -836,7 +834,7 @@ LEVEL = Info
|
|||||||
;EMAIL_DOMAIN_BLOCKLIST =
|
;EMAIL_DOMAIN_BLOCKLIST =
|
||||||
;;
|
;;
|
||||||
;; Disallow registration, only allow admins to create accounts.
|
;; Disallow registration, only allow admins to create accounts.
|
||||||
;DISABLE_REGISTRATION = false
|
;DISABLE_REGISTRATION = true
|
||||||
;;
|
;;
|
||||||
;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false
|
;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false
|
||||||
;ALLOW_ONLY_INTERNAL_REGISTRATION = false
|
;ALLOW_ONLY_INTERNAL_REGISTRATION = false
|
||||||
@@ -968,12 +966,11 @@ LEVEL = Info
|
|||||||
;; Value for the domain part of the user's email address in the git log if user
|
;; 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
|
;; 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
|
;; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default
|
||||||
;; value is "noreply." + DOMAIN, where DOMAIN resolves to the value from server.DOMAIN
|
;; value is "noreply." + the domain part of ROOT_URL
|
||||||
;; Note: do not use the <DOMAIN> notation below
|
;NO_REPLY_ADDRESS =
|
||||||
;NO_REPLY_ADDRESS = ; noreply.<DOMAIN>
|
|
||||||
;;
|
;;
|
||||||
;; Show Registration button
|
;; Show Registration button, defaults to true only if both DISABLE_REGISTRATION and ALLOW_ONLY_EXTERNAL_REGISTRATION are false
|
||||||
;SHOW_REGISTRATION_BUTTON = true
|
;SHOW_REGISTRATION_BUTTON = false
|
||||||
;;
|
;;
|
||||||
;; Show milestones dashboard page - a view of all the user's milestones
|
;; Show milestones dashboard page - a view of all the user's milestones
|
||||||
;SHOW_MILESTONES_DASHBOARD_PAGE = true
|
;SHOW_MILESTONES_DASHBOARD_PAGE = true
|
||||||
@@ -1670,13 +1667,13 @@ LEVEL = Info
|
|||||||
;;
|
;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy
|
;; General queue type, currently support: level, channel, redis, dummy
|
||||||
;; default to persistable-channel
|
;; default to level
|
||||||
;TYPE = persistable-channel
|
;TYPE = level
|
||||||
;;
|
;;
|
||||||
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
|
;; data-dir for storing level queues, individual queues will default to `queues/common` meaning the queue is shared.
|
||||||
;; Relative paths will be made absolute against "APP_DATA_PATH"
|
;; Relative paths will be made absolute against "APP_DATA_PATH"
|
||||||
;DATADIR = queues/
|
;DATADIR = queues/common
|
||||||
;;
|
;;
|
||||||
;; Default queue length before a channel queue will block
|
;; Default queue length before a channel queue will block
|
||||||
;LENGTH = 100000
|
;LENGTH = 100000
|
||||||
@@ -1684,7 +1681,7 @@ LEVEL = Info
|
|||||||
;; Batch size to send for batched queues
|
;; Batch size to send for batched queues
|
||||||
;BATCH_LENGTH = 20
|
;BATCH_LENGTH = 20
|
||||||
;;
|
;;
|
||||||
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb
|
;; When `TYPE` is `level`, this provides a directory for the underlying leveldb
|
||||||
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
|
;; 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.
|
;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR.
|
||||||
;CONN_STR =
|
;CONN_STR =
|
||||||
@@ -1752,7 +1749,6 @@ LEVEL = Info
|
|||||||
;ENABLE_OPENID_SIGNIN = false
|
;ENABLE_OPENID_SIGNIN = false
|
||||||
;;
|
;;
|
||||||
;; Whether to allow registering via OpenID
|
;; Whether to allow registering via OpenID
|
||||||
;; Do not include to rely on rhw DISABLE_REGISTRATION setting
|
|
||||||
;;ENABLE_OPENID_SIGNUP = false
|
;;ENABLE_OPENID_SIGNUP = false
|
||||||
;;
|
;;
|
||||||
;; Allowed URI patterns (POSIX regexp).
|
;; Allowed URI patterns (POSIX regexp).
|
||||||
@@ -1983,7 +1979,7 @@ LEVEL = Info
|
|||||||
;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory"
|
;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory"
|
||||||
;ADAPTER = memory
|
;ADAPTER = memory
|
||||||
;;
|
;;
|
||||||
;; For "memory" only, GC interval in seconds, default is 60
|
;; For "memory" and "twoqueue", GC interval in seconds, default is 60
|
||||||
;INTERVAL = 60
|
;INTERVAL = 60
|
||||||
;;
|
;;
|
||||||
;; For "redis" and "memcache", connection host address
|
;; For "redis" and "memcache", connection host address
|
||||||
@@ -2014,19 +2010,17 @@ LEVEL = Info
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
|
;; Either "memory", "file", "redis" or "db", default is "file". "db" will reuse the configuration in [database]
|
||||||
;; Default is "memory". "db" will reuse the configuration in [database]
|
;PROVIDER = file
|
||||||
;PROVIDER = memory
|
|
||||||
;;
|
;;
|
||||||
;; Provider config options
|
;; Provider config options
|
||||||
;; memory: doesn't have any config yet
|
;; memory: doesn't have any config yet
|
||||||
;; file: session file path, e.g. `data/sessions`, relative paths will be made absolute against _`AppWorkPath`_.
|
;; file: session file path, e.g. `data/sessions`, relative paths will be made absolute against _`AppWorkPath`_.
|
||||||
;; redis: default to [redis] CONN_STR
|
;; redis: default to [redis] CONN_STR
|
||||||
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
|
|
||||||
;PROVIDER_CONFIG =
|
;PROVIDER_CONFIG =
|
||||||
;;
|
;;
|
||||||
;; Session cookie name
|
;; Session cookie name
|
||||||
;COOKIE_NAME = i_like_gitea
|
;COOKIE_NAME = gitea_session
|
||||||
;;
|
;;
|
||||||
;; 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.
|
;; 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 =
|
;COOKIE_SECURE =
|
||||||
@@ -2052,7 +2046,8 @@ LEVEL = Info
|
|||||||
;; How Gitea deals with missing repository avatars
|
;; 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
|
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
|
||||||
;REPOSITORY_AVATAR_FALLBACK = none
|
;REPOSITORY_AVATAR_FALLBACK = none
|
||||||
;REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png
|
;; Image URL for the "image" fallback, used as-is, defaults to Gitea's builtin repository avatar
|
||||||
|
;REPOSITORY_AVATAR_FALLBACK_IMAGE =
|
||||||
;;
|
;;
|
||||||
;; Max Width and Height of uploaded avatars.
|
;; Max Width and Height of uploaded avatars.
|
||||||
;; This is to limit the amount of RAM used when resizing the image.
|
;; This is to limit the amount of RAM used when resizing the image.
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ 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.
|
- `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).
|
- `pam`: Enable support for PAM (Linux Pluggable Authentication Modules).
|
||||||
Can be used to authenticate local users or extend authentication to methods available to PAM.
|
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:
|
To include all assets, use the `bindata` tag:
|
||||||
|
|
||||||
@@ -33,13 +32,6 @@ To include all assets, use the `bindata` tag:
|
|||||||
TAGS="bindata" make build
|
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
|
## Changing default paths
|
||||||
|
|
||||||
Gitea will search for a number of things from the _`CustomPath`_.
|
Gitea will search for a number of things from the _`CustomPath`_.
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ Operations that must roll back together should run inside `db.WithTx()` (or
|
|||||||
Functions that participate in a transaction take a `context.Context` as their first
|
Functions that participate in a transaction take a `context.Context` as their first
|
||||||
parameter so the transaction can be propagated.
|
parameter so the transaction can be propagated.
|
||||||
|
|
||||||
|
PostgreSQL, MySQL and MSSQL (via `READ_COMMITTED_SNAPSHOT`) read the last committed
|
||||||
|
row version, so reads never wait for writers. Guard read-then-write logic with a
|
||||||
|
conditional `UPDATE` or a lock.
|
||||||
|
|
||||||
### XORM gotchas
|
### XORM gotchas
|
||||||
|
|
||||||
- Never call `x.Update(exemplar)` without an explicit `WHERE` clause — it updates
|
- Never call `x.Update(exemplar)` without an explicit `WHERE` clause — it updates
|
||||||
|
|||||||
+12
-10
@@ -17,6 +17,7 @@ import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts
|
|||||||
|
|
||||||
const jsExts = ['js', 'mjs', 'cjs'] as const;
|
const jsExts = ['js', 'mjs', 'cjs'] as const;
|
||||||
const tsExts = ['ts', 'mts', 'cts'] as const;
|
const tsExts = ['ts', 'mts', 'cts'] as const;
|
||||||
|
const vueExts = ['vue'] as const;
|
||||||
|
|
||||||
const restrictedGlobals = [
|
const restrictedGlobals = [
|
||||||
{name: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'},
|
{name: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'},
|
||||||
@@ -36,7 +37,7 @@ export default defineConfig([
|
|||||||
'public/assets/js',
|
'public/assets/js',
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
files: [`**/*.{${[...jsExts, ...tsExts].join(',')}}`],
|
files: [`**/*.{${[...jsExts, ...tsExts, ...vueExts].join(',')}}`],
|
||||||
ignores: ['dist/*'],
|
ignores: ['dist/*'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: 'latest',
|
||||||
@@ -48,7 +49,9 @@ export default defineConfig([
|
|||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
impliedStrict: true,
|
impliedStrict: true,
|
||||||
},
|
},
|
||||||
|
parser: typescriptParser,
|
||||||
project: true,
|
project: true,
|
||||||
|
extraFileExtensions: vueExts.map((ext) => `.${ext}`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
linterOptions: {
|
linterOptions: {
|
||||||
@@ -66,8 +69,11 @@ export default defineConfig([
|
|||||||
wc,
|
wc,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
'import-x/extensions': [...jsExts, ...tsExts].map((ext) => `.${ext}`),
|
'import-x/extensions': [...jsExts, ...tsExts, ...vueExts].map((ext) => `.${ext}`),
|
||||||
'import-x/parsers': {'@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`)},
|
'import-x/parsers': {
|
||||||
|
'@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`),
|
||||||
|
'vue-eslint-parser': vueExts.map((ext) => `.${ext}`),
|
||||||
|
},
|
||||||
'import-x/resolver': {'typescript': true},
|
'import-x/resolver': {'typescript': true},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
@@ -958,7 +964,7 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-iterable-in-constructor': [2],
|
'unicorn/prefer-iterable-in-constructor': [2],
|
||||||
'unicorn/prefer-iterator-concat': [0], // too opinionated
|
'unicorn/prefer-iterator-concat': [0], // too opinionated
|
||||||
'unicorn/prefer-iterator-helpers': [0],
|
'unicorn/prefer-iterator-helpers': [0],
|
||||||
'unicorn/prefer-iterator-to-array': [2],
|
'unicorn/prefer-iterator-to-array': [0], // Iterator#toArray requires ES2025
|
||||||
'unicorn/prefer-iterator-to-array-at-end': [2],
|
'unicorn/prefer-iterator-to-array-at-end': [2],
|
||||||
'unicorn/prefer-iterator-zip': [0],
|
'unicorn/prefer-iterator-zip': [0],
|
||||||
'unicorn/prefer-json-import': [0],
|
'unicorn/prefer-json-import': [0],
|
||||||
@@ -1096,17 +1102,13 @@ export default defineConfig([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.vue'],
|
files: vueExts.map((ext) => `**/*.${ext}`),
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extends: [
|
extends: [
|
||||||
vue.configs['flat/recommended'],
|
vue.configs['flat/recommended'],
|
||||||
vueScopedCss.configs.recommended,
|
vueScopedCss.configs.recommended,
|
||||||
],
|
],
|
||||||
rules: {
|
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/attributes-order': [0],
|
||||||
'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}],
|
'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}],
|
||||||
'vue/max-attributes-per-line': [0],
|
'vue/max-attributes-per-line': [0],
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ toolchain go1.27.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
connectrpc.com/connect v1.21.0
|
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/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||||
gitea.dev/actionslib v1.2.1
|
gitea.dev/actionslib v1.2.1
|
||||||
@@ -25,6 +21,7 @@ require (
|
|||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||||
github.com/blevesearch/bleve/v2 v2.6.1
|
github.com/blevesearch/bleve/v2 v2.6.1
|
||||||
github.com/bohde/codel v0.2.0
|
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/buildkite/terminal-to-html/v3 v3.17.1
|
||||||
github.com/caddyserver/certmagic v0.25.4
|
github.com/caddyserver/certmagic v0.25.4
|
||||||
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33
|
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260812203852-971c0284dc33
|
||||||
@@ -35,7 +32,6 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.1.0
|
github.com/dustin/go-humanize v1.1.0
|
||||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5
|
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5
|
||||||
github.com/emersion/go-imap v1.2.1
|
github.com/emersion/go-imap v1.2.1
|
||||||
github.com/emirpasic/gods v1.18.1
|
|
||||||
github.com/felixge/fgprof v0.9.5
|
github.com/felixge/fgprof v0.9.5
|
||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/getkin/kin-openapi v0.149.0
|
github.com/getkin/kin-openapi v0.149.0
|
||||||
@@ -43,8 +39,6 @@ require (
|
|||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-co-op/gocron/v2 v2.22.0
|
github.com/go-co-op/gocron/v2 v2.22.0
|
||||||
github.com/go-enry/go-enry/v2 v2.9.6
|
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-ldap/ldap/v3 v3.4.14
|
||||||
github.com/go-redsync/redsync/v4 v4.18.0
|
github.com/go-redsync/redsync/v4 v4.18.0
|
||||||
github.com/go-sql-driver/mysql v1.10.1
|
github.com/go-sql-driver/mysql v1.10.1
|
||||||
@@ -109,15 +103,13 @@ require (
|
|||||||
modernc.org/sqlite v1.59.0
|
modernc.org/sqlite v1.59.0
|
||||||
mvdan.cc/xurls/v2 v2.6.0
|
mvdan.cc/xurls/v2 v2.6.0
|
||||||
xorm.io/builder v0.3.13
|
xorm.io/builder v0.3.13
|
||||||
xorm.io/xorm v1.4.1
|
xorm.io/xorm v1.4.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
github.com/DataDog/zstd v1.5.7 // 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/RoaringBitmap/roaring/v2 v2.18.2 // indirect
|
||||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||||
@@ -148,18 +140,13 @@ require (
|
|||||||
github.com/bodgit/sevenzip v1.6.4 // indirect
|
github.com/bodgit/sevenzip v1.6.4 // indirect
|
||||||
github.com/bodgit/windows v1.0.1 // indirect
|
github.com/bodgit/windows v1.0.1 // indirect
|
||||||
github.com/boombuler/barcode v1.1.0 // 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/caddyserver/zerossl v0.1.5 // indirect
|
||||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // 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/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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||||
@@ -168,7 +155,6 @@ require (
|
|||||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
|
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-asn1-ber/asn1-ber v1.5.8 // indirect
|
||||||
github.com/go-enry/go-oniguruma v1.2.1 // 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/jsonpointer v0.23.1 // indirect
|
||||||
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
|
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
@@ -176,9 +162,7 @@ require (
|
|||||||
github.com/goccy/go-json v0.10.6 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // 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/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-querystring v1.2.0 // indirect
|
||||||
github.com/google/go-tpm v0.9.8 // indirect
|
github.com/google/go-tpm v0.9.8 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
@@ -188,10 +172,8 @@ require (
|
|||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
github.com/inbucket/html2text v1.0.0 // 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/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // 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/cpuid/v2 v2.4.0 // indirect
|
||||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
@@ -219,9 +201,9 @@ require (
|
|||||||
github.com/olekukonko/ll v0.1.8 // indirect
|
github.com/olekukonko/ll v0.1.8 // indirect
|
||||||
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
||||||
github.com/onsi/ginkgo v1.16.5 // 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/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.27 // 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/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
@@ -232,15 +214,12 @@ require (
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.10.2 // 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/sorairolake/lzip-go v0.3.8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/stangelandcl/ppmd v0.1.1 // indirect
|
github.com/stangelandcl/ppmd v0.1.1 // indirect
|
||||||
github.com/tinylib/msgp v1.6.4 // 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/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/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||||
@@ -251,10 +230,10 @@ require (
|
|||||||
go.uber.org/zap/exp v0.3.0 // indirect
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
go4.org v0.0.0-20260112195520-a5071408f32f // 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/time v0.15.0 // indirect
|
||||||
golang.org/x/tools v0.49.0 // indirect
|
golang.org/x/tools v0.49.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
|
||||||
modernc.org/libc v1.75.7 // indirect
|
modernc.org/libc v1.75.7 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.12.1 // indirect
|
modernc.org/memory v1.12.1 // indirect
|
||||||
|
|||||||
@@ -4,18 +4,8 @@ code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
|||||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
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 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs=
|
||||||
connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
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 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
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 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
|
||||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
|
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
|
||||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
|
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
|
||||||
@@ -46,9 +36,6 @@ 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/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 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
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 h1:3JhDl+JysaO8nhNU1XMaw35VSGjV4IEQAefaG4Lyok4=
|
||||||
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347/go.mod h1:2ErI0aycD43Ufr6CFK5lT/NrHGmoZuVbn1nlPThw69o=
|
github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347/go.mod h1:2ErI0aycD43Ufr6CFK5lT/NrHGmoZuVbn1nlPThw69o=
|
||||||
github.com/ProtonMail/go-crypto v1.5.0 h1:sKmuvjOgsnrtpMvZ+84MCnTJCpjxZ1qCFn076lz1yT0=
|
github.com/ProtonMail/go-crypto v1.5.0 h1:sKmuvjOgsnrtpMvZ+84MCnTJCpjxZ1qCFn076lz1yT0=
|
||||||
@@ -75,10 +62,6 @@ 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/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 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
|
||||||
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
|
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 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -173,17 +156,8 @@ 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/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 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
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 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
@@ -205,8 +179,6 @@ 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/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 h1:MTcuJQkIFRLfNn9FfAvdO0p3FhNyaCf+IGTwy0TxX6E=
|
||||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.5/go.mod h1:SizrS3EM1vFF0v/JZlJ5LfK6tXhwln2823YZwNh/vBE=
|
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 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
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=
|
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
||||||
@@ -214,8 +186,6 @@ 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 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
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/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 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||||
@@ -230,8 +200,6 @@ 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/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 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
|
||||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
|
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 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
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=
|
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
@@ -245,14 +213,6 @@ 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-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 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
|
||||||
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
|
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
|
||||||
github.com/go-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 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
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=
|
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
|
||||||
@@ -297,8 +257,6 @@ 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/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 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
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.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/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
@@ -314,9 +272,6 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
|||||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
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 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
|
||||||
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
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.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.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
@@ -343,8 +298,6 @@ 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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
@@ -381,8 +334,6 @@ 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/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 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs=
|
||||||
github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU=
|
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 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
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=
|
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||||
@@ -402,13 +353,8 @@ 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/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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
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 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
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.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||||
@@ -526,8 +472,6 @@ 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/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 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
||||||
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
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 h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -573,16 +517,8 @@ 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/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 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
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 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
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 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
@@ -606,7 +542,6 @@ 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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
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.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
@@ -619,8 +554,6 @@ 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.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=
|
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/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 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
|
||||||
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
|
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=
|
github.com/urfave/cli/v3 v3.13.0 h1:Dr6jqMfIyyFsRVn7Nz5mqLsMY+ZMpfh3a0aMs+umPVY=
|
||||||
@@ -629,8 +562,6 @@ 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/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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
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 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
@@ -681,11 +612,6 @@ 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-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-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-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 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
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=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
@@ -694,10 +620,6 @@ 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/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.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.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 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
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=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -706,14 +628,8 @@ 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-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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-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.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.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 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||||
@@ -723,11 +639,6 @@ 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-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-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.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 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
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=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -736,37 +647,22 @@ 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-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-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-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-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-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-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-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-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-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-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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-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.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 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
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=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -774,11 +670,6 @@ 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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.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.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 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
@@ -787,9 +678,6 @@ 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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
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 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -817,8 +705,6 @@ 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/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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
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.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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
@@ -862,5 +748,5 @@ pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0=
|
|||||||
pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
|
pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
|
||||||
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
||||||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||||
xorm.io/xorm v1.4.1 h1:m7QlNd0eBGb31IV4Q/ow0Du83rtdC1CiwlvJZGvYde8=
|
xorm.io/xorm v1.4.3 h1:MwWFWzVr+/6D07qGCDhBAfABcuT0gvqY3XmTy1215BM=
|
||||||
xorm.io/xorm v1.4.1/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
xorm.io/xorm v1.4.3/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
||||||
|
|||||||
@@ -429,6 +429,9 @@ func prepareMigrationTasks() []*migration {
|
|||||||
newMigration(353, "Add audit event table", v28.AddAuditEventTable),
|
newMigration(353, "Add audit event table", v28.AddAuditEventTable),
|
||||||
newMigration(354, "Add Actions job queue indexes", v28.AddActionQueueIndexes),
|
newMigration(354, "Add Actions job queue indexes", v28.AddActionQueueIndexes),
|
||||||
newMigration(355, "Add AutoMerge merged_commit_id column", v28.AddAutoMergeMergedCommitID),
|
newMigration(355, "Add AutoMerge merged_commit_id column", v28.AddAutoMergeMergedCommitID),
|
||||||
|
// Gitea 28.0.0 ends at migration ID number 355 (database version 356)
|
||||||
|
|
||||||
|
newMigration(356, "Add index on action_run commit_sha", v28.AddActionRunCommitSHAIndex),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v28
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.dev/modelmigration/base"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddActionRunCommitSHAIndex indexes the runs lookup by commit, which the API `head_sha` filter uses.
|
||||||
|
func AddActionRunCommitSHAIndex(_ context.Context, x base.EngineMigration) error {
|
||||||
|
type ActionRun struct {
|
||||||
|
CommitSHA string `xorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
IgnoreConstrains: true,
|
||||||
|
}, new(ActionRun))
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ type ActionRun struct {
|
|||||||
ScheduleID int64
|
ScheduleID int64
|
||||||
Ref string `xorm:"index"` // the commit/tag/… that caused the run
|
Ref string `xorm:"index"` // the commit/tag/… that caused the run
|
||||||
IsRefDeleted bool `xorm:"-"`
|
IsRefDeleted bool `xorm:"-"`
|
||||||
CommitSHA string
|
CommitSHA string `xorm:"index"`
|
||||||
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
|
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
|
||||||
NeedApproval bool // may need approval if it's a fork pull request
|
NeedApproval bool // may need approval if it's a fork pull request
|
||||||
ApprovedBy int64 `xorm:"index"` // who approved
|
ApprovedBy int64 `xorm:"index"` // who approved
|
||||||
@@ -295,9 +295,8 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
|
|||||||
return &run, nil
|
return &run, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) ([]*ActionRun, error) {
|
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) (runs []*ActionRun, err error) {
|
||||||
var runs []*ActionRun
|
err = db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).OrderBy("id").Find(&runs)
|
||||||
err := db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).Find(&runs)
|
|
||||||
return runs, err
|
return runs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ func (r *ActionRunner) GenerateAndFillToken() {
|
|||||||
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
|
// 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
|
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
|
||||||
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
|
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
|
||||||
return !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) })
|
return len(jobRunsOn) > 0 && !slices.ContainsFunc(jobRunsOn, func(label string) bool { return !util.SliceContainsString(r.AgentLabels, label, true) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -86,4 +86,5 @@ func TestCanMatchLabelsCaseInsensitive(t *testing.T) {
|
|||||||
runner := &ActionRunner{AgentLabels: []string{"self-hosted", "Linux", "X64"}}
|
runner := &ActionRunner{AgentLabels: []string{"self-hosted", "Linux", "X64"}}
|
||||||
assert.True(t, runner.CanMatchLabels([]string{"SELF-HOSTED", "linux"}))
|
assert.True(t, runner.CanMatchLabels([]string{"SELF-HOSTED", "linux"}))
|
||||||
assert.False(t, runner.CanMatchLabels([]string{"linux", "arm64"}))
|
assert.False(t, runner.CanMatchLabels([]string{"linux", "arm64"}))
|
||||||
|
assert.False(t, runner.CanMatchLabels(nil))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,6 +298,12 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
|
if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
// A short page means no waiting jobs remain beyond it.
|
||||||
|
isLastPage := len(jobs) < pickTaskBatchSize
|
||||||
|
if !isLastPage {
|
||||||
|
last := jobs[len(jobs)-1] // read before a lost claim bumps Updated
|
||||||
|
cursorUpdated, cursorID = last.Updated, last.ID
|
||||||
|
}
|
||||||
|
|
||||||
for _, v := range jobs {
|
for _, v := range jobs {
|
||||||
if !runner.CanMatchLabels(v.RunsOn) {
|
if !runner.CanMatchLabels(v.RunsOn) {
|
||||||
@@ -313,12 +319,9 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
// Another runner claimed this job concurrently; try the next one.
|
// Another runner claimed this job concurrently; try the next one.
|
||||||
}
|
}
|
||||||
|
|
||||||
// A short page means no waiting jobs remain beyond it.
|
if isLastPage {
|
||||||
if len(jobs) < pickTaskBatchSize {
|
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
last := jobs[len(jobs)-1]
|
|
||||||
cursorUpdated, cursorID = last.Updated, last.ID
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,23 +83,17 @@ func GetActivityStats(ctx context.Context, repo *repo_model.Repository, timeFrom
|
|||||||
|
|
||||||
// GetActivityStatsTopAuthors returns top author stats for git commits for all branches
|
// 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) {
|
func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository, timeFrom time.Time, count int) ([]*ActivityAuthorData, error) {
|
||||||
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo)
|
authors, err := git.GetCodeActivityAuthors(ctx, repo, timeFrom)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("OpenRepository: %w", err)
|
|
||||||
}
|
|
||||||
defer closer.Close()
|
|
||||||
|
|
||||||
code, err := gitRepo.GetCodeActivityStats(ctx, timeFrom, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("FillFromGit: %w", err)
|
return nil, fmt.Errorf("FillFromGit: %w", err)
|
||||||
}
|
}
|
||||||
if code.Authors == nil {
|
if authors == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
users := make(map[int64]*ActivityAuthorData)
|
users := make(map[int64]*ActivityAuthorData)
|
||||||
var unknownUserID int64
|
var unknownUserID int64
|
||||||
unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx)
|
unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx)
|
||||||
for _, v := range code.Authors {
|
for _, v := range authors {
|
||||||
if len(v.Email) == 0 {
|
if len(v.Email) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -564,6 +564,12 @@ func (grant *OAuth2Grant) SetNonce(ctx context.Context, nonce string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateGrantScope(ctx context.Context, grant *OAuth2Grant, newScope string) error {
|
||||||
|
grant.Scope = newScope
|
||||||
|
_, err := db.GetEngine(ctx).ID(grant.ID).Cols("scope").Update(grant)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// GetOAuth2GrantByID returns the grant with the given ID
|
// GetOAuth2GrantByID returns the grant with the given ID
|
||||||
func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) {
|
func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) {
|
||||||
grant = new(OAuth2Grant)
|
grant = new(OAuth2Grant)
|
||||||
|
|||||||
+29
-72
@@ -5,7 +5,6 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
@@ -13,49 +12,46 @@ import (
|
|||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Session represents a session compatible for go-chi session
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
Key string `xorm:"pk CHAR(16)"` // has to be Key to match with go-chi/session
|
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 - this may need to be increased
|
Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb
|
||||||
Expiry timeutil.TimeStamp // has to be Expiry to match with go-chi/session
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DbSessionLastAccessTime = "expiry" // maybe we can make a deeper clean up in the future, just keep this PR focused
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
db.RegisterModel(new(Session))
|
db.RegisterModel(new(Session))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSession updates the session with provided id
|
// 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) error {
|
func UpdateSession(ctx context.Context, key string, data []byte, create bool) error {
|
||||||
_, err := db.GetEngine(ctx).ID(key).Update(&Session{
|
session := &Session{Key: key, Data: data, LastAccessTime: timeutil.TimeStampNow()}
|
||||||
Data: data,
|
update := func() (int64, error) {
|
||||||
Expiry: timeutil.TimeStampNow(),
|
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()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadSession reads the data for the provided session
|
func UpdateSessionLastAccessTime(ctx context.Context, key string) error {
|
||||||
func ReadSession(ctx context.Context, key string) (*Session, error) {
|
_, err := db.GetEngine(ctx).ID(key).Cols(DbSessionLastAccessTime).Update(&Session{LastAccessTime: timeutil.TimeStampNow()})
|
||||||
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
|
return err
|
||||||
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
|
// DestroySession destroys a session
|
||||||
@@ -66,47 +62,8 @@ func DestroySession(ctx context.Context, key string) error {
|
|||||||
return err
|
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
|
// CleanupSessions cleans up expired sessions
|
||||||
func CleanupSessions(ctx context.Context, maxLifetime int64) error {
|
func CleanupSessions(ctx context.Context, maxLifetime int64) error {
|
||||||
_, err := db.GetEngine(ctx).Where("expiry <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
|
_, err := db.GetEngine(ctx).Where(DbSessionLastAccessTime+" <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
@@ -59,6 +61,11 @@ func InitEngine(ctx context.Context) error {
|
|||||||
xe.SetMaxIdleConns(setting.Database.MaxIdleConns)
|
xe.SetMaxIdleConns(setting.Database.MaxIdleConns)
|
||||||
xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||||
|
|
||||||
|
if setting.Database.Type.IsMySQL() {
|
||||||
|
// like PostgreSQL and MSSQL, avoids MariaDB snapshot isolation errors
|
||||||
|
xe.SetDefaultTxOptions(&sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||||
|
}
|
||||||
|
|
||||||
if setting.Database.SlowQueryThreshold > 0 {
|
if setting.Database.SlowQueryThreshold > 0 {
|
||||||
xe.AddHook(&EngineHook{
|
xe.AddHook(&EngineHook{
|
||||||
Threshold: setting.Database.SlowQueryThreshold,
|
Threshold: setting.Database.SlowQueryThreshold,
|
||||||
@@ -103,6 +110,10 @@ func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Conte
|
|||||||
|
|
||||||
preprocessDatabaseCollation(xormEngine)
|
preprocessDatabaseCollation(xormEngine)
|
||||||
|
|
||||||
|
if setting.Database.Type.IsMSSQL() {
|
||||||
|
enableMSSQLReadCommittedSnapshot(ctx, xormEngine)
|
||||||
|
}
|
||||||
|
|
||||||
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
|
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
|
||||||
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
|
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
|
||||||
//
|
//
|
||||||
@@ -125,3 +136,12 @@ func InitEngineWithMigration(ctx context.Context, migrateFunc func(context.Conte
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enableMSSQLReadCommittedSnapshot stops MSSQL reads waiting on writers, like PostgreSQL and MySQL
|
||||||
|
func enableMSSQLReadCommittedSnapshot(ctx context.Context, engine EngineMigration) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // ALTER waits for all other connections to close
|
||||||
|
defer cancel()
|
||||||
|
if _, err := engine.Context(ctx).Exec("IF (SELECT is_read_committed_snapshot_on FROM sys.databases WHERE database_id = DB_ID()) = 0 ALTER DATABASE CURRENT SET READ_COMMITTED_SNAPSHOT ON"); err != nil {
|
||||||
|
log.Error("Unable to set READ_COMMITTED_SNAPSHOT=ON: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -311,17 +311,17 @@ func (opts *CommitStatusOptions) ToConds() builder.Cond {
|
|||||||
func (opts *CommitStatusOptions) ToOrders() string {
|
func (opts *CommitStatusOptions) ToOrders() string {
|
||||||
switch opts.SortType {
|
switch opts.SortType {
|
||||||
case "oldest":
|
case "oldest":
|
||||||
return "created_unix ASC"
|
return "created_unix ASC, `index` ASC"
|
||||||
case "recentupdate":
|
case "recentupdate":
|
||||||
return "updated_unix DESC"
|
return "updated_unix DESC, `index` DESC"
|
||||||
case "leastupdate":
|
case "leastupdate":
|
||||||
return "updated_unix ASC"
|
return "updated_unix ASC, `index` ASC"
|
||||||
case "leastindex":
|
case "leastindex":
|
||||||
return "`index` DESC"
|
return "`index` DESC"
|
||||||
case "highestindex":
|
case "highestindex":
|
||||||
return "`index` ASC"
|
return "`index` ASC"
|
||||||
default:
|
default:
|
||||||
return "created_unix DESC"
|
return "created_unix DESC, `index` DESC" // timestamps have 1s resolution, `index` keeps paging stable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,28 +32,15 @@ func TestGetCommitStatuses(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 5, int(maxResults))
|
assert.Equal(t, 5, int(maxResults))
|
||||||
assert.Len(t, statuses, 5)
|
var indexes []int64
|
||||||
|
for _, status := range statuses {
|
||||||
assert.Equal(t, "ci/awesomeness", statuses[0].Context)
|
indexes = append(indexes, status.Index)
|
||||||
assert.Equal(t, commitstatus.CommitStatusPending, statuses[0].State)
|
}
|
||||||
|
assert.Equal(t, []int64{5, 4, 3, 2, 1}, indexes)
|
||||||
|
assert.Equal(t, "deploy/awesomeness", statuses[0].Context)
|
||||||
|
assert.Equal(t, commitstatus.CommitStatusError, statuses[0].State)
|
||||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(t.Context()))
|
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(t.Context()))
|
||||||
|
|
||||||
assert.Equal(t, "cov/awesomeness", statuses[1].Context)
|
|
||||||
assert.Equal(t, commitstatus.CommitStatusWarning, statuses[1].State)
|
|
||||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[1].APIURL(t.Context()))
|
|
||||||
|
|
||||||
assert.Equal(t, "cov/awesomeness", statuses[2].Context)
|
|
||||||
assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State)
|
|
||||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[2].APIURL(t.Context()))
|
|
||||||
|
|
||||||
assert.Equal(t, "ci/awesomeness", statuses[3].Context)
|
|
||||||
assert.Equal(t, commitstatus.CommitStatusFailure, statuses[3].State)
|
|
||||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[3].APIURL(t.Context()))
|
|
||||||
|
|
||||||
assert.Equal(t, "deploy/awesomeness", statuses[4].Context)
|
|
||||||
assert.Equal(t, commitstatus.CommitStatusError, statuses[4].State)
|
|
||||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(t.Context()))
|
|
||||||
|
|
||||||
statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](t.Context(), &git_model.CommitStatusOptions{
|
statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](t.Context(), &git_model.CommitStatusOptions{
|
||||||
ListOptions: db.ListOptions{Page: 2, PageSize: 50},
|
ListOptions: db.ListOptions{Page: 2, PageSize: 50},
|
||||||
RepoID: repo1.ID,
|
RepoID: repo1.ID,
|
||||||
|
|||||||
@@ -123,23 +123,13 @@ func (protectBranch *ProtectedBranch) LoadRepo(ctx context.Context) (err error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CanUserPush returns if some user could push to this protected branch
|
// CanUserPush returns if some user could push to this protected branch
|
||||||
func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User) bool {
|
func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User, permissionInRepo access_model.Permission) bool {
|
||||||
if !protectBranch.CanPush {
|
if !protectBranch.CanPush {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !protectBranch.EnableWhitelist {
|
if !protectBranch.EnableWhitelist {
|
||||||
if err := protectBranch.LoadRepo(ctx); err != nil {
|
return permissionInRepo.CanWrite(unit.TypeCode)
|
||||||
log.Error("LoadRepo: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
writeAccess, err := access_model.HasAccessUnit(ctx, user, protectBranch.Repo, unit.TypeCode, perm.AccessModeWrite)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("HasAccessUnit: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return writeAccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(protectBranch.WhitelistUserIDs, user.ID) {
|
if slices.Contains(protectBranch.WhitelistUserIDs, user.ID) {
|
||||||
@@ -160,17 +150,17 @@ func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *use
|
|||||||
|
|
||||||
// CanUserForcePush returns if some user could force push to this protected branch
|
// CanUserForcePush returns if some user could force push to this protected branch
|
||||||
// Since force-push extends normal push, we also check if user has regular push access
|
// Since force-push extends normal push, we also check if user has regular push access
|
||||||
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User) bool {
|
func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user *user_model.User, permissionInRepo access_model.Permission) bool {
|
||||||
if !protectBranch.CanForcePush {
|
if !protectBranch.CanForcePush {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !protectBranch.EnableForcePushAllowlist {
|
if !protectBranch.EnableForcePushAllowlist {
|
||||||
return protectBranch.CanUserPush(ctx, user)
|
return protectBranch.CanUserPush(ctx, user, permissionInRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) {
|
if slices.Contains(protectBranch.ForcePushAllowlistUserIDs, user.ID) {
|
||||||
return protectBranch.CanUserPush(ctx, user)
|
return protectBranch.CanUserPush(ctx, user, permissionInRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 {
|
if len(protectBranch.ForcePushAllowlistTeamIDs) == 0 {
|
||||||
@@ -182,7 +172,7 @@ func (protectBranch *ProtectedBranch) CanUserForcePush(ctx context.Context, user
|
|||||||
log.Error("IsUserInTeams: %v", err)
|
log.Error("IsUserInTeams: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return in && protectBranch.CanUserPush(ctx, user)
|
return in && protectBranch.CanUserPush(ctx, user, permissionInRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
|
// IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch
|
||||||
|
|||||||
@@ -407,6 +407,21 @@ func (pr *PullRequest) GetGitHeadRefName() string { // TODO: make it return RefN
|
|||||||
return git.RefNameFromPullIndex(pr.Index).String()
|
return git.RefNameFromPullIndex(pr.Index).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pr *PullRequest) GetInstructionsCliArgs() (ret struct {
|
||||||
|
BaseBranchArg string
|
||||||
|
HeadBranchArg string
|
||||||
|
LocalBranchArg string
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
ret.BaseBranchArg = util.ShellEscape(pr.BaseBranch)
|
||||||
|
ret.HeadBranchArg = util.ShellEscape(pr.HeadBranch)
|
||||||
|
ret.LocalBranchArg = ret.HeadBranchArg
|
||||||
|
if pr.HeadRepo != nil && pr.HeadRepoID != pr.BaseRepoID {
|
||||||
|
ret.LocalBranchArg = util.ShellEscape(pr.HeadRepo.OwnerName) + "-" + ret.HeadBranchArg
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
|
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
|
||||||
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
|
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
|
||||||
opts := FindCommentsOptions{
|
opts := FindCommentsOptions{
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool {
|
|||||||
c.gitRepo, c.gitRepoCloser = r, closer
|
c.gitRepo, c.gitRepoCloser = r, closer
|
||||||
}
|
}
|
||||||
|
|
||||||
exist = c.gitRepo.IsReferenceExist(c.ctx, commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition.
|
exist = c.gitRepo.IsReferenceExist(c.ctx, commitID)
|
||||||
c.commitCache[commitID] = exist
|
c.commitCache[commitID] = exist
|
||||||
return exist
|
return exist
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,13 +78,13 @@ func (m *PushMirror) GetRemoteName() string {
|
|||||||
return m.RemoteName
|
return m.RemoteName
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePushMirror updates the push-mirror
|
// UpdatePushMirrorSyncStatus updates the sync status (last update time and last error) of the push-mirror
|
||||||
func UpdatePushMirror(ctx context.Context, m *PushMirror) error {
|
func UpdatePushMirrorSyncStatus(ctx context.Context, m *PushMirror) error {
|
||||||
_, err := db.GetEngine(ctx).ID(m.ID).AllCols().Update(m)
|
_, err := db.GetEngine(ctx).ID(m.ID).Cols("last_update", "last_error").Update(m)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePushMirrorInterval updates the push-mirror
|
// UpdatePushMirrorInterval updates the sync interval of the push-mirror
|
||||||
func UpdatePushMirrorInterval(ctx context.Context, m *PushMirror) error {
|
func UpdatePushMirrorInterval(ctx context.Context, m *PushMirror) error {
|
||||||
_, err := db.GetEngine(ctx).ID(m.ID).Cols("interval").Update(m)
|
_, err := db.GetEngine(ctx).ID(m.ID).Cols("interval").Update(m)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ 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
|
// 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"
|
const keepFile = ".keep"
|
||||||
|
|
||||||
// find and delete all untracked files
|
// find and delete all untracked files
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type SearchUserOptions struct {
|
|||||||
Types []UserType
|
Types []UserType
|
||||||
UID int64
|
UID int64
|
||||||
LoginName string // this option should be used only for admin user
|
LoginName string // this option should be used only for admin user
|
||||||
SourceID int64 // 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
|
||||||
OrderBy db.SearchOrderBy
|
OrderBy db.SearchOrderBy
|
||||||
Visible []structs.VisibleType
|
Visible []structs.VisibleType
|
||||||
Actor *User // The user doing the search
|
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})
|
cond = cond.And(builder.Eq{"id": opts.UID})
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.SourceID > 0 {
|
if opts.SourceID.Has() {
|
||||||
cond = cond.And(builder.Eq{"login_source": opts.SourceID})
|
cond = cond.And(builder.Eq{"login_source": opts.SourceID.Value()})
|
||||||
}
|
}
|
||||||
if opts.LoginName != "" {
|
if opts.LoginName != "" {
|
||||||
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
|
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
|
||||||
|
|||||||
@@ -225,7 +225,6 @@ func replaceScalars(node *yaml.Node, replace func(string) string) {
|
|||||||
// buildMatrixCombos builds one Job per matrix combination from src, baking the combination into the
|
// 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.
|
// strategy and interpolating the name, runs-on and continue-on-error with it.
|
||||||
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) {
|
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))
|
order, names := make([]int, len(matrixes)), make([]string, len(matrixes))
|
||||||
for index, matrix := range matrixes {
|
for index, matrix := range matrixes {
|
||||||
order[index], names[index] = index, matrixName(matrix)
|
order[index], names[index] = index, matrixName(matrix)
|
||||||
@@ -259,15 +258,16 @@ func buildMatrixCombos(jobID string, src *Job, matrixes []map[string]any, gitCtx
|
|||||||
if err := evaluator.EvaluateYamlNode(&rawRunsOn); err != nil {
|
if err := evaluator.EvaluateYamlNode(&rawRunsOn); err != nil {
|
||||||
return nil, fmt.Errorf("interpolate runs-on for job %q: %w", jobID, err)
|
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)
|
runsOn := model.RunsOnFromNode(rawRunsOn)
|
||||||
if len(runsOn) == 0 && len(srcRunsOn) > 0 { // match no runner rather than every runner
|
|
||||||
runsOn = []string{""}
|
|
||||||
}
|
|
||||||
for i := range runsOn {
|
for i := range runsOn {
|
||||||
runsOn[i] = escapeExpressions(runsOn[i])
|
runsOn[i] = escapeExpressions(runsOn[i])
|
||||||
}
|
}
|
||||||
combo.RawRunsOn = model.RunsOnNode(runsOn, "")
|
combo.RawRunsOn = model.RunsOnNode(runsOn, "")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if err := evaluator.EvaluateYamlNode(&combo.RawContinueOnError); err != nil {
|
if err := evaluator.EvaluateYamlNode(&combo.RawContinueOnError); err != nil {
|
||||||
return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", jobID, err)
|
return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", jobID, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,17 +290,26 @@ func TestParseInterpolatesRunName(t *testing.T) {
|
|||||||
assert.Empty(t, result[0].RunName)
|
assert.Empty(t, result[0].RunName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseRunsOnFromJSONArray(t *testing.T) {
|
func TestParseRunsOnFromJSONKeepsWhatGitHubRejectsForTheJobToFail(t *testing.T) {
|
||||||
content := []byte("on: push\njobs:\n build:\n runs-on: ${{ fromJSON(vars.RUNNER) }}\n steps: [{run: echo}]\n")
|
content := []byte("on: push\njobs:\n build:\n runs-on: ${{ fromJSON(vars.RUNNER) }}\n steps: [{run: echo}]\n")
|
||||||
_, err := Parse(content)
|
_, err := Parse(content)
|
||||||
require.NoError(t, err)
|
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}))
|
result, err := Parse(content, WithGitContext(&model.GithubContext{}), WithVars(map[string]string{"RUNNER": runner}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, result, 1)
|
require.Len(t, result, 1)
|
||||||
_, job := result[0].Job()
|
_, job := result[0].Job()
|
||||||
assert.Equal(t, want, job.RunsOn(), runner)
|
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 TestJobFieldsWithoutMatrix(t *testing.T) {
|
func TestJobFieldsWithoutMatrix(t *testing.T) {
|
||||||
@@ -458,6 +467,37 @@ 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) {
|
func TestRejectsUnevaluatedMatrixFilters(t *testing.T) {
|
||||||
for _, filter := range []string{"include", "exclude"} {
|
for _, filter := range []string{"include", "exclude"} {
|
||||||
t.Run(filter, func(t *testing.T) {
|
t.Run(filter, func(t *testing.T) {
|
||||||
|
|||||||
@@ -174,6 +174,14 @@ func (j *Job) EraseNeeds() *Job {
|
|||||||
return j
|
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.
|
// RunsOn returns the labels Gitea matches runners against, unescaped like DisplayName.
|
||||||
func (j *Job) RunsOn() []string {
|
func (j *Job) RunsOn() []string {
|
||||||
runsOn := model.RunsOnFromNode(j.RawRunsOn)
|
runsOn := model.RunsOnFromNode(j.RawRunsOn)
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/expreval"
|
"gitea.dev/actionslib/pkg/expreval"
|
||||||
"gitea.dev/actionslib/pkg/exprparser"
|
"gitea.dev/actionslib/pkg/exprparser"
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"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.
|
// jobConditionContexts are what github.com gives `jobs.<job_id>.if`, which it decides before the matrix, plus the `gitea` alias.
|
||||||
@@ -21,7 +24,7 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Keep unknown and case-distinct keys accepted for existing Gitea workflows.
|
// Keep unknown and case-distinct keys outside of jobs accepted for existing Gitea workflows.
|
||||||
workflow, err := readWorkflowDoc(doc)
|
workflow, err := readWorkflowDoc(doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -33,6 +36,9 @@ func ValidateWorkflowStatic(content []byte) ([]*Event, error) {
|
|||||||
if err := validateWorkflowStructure(workflow); err != nil {
|
if err := validateWorkflowStructure(workflow); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := validateJobKinds(doc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var header struct {
|
var header struct {
|
||||||
RunName string `yaml:"run-name"`
|
RunName string `yaml:"run-name"`
|
||||||
}
|
}
|
||||||
@@ -76,7 +82,6 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
|
|||||||
if job == nil {
|
if job == nil {
|
||||||
return fmt.Errorf("job %q has no configuration", id)
|
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() {
|
for _, dependency := range job.Needs() {
|
||||||
if _, ok := workflow.Jobs[dependency]; !ok {
|
if _, ok := workflow.Jobs[dependency]; !ok {
|
||||||
return fmt.Errorf("job %q needs unknown job %q", id, dependency)
|
return fmt.Errorf("job %q needs unknown job %q", id, dependency)
|
||||||
@@ -110,3 +115,116 @@ func validateWorkflowStructure(workflow *model.Workflow) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,17 +31,22 @@ jobs:
|
|||||||
func TestReadWorkflowEventsStaticErrors(t *testing.T) {
|
func TestReadWorkflowEventsStaticErrors(t *testing.T) {
|
||||||
for content, static := range map[string]bool{
|
for content, static := range map[string]bool{
|
||||||
"on: push\njobs: {}": true,
|
"on: push\njobs: {}": true,
|
||||||
"on: push\njobs: {test: {needs: absent}}": true,
|
"on: push\njobs: {test: {runs-on: x, needs: absent}}": true,
|
||||||
"on: push\njobs: {one: {needs: two}, two: {needs: one}}": true,
|
"on: push\njobs: {one: {runs-on: x, needs: two}, two: {runs-on: x, needs: one}}": true,
|
||||||
"on: push\njobs: {test: {strategy: {matrix: {os: []}}}}": true,
|
"on: push\njobs: {test: {runs-on: x, strategy: {matrix: {os: []}}}}": true,
|
||||||
"on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {}}": true,
|
"on: push\nrun-name: ${{ secrets.TOKEN }}\njobs: {test: {runs-on: x}}": true,
|
||||||
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {steps: [{run: echo}]}}": false,
|
"on: push\njobs: {test: {steps: [{run: echo}]}}": true,
|
||||||
|
"on: push\nrun-name: ${{ fromJSON(inputs.x) }}\njobs: {test: {runs-on: x, steps: [{run: echo}]}}": false,
|
||||||
} {
|
} {
|
||||||
_, gotStatic, err := readWorkflowEvents([]byte(content))
|
_, gotStatic, err := readWorkflowEvents([]byte(content))
|
||||||
require.Error(t, err, content)
|
require.Error(t, err, content)
|
||||||
assert.Equal(t, static, gotStatic, content)
|
assert.Equal(t, static, gotStatic, content)
|
||||||
}
|
}
|
||||||
for _, content := range []string{"on: push\njobs: {test: {steps: [{run: echo}]}}", "on: push\nrun-name: ${{ github.ref }}\njobs: {test: {}}"} {
|
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}}",
|
||||||
|
} {
|
||||||
_, _, err := readWorkflowEvents([]byte(content))
|
_, _, err := readWorkflowEvents([]byte(content))
|
||||||
assert.NoError(t, err, content)
|
assert.NoError(t, err, content)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-3
@@ -13,8 +13,6 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/util"
|
"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
|
var defaultCache StringCache
|
||||||
@@ -83,7 +81,7 @@ func GetCache() StringCache {
|
|||||||
|
|
||||||
// GetString returns the key value from cache with callback when no key exists in cache
|
// 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) {
|
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()
|
return getFunc()
|
||||||
}
|
}
|
||||||
cached, exist := defaultCache.Get(key)
|
cached, exist := defaultCache.Get(key)
|
||||||
|
|||||||
Vendored
+70
@@ -0,0 +1,70 @@
|
|||||||
|
// 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
+18
-133
@@ -4,159 +4,44 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/graceful"
|
"gitea.dev/modules/graceful"
|
||||||
"gitea.dev/modules/nosql"
|
"gitea.dev/modules/nosql"
|
||||||
|
|
||||||
"gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RedisCacher represents a redis cache adapter implementation.
|
type redisCache struct {
|
||||||
type RedisCacher struct {
|
client redis.UniversalClient
|
||||||
c redis.UniversalClient
|
|
||||||
prefix string
|
prefix string
|
||||||
hsetName string
|
|
||||||
occupyMode bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// toStr convert string/int/int64 interface to string. it's only used by the RedisCacher.Put internally
|
func newRedisCache(conn string) backend {
|
||||||
func toStr(v any) string {
|
uri := nosql.ToRedisURI(conn)
|
||||||
if v == nil {
|
return &redisCache{
|
||||||
return ""
|
client: nosql.GetManager().GetRedisClient(uri.String()),
|
||||||
}
|
prefix: uri.Query().Get("prefix"),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put puts value (string type) into cache with key and expire time.
|
func (c *redisCache) Get(key string) (string, bool) {
|
||||||
// If expired is 0, it lives forever.
|
value, err := c.client.Get(graceful.GetManager().HammerContext(), c.prefix+key).Result()
|
||||||
func (c *RedisCacher) Put(key string, val any, expire int64) error {
|
return value, err == nil
|
||||||
// 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 {
|
func (c *redisCache) Put(key, value string, ttl int64) error {
|
||||||
return nil
|
return c.client.Set(graceful.GetManager().HammerContext(), c.prefix+key, value, time.Duration(ttl)*time.Second).Err()
|
||||||
}
|
|
||||||
return c.c.HSet(graceful.GetManager().HammerContext(), c.hsetName, key, "0").Err()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets cached value by given key.
|
func (c *redisCache) Delete(key string) error {
|
||||||
func (c *RedisCacher) Get(key string) any {
|
return c.client.Del(graceful.GetManager().HammerContext(), c.prefix+key).Err()
|
||||||
val, err := c.c.Get(graceful.GetManager().HammerContext(), c.prefix+key).Result()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes cached value by given key.
|
func (c *redisCache) IsExist(key string) bool {
|
||||||
func (c *RedisCacher) Delete(key string) error {
|
return c.client.Exists(graceful.GetManager().HammerContext(), c.prefix+key).Val() == 1
|
||||||
key = c.prefix + key
|
|
||||||
if err := c.c.Del(graceful.GetManager().HammerContext(), key).Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.occupyMode {
|
func (c *redisCache) Ping() error {
|
||||||
return nil
|
return c.client.Ping(graceful.GetManager().HammerContext()).Err()
|
||||||
}
|
|
||||||
return c.c.HDel(graceful.GetManager().HammerContext(), c.hsetName, key).Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,9 +8,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.dev/modules/nosql"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createTestCache() {
|
func createTestCache() {
|
||||||
@@ -32,6 +35,98 @@ func TestNewContext(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Nil(t, con)
|
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) {
|
func TestTest(t *testing.T) {
|
||||||
|
|||||||
Vendored
+78
-160
@@ -4,205 +4,123 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/json"
|
"gitea.dev/modules/json"
|
||||||
|
|
||||||
mc "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
|
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TwoQueueCache represents a LRU 2Q cache adapter implementation
|
const twoQueueDefaultSize = 50000
|
||||||
type TwoQueueCache struct {
|
|
||||||
lock sync.Mutex
|
type twoQueueCache struct {
|
||||||
cache *lru.TwoQueueCache[string, any]
|
cache *lru.TwoQueueCache[string, memoryItem] // wrap the existing thread-safe 2Q (two-queue) cache directly
|
||||||
interval int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TwoQueueCacheConfig describes the configuration for TwoQueueCache
|
type twoQueueCacheConfig struct {
|
||||||
type TwoQueueCacheConfig struct {
|
Size int `json:"size"`
|
||||||
Size int `ini:"SIZE" json:"size"`
|
RecentRatio float64 `json:"recent_ratio"`
|
||||||
RecentRatio float64 `ini:"RECENT_RATIO" json:"recent_ratio"`
|
GhostRatio float64 `json:"ghost_ratio"`
|
||||||
GhostRatio float64 `ini:"GHOST_RATIO" json:"ghost_ratio"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryItem represents a memory cache item.
|
func newTwoQueueCache(conn string, gcInterval time.Duration) (backend, error) {
|
||||||
type MemoryItem struct {
|
lruCache, err := newTwoQueueLRU(conn)
|
||||||
Val any
|
if err != nil {
|
||||||
Created int64
|
return nil, err
|
||||||
Timeout int64
|
}
|
||||||
|
cache := &twoQueueCache{cache: lruCache}
|
||||||
|
startGC(gcInterval, cache.deleteExpired)
|
||||||
|
return cache, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (item *MemoryItem) hasExpired() bool {
|
func newTwoQueueLRU(conn string) (*lru.TwoQueueCache[string, memoryItem], error) {
|
||||||
return item.Timeout > 0 &&
|
if conn == "" {
|
||||||
(time.Now().Unix()-item.Created) >= item.Timeout
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ mc.Cache = &TwoQueueCache{}
|
func (c *twoQueueCache) Get(key string) (string, bool) {
|
||||||
|
item, ok := c.cache.Get(key)
|
||||||
// Put puts value into cache with key and expire time.
|
if !ok {
|
||||||
func (c *TwoQueueCache) Put(key string, val any, timeout int64) error {
|
return "", false
|
||||||
item := &MemoryItem{
|
|
||||||
Val: val,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
Timeout: timeout,
|
|
||||||
}
|
}
|
||||||
c.lock.Lock()
|
if item.expired(timeNow()) {
|
||||||
defer c.lock.Unlock()
|
c.cache.Remove(key)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return item.value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *twoQueueCache) Put(key, value string, ttl int64) error {
|
||||||
|
item := newMemoryItem(value, ttl)
|
||||||
c.cache.Add(key, item)
|
c.cache.Add(key, item)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets cached value by given key.
|
func (c *twoQueueCache) Delete(key string) error {
|
||||||
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)
|
c.cache.Remove(key)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return item.Val
|
func (c *twoQueueCache) IsExist(key string) bool {
|
||||||
|
item, ok := c.cache.Peek(key)
|
||||||
|
return ok && !item.expired(timeNow())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes cached value by given key.
|
func (c *twoQueueCache) Ping() error {
|
||||||
func (c *TwoQueueCache) Delete(key string) error {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
c.cache.Remove(key)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Incr increases cached int-type value by given key as a counter.
|
func (c *twoQueueCache) deleteExpired() {
|
||||||
func (c *TwoQueueCache) Incr(key string) error {
|
now := timeNow()
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) checkAndInvalidate(key string) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
cached, ok := c.cache.Peek(key)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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() {
|
for _, key := range c.cache.Keys() {
|
||||||
c.checkAndInvalidate(key)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
time.AfterFunc(time.Duration(c.interval)*time.Second, c.startGC)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartAndGC starts GC routine based on config string settings.
|
type memoryItem struct {
|
||||||
func (c *TwoQueueCache) StartAndGC(opts mc.Options) error {
|
value string
|
||||||
var err error
|
expiresAt time.Time
|
||||||
size := 50000
|
|
||||||
if opts.AdapterConfig != "" {
|
|
||||||
size, err = strconv.Atoi(opts.AdapterConfig)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if !json.Valid([]byte(opts.AdapterConfig)) {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &TwoQueueCacheConfig{
|
func newMemoryItem(value string, ttl int64) memoryItem {
|
||||||
Size: 50000,
|
item := memoryItem{value: value}
|
||||||
RecentRatio: lru.Default2QRecentRatio,
|
if ttl > 0 {
|
||||||
GhostRatio: lru.Default2QGhostEntries,
|
item.expiresAt = timeNow().Add(time.Duration(ttl) * time.Second)
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal([]byte(opts.AdapterConfig), cfg)
|
return item
|
||||||
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 (item memoryItem) expired(now time.Time) bool {
|
||||||
func (c *TwoQueueCache) Ping() error {
|
return !item.expiresAt.IsZero() && !now.Before(item.expiresAt)
|
||||||
return mc.GenericPing(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func startGC(interval time.Duration, deleteExpired func()) {
|
||||||
mc.Register("twoqueue", &TwoQueueCache{})
|
if interval <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for range time.Tick(interval) {
|
||||||
|
deleteExpired()
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+33
-36
@@ -5,13 +5,14 @@ package cache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/json"
|
"gitea.dev/modules/json"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
chi_cache "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetJSONError struct {
|
type GetJSONError struct {
|
||||||
@@ -30,57 +31,57 @@ type StringCache interface {
|
|||||||
Ping() error
|
Ping() error
|
||||||
|
|
||||||
Get(key string) (string, bool)
|
Get(key string) (string, bool)
|
||||||
Put(key, value string, ttl int64) error
|
Put(key, value string, ttl int64) error // ttl in seconds, 0 never expires, negative removes the key
|
||||||
Delete(key string) error
|
Delete(key string) error
|
||||||
IsExist(key string) bool
|
IsExist(key string) bool
|
||||||
|
|
||||||
PutJSON(key string, v any, ttl int64) error
|
PutJSON(key string, v any, ttl int64) error
|
||||||
GetJSON(key string, ptr any) (exist bool, err *GetJSONError)
|
GetJSON(key string, ptr any) (exist bool, err *GetJSONError)
|
||||||
|
}
|
||||||
|
|
||||||
ChiCache() chi_cache.Cache
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
type stringCache struct {
|
type stringCache struct {
|
||||||
chiCache chi_cache.Cache
|
backend
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStringCache(cacheConfig setting.Cache) (StringCache, error) {
|
func NewStringCache(cacheConfig setting.Cache) (StringCache, error) {
|
||||||
adapter := util.IfZero(cacheConfig.Adapter, "memory")
|
cacheBackend, err := newBackend(cacheConfig)
|
||||||
interval := util.IfZero(cacheConfig.Interval, 60)
|
|
||||||
cc, err := chi_cache.NewCacher(chi_cache.Options{
|
|
||||||
Adapter: adapter,
|
|
||||||
AdapterConfig: cacheConfig.Conn,
|
|
||||||
Interval: interval,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &stringCache{chiCache: cc}, nil
|
return &stringCache{backend: cacheBackend}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *stringCache) Ping() error {
|
func newBackend(cacheConfig setting.Cache) (backend, error) {
|
||||||
return sc.chiCache.Ping()
|
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) 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 {
|
func (sc *stringCache) Put(key, value string, ttl int64) error {
|
||||||
return sc.chiCache.Put(key, value, ttl)
|
if ttl < 0 {
|
||||||
|
return sc.backend.Delete(key)
|
||||||
}
|
}
|
||||||
|
return sc.backend.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>:"
|
const cachedErrorPrefix = "<CACHED-ERROR>:"
|
||||||
@@ -97,7 +98,7 @@ func (sc *stringCache) PutJSON(key string, v any, ttl int64) error {
|
|||||||
}
|
}
|
||||||
s = util.UnsafeBytesToString(b)
|
s = util.UnsafeBytesToString(b)
|
||||||
}
|
}
|
||||||
return sc.chiCache.Put(key, s, ttl)
|
return sc.Put(key, s, ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSONError) {
|
func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSONError) {
|
||||||
@@ -114,7 +115,3 @@ func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSON
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *stringCache) ChiCache() chi_cache.Cache {
|
|
||||||
return sc.chiCache
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/translation"
|
"gitea.dev/modules/translation"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type htmlChunkReader struct {
|
type htmlChunkReader struct {
|
||||||
@@ -30,6 +32,10 @@ type escapeStreamer struct {
|
|||||||
ambiguousTables []*AmbiguousTable
|
ambiguousTables []*AmbiguousTable
|
||||||
allowed map[rune]bool
|
allowed map[rune]bool
|
||||||
|
|
||||||
|
tagPartial []byte // partial tag content, used to detect if we are in some tags
|
||||||
|
|
||||||
|
inTagMath bool // MathML operators like U+2212 are intended and wrapping them breaks the math layout
|
||||||
|
|
||||||
out io.Writer
|
out io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +68,7 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
|
|||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
if partInTag[i] {
|
if partInTag[i] {
|
||||||
lastIsTag = true
|
lastIsTag = true
|
||||||
|
es.trackHtmlTag(part)
|
||||||
if _, err := out.Write(part); err != nil {
|
if _, err := out.Write(part); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -75,7 +82,11 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = es.detectAndWriteRunes(part); err != nil {
|
if es.inTagMath {
|
||||||
|
if _, err := out.Write(part); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if err = es.detectAndWriteRunes(part); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +94,34 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trackHtmlTag receives tag parts, a tag might be split into multiple parts
|
||||||
|
func (e *escapeStreamer) trackHtmlTag(part []byte) {
|
||||||
|
const maxHeadLen = 100 // only read the first N bytes of the tag for detection purpose
|
||||||
|
if part[0] == '<' {
|
||||||
|
// start a new tag
|
||||||
|
e.tagPartial = e.tagPartial[:0]
|
||||||
|
}
|
||||||
|
if len(e.tagPartial) >= maxHeadLen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.tagPartial = append(e.tagPartial, part[:min(len(part), maxHeadLen-len(e.tagPartial))]...)
|
||||||
|
|
||||||
|
isTag := func(prefix string) bool {
|
||||||
|
if len(e.tagPartial) < len(prefix)+1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !util.AsciiEqualFold(e.tagPartial[:len(prefix)], []byte(prefix)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.IndexByte(" \t\n\r\f>", e.tagPartial[len(prefix)]) != -1
|
||||||
|
}
|
||||||
|
if isTag("<math") {
|
||||||
|
e.inTagMath = true
|
||||||
|
} else if isTag("</math") {
|
||||||
|
e.inTagMath = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) {
|
func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) {
|
||||||
remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom)
|
remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom)
|
||||||
if ok {
|
if ok {
|
||||||
|
|||||||
@@ -141,6 +141,12 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
|
|||||||
result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`,
|
result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`,
|
||||||
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
|
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ambiguous in math",
|
||||||
|
text: "<math><mo>−</mo><mi>b</mi></math> −",
|
||||||
|
result: `<math><mo>−</mo><mi>b</mi></math> <span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:− [U+2212],- [U+002D]"><span class="char">−</span></span>`,
|
||||||
|
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEscapeControlReader(t *testing.T) {
|
func TestEscapeControlReader(t *testing.T) {
|
||||||
@@ -156,6 +162,24 @@ func TestEscapeControlReader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrackHtmlTag(t *testing.T) {
|
||||||
|
e := &escapeStreamer{}
|
||||||
|
for _, tt := range []struct {
|
||||||
|
parts []string
|
||||||
|
inMath bool
|
||||||
|
}{
|
||||||
|
{[]string{"<ma", `TH display="block">`}, true},
|
||||||
|
{[]string{"<mo>"}, true},
|
||||||
|
{[]string{"</MA", "th>"}, false},
|
||||||
|
{[]string{"<mathx>"}, false},
|
||||||
|
} {
|
||||||
|
for _, part := range tt.parts {
|
||||||
|
e.trackHtmlTag([]byte(part))
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.inMath, e.inTagMath, "%v", tt.parts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
|
func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
|
||||||
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
|
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
|
||||||
_, out := EscapeControlHTML("a test", &translation.MockLocale{})
|
_, out := EscapeControlHTML("a test", &translation.MockLocale{})
|
||||||
|
|||||||
@@ -0,0 +1,468 @@
|
|||||||
|
// 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"
|
||||||
|
"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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if yaml.Unmarshal([]byte(content), &node) != nil {
|
||||||
|
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 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 (
|
||||||
|
bibtexEscaper = strings.NewReplacer("&", `\&`, "%", `\%`, "$", `\$`, "#", `\#`, "_", `\_`, "{", `\{`, "}", `\}`)
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
keyUnsafeChars = regexp.MustCompile(`[^a-zA-Z0-9-]+`)
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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(), keyLetters.Replace(key))
|
||||||
|
return strings.Trim(keyUnsafeChars.ReplaceAllString(key, "_"), "_")
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package citation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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"},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BatchChecker provides a reader for check-attribute content that can be long running
|
// BatchChecker provides a reader for check-attribute content that can be long running
|
||||||
@@ -120,12 +121,17 @@ func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) {
|
|||||||
return fmt.Errorf("CheckPath timeout: %s", debugMsg)
|
return fmt.Errorf("CheckPath timeout: %s", debugMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeout := time.NewTimer(5 * time.Second)
|
||||||
|
defer timeout.Stop()
|
||||||
|
|
||||||
rs = NewAttributes()
|
rs = NewAttributes()
|
||||||
for i := 0; i < c.attributesNum; i++ {
|
for i := 0; i < c.attributesNum; i++ {
|
||||||
select {
|
select {
|
||||||
case <-time.After(5 * time.Second):
|
case <-timeout.C:
|
||||||
// there is no "hang" problem now. This code is just used to catch other potential problems.
|
// there is no "hang" problem now. This code is just used to catch other potential problems.
|
||||||
return nil, reportTimeout()
|
err = reportTimeout()
|
||||||
|
setting.PanicInDevOrTesting("Unexpected timeout, need to investigate: %v", err)
|
||||||
|
return nil, err
|
||||||
case attr, ok := <-c.stdOut.ReadAttribute():
|
case attr, ok := <-c.stdOut.ReadAttribute():
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, c.ctx.Err()
|
return nil, c.ctx.Err()
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ func TestReadingBlameOutputSha256(t *testing.T) {
|
|||||||
setting.AppDataPath = t.TempDir()
|
setting.AppDataPath = t.TempDir()
|
||||||
ctx := t.Context()
|
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) {
|
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
|
||||||
storage := mockRepository("repo5_pulls_sha256")
|
storage := mockRepository("repo5_pulls_sha256")
|
||||||
repo, err := OpenRepository(ctx, storage)
|
repo, err := OpenRepository(ctx, storage)
|
||||||
|
|||||||
+96
-2
@@ -12,11 +12,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/util"
|
"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)
|
// Name returns name of the tree entry this blob object was created from (or empty string)
|
||||||
func (b *Blob) Name() string {
|
func (b *Blob) Name() string {
|
||||||
return b.name
|
return b.name
|
||||||
@@ -114,3 +113,98 @@ loop:
|
|||||||
_ = encoder.Close()
|
_ = encoder.Close()
|
||||||
return base64buf.String(), nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
// 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()
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// 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,3 +113,36 @@ func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string
|
|||||||
|
|
||||||
return results, unHitEntryPaths, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
// 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,11 +4,14 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/git/gitrepo"
|
"gitea.dev/modules/git/gitrepo"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -164,3 +167,40 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
|
|||||||
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
|
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,8 +1,6 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
+2
-3
@@ -29,7 +29,6 @@ const RequiredVersion = "2.25.0" // the minimum Git version required
|
|||||||
type Features struct {
|
type Features struct {
|
||||||
gitVersion *version.Version
|
gitVersion *version.Version
|
||||||
|
|
||||||
UsingGogit bool
|
|
||||||
SupportProcReceive bool // >= 2.29
|
SupportProcReceive bool // >= 2.29
|
||||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||||
SupportedObjectFormats []ObjectFormat // sha1, sha256
|
SupportedObjectFormats []ObjectFormat // sha1, sha256
|
||||||
@@ -79,9 +78,9 @@ func loadGitVersionFeatures() (*Features, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
features := &Features{gitVersion: ver, UsingGogit: isGogit}
|
features := &Features{gitVersion: ver}
|
||||||
features.SupportProcReceive = features.CheckVersionAtLeast("2.29")
|
features.SupportProcReceive = features.CheckVersionAtLeast("2.29")
|
||||||
features.SupportHashSha256 = features.CheckVersionAtLeast("2.42") && !isGogit
|
features.SupportHashSha256 = features.CheckVersionAtLeast("2.42")
|
||||||
features.SupportedObjectFormats = []ObjectFormat{Sha1ObjectFormat}
|
features.SupportedObjectFormats = []ObjectFormat{Sha1ObjectFormat}
|
||||||
if features.SupportHashSha256 {
|
if features.SupportHashSha256 {
|
||||||
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
|
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ type Command struct {
|
|||||||
cmd *process.Cmd
|
cmd *process.Cmd
|
||||||
|
|
||||||
cmdCtx context.Context
|
cmdCtx context.Context
|
||||||
cmdCancel process.CancelCauseFunc
|
cmdCtxCancel process.CancelCauseFunc
|
||||||
cmdFinished process.FinishedFunc
|
cmdFinished func()
|
||||||
cmdStartTime time.Time
|
cmdStartTime time.Time
|
||||||
|
|
||||||
pipelineFunc func(Context) error
|
pipelineFunc func(Context) error
|
||||||
@@ -428,19 +428,24 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
|
|||||||
if c.callerInfo == "" {
|
if c.callerInfo == "" {
|
||||||
c.WithParentCallerInfo()
|
c.WithParentCallerInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
// these logs are for debugging purposes only, so no guarantee of correctness or stability
|
// these logs are for debugging purposes only, so no guarantee of correctness or stability
|
||||||
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.gitDir), cmdLogString)
|
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.gitDir), cmdLogString)
|
||||||
log.Debug("git.Command: %s", desc)
|
log.Debug("git.Command: %s", desc)
|
||||||
|
|
||||||
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
|
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
|
||||||
defer span.End()
|
|
||||||
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo)
|
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo)
|
||||||
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
|
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
|
||||||
|
|
||||||
|
var cmdCtxFinished func()
|
||||||
if c.cmdTimeout <= 0 {
|
if c.cmdTimeout <= 0 {
|
||||||
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContext(ctx, desc)
|
c.cmdCtx, c.cmdCtxCancel, cmdCtxFinished = process.GetManager().AddContext(ctx, desc)
|
||||||
} else {
|
} else {
|
||||||
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc)
|
c.cmdCtx, c.cmdCtxCancel, cmdCtxFinished = process.GetManager().AddContextTimeout(ctx, c.cmdTimeout, desc)
|
||||||
|
}
|
||||||
|
c.cmdFinished = func() {
|
||||||
|
cmdCtxFinished()
|
||||||
|
span.End()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.cmdStartTime = time.Now()
|
c.cmdStartTime = time.Now()
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ func (c *cmdContext) CancelPipeline(err error) error {
|
|||||||
// * context canceled by pipeline caller with/without error (normal cancellation)
|
// * context canceled by pipeline caller with/without error (normal cancellation)
|
||||||
// * context canceled by parent context (still context.Canceled error)
|
// * context canceled by parent context (still context.Canceled error)
|
||||||
// * other causes
|
// * other causes
|
||||||
c.cmd.cmdCancel(pipelineError{err})
|
c.cmd.cmdCtxCancel(pipelineError{err})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,20 @@ type LastCommitCache struct {
|
|||||||
|
|
||||||
// Put puts the last commit id with commit and entry path
|
// Put puts the last commit id with commit and entry path
|
||||||
func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
|
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)
|
log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
|
||||||
return c.cache.Put(getCacheKey(c.repo, ref, entryPath), commitID, c.ttlFn())
|
return c.cache.Put(getCacheKey(c.repo, ref, entryPath), commitID, c.ttlFn())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets the last commit information by commit id and entry path
|
// Get gets the last commit information by commit id and entry path
|
||||||
func (c *LastCommitCache) Get(ctx context.Context, ref, entryPath string) (*Commit, error) {
|
func (c *LastCommitCache) Get(ctx context.Context, ref, entryPath string) (*Commit, error) {
|
||||||
lastCommitID, ok := c.cache.Get(getCacheKey(c.repo, ref, entryPath))
|
var lastCommitID string
|
||||||
if !ok || lastCommitID == "" {
|
if c.ttlFn() >= 0 {
|
||||||
|
lastCommitID, _ = c.cache.Get(getCacheKey(c.repo, ref, entryPath))
|
||||||
|
}
|
||||||
|
if lastCommitID == "" {
|
||||||
return nil, nil //nolint:nilnil // return nil when cache miss
|
return nil, nil //nolint:nilnil // return nil when cache miss
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,3 +78,43 @@ func (c *LastCommitCache) GetCommitByPath(ctx context.Context, entryCommitID Obj
|
|||||||
|
|
||||||
return lastCommit, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -23,27 +21,18 @@ import (
|
|||||||
// logNameStatusRepo opens git log --raw in the provided repo and returns a parser
|
// 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 {
|
func logNameStatusRepo(ctx context.Context, repo RepositoryFacade, head, treepath string, paths ...string) *logNameStatusRepoParser {
|
||||||
cmd := gitcmd.NewCommand()
|
cmd := gitcmd.NewCommand()
|
||||||
cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head)
|
cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "--no-follow", "-t", "-z").AddDynamicArguments(head)
|
||||||
|
|
||||||
var files []string
|
var files []string
|
||||||
if len(paths) < 70 {
|
|
||||||
if treepath != "" {
|
if treepath != "" {
|
||||||
files = append(files, treepath)
|
files = append(files, treepath) // matches all entries, adding them would not narrow the log
|
||||||
for _, pth := range paths {
|
} else if len(paths) < 70 {
|
||||||
if pth != "" {
|
|
||||||
files = append(files, path.Join(treepath, pth))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, pth := range paths {
|
for _, pth := range paths {
|
||||||
if pth != "" {
|
if pth != "" {
|
||||||
files = append(files, 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"
|
// Use the :(literal) pathspec magic to handle edge cases with files named like ":file.txt" or "*.jpg"
|
||||||
for i, file := range files {
|
for i, file := range files {
|
||||||
files[i] = ":(literal)" + file
|
files[i] = ":(literal)" + file
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// 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,8 +43,6 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
|||||||
return entries, nil
|
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) {
|
func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd BufferedReader, sz int64) ([]*TreeEntry, error) {
|
||||||
entries := make([]*TreeEntry, 0, 10)
|
entries := make([]*TreeEntry, 0, 10)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func fillResultNameRev(ctx context.Context, repo git.RepositoryFacade, results []*LFSResult) error {
|
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{}
|
wg := errgroup.Group{}
|
||||||
cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithRepo(repo)
|
cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithRepo(repo)
|
||||||
stdin, stdinClose := cmd.MakeStdinPipe()
|
stdin, stdinClose := cmd.MakeStdinPipe()
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
const isGogit = false
|
|
||||||
|
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
RepositoryBase
|
RepositoryBase
|
||||||
}
|
}
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
const isGogit = true
|
|
||||||
|
|
||||||
type Repository struct {
|
|
||||||
RepositoryBase
|
|
||||||
|
|
||||||
gogitRepo *gogit.Repository
|
|
||||||
gogitStorage *reindexingStorage
|
|
||||||
}
|
|
||||||
|
|
||||||
// reindexingStorage picks up packs that git wrote after go-git loaded its index
|
|
||||||
// https://github.com/go-git/go-git/issues/2439
|
|
||||||
type reindexingStorage struct {
|
|
||||||
*filesystem.Storage
|
|
||||||
packs []plumbing.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *reindexingStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) {
|
|
||||||
obj, err := s.Storage.EncodedObject(t, h)
|
|
||||||
if !errors.Is(err, plumbing.ErrObjectNotFound) {
|
|
||||||
return obj, err
|
|
||||||
}
|
|
||||||
packs, _ := s.ObjectPacks()
|
|
||||||
if slices.Equal(packs, s.packs) {
|
|
||||||
return obj, err
|
|
||||||
}
|
|
||||||
s.packs = packs
|
|
||||||
s.Reindex()
|
|
||||||
return s.Storage.EncodedObject(t, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -5,9 +5,13 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
|
"gitea.dev/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BranchPrefix base dir of the branch information file store on git
|
// BranchPrefix base dir of the branch information file store on git
|
||||||
@@ -24,3 +28,170 @@ func (repo *Repository) AddRemote(ctx context.Context, name, url string, fetch b
|
|||||||
RunStdString(ctx)
|
RunStdString(ctx)
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -102,10 +102,6 @@ func TestRepository_IsObjectExist(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer repo.Close()
|
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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
arg string
|
arg string
|
||||||
@@ -129,7 +125,7 @@ func TestRepository_IsObjectExist(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "short commit hash",
|
name: "short commit hash",
|
||||||
arg: "ce06481",
|
arg: "ce06481",
|
||||||
want: supportShortHash,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "blob hash",
|
name: "blob hash",
|
||||||
@@ -139,7 +135,7 @@ func TestRepository_IsObjectExist(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "short blob hash",
|
name: "short blob hash",
|
||||||
arg: "153f451",
|
arg: "153f451",
|
||||||
want: supportShortHash,
|
want: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -155,10 +151,6 @@ func TestRepository_IsReferenceExist(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer repo.Close()
|
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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
arg string
|
arg string
|
||||||
@@ -187,12 +179,12 @@ func TestRepository_IsReferenceExist(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "blob hash",
|
name: "blob hash",
|
||||||
arg: "153f451b9ee7fa1da317ab17a127e9fd9d384310",
|
arg: "153f451b9ee7fa1da317ab17a127e9fd9d384310",
|
||||||
want: supportBlobHash,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "short blob hash",
|
name: "short blob hash",
|
||||||
arg: "153f451",
|
arg: "153f451",
|
||||||
want: supportBlobHash,
|
want: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package git
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -458,3 +459,124 @@ func (repo *Repository) GetCommitBranchStart(ctx context.Context, env []string,
|
|||||||
|
|
||||||
return "", nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// 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,7 +4,9 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
@@ -96,3 +98,64 @@ func (repo *Repository) UnstableGuessRefByShortName(ctx context.Context, shortNa
|
|||||||
}
|
}
|
||||||
return ""
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// 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,10 +61,28 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time.
|
|||||||
} else {
|
} else {
|
||||||
gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch)
|
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()
|
stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe()
|
||||||
defer stdoutReaderClose()
|
defer stdoutReaderClose()
|
||||||
err = gitCmd.
|
err := gitCmd.
|
||||||
WithRepo(repo).
|
WithRepo(repo).
|
||||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||||
scanner := bufio.NewScanner(stdoutReader)
|
scanner := bufio.NewScanner(stdoutReader)
|
||||||
@@ -116,8 +134,8 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return fmt.Errorf("GetCodeActivityStats scan: %w", err)
|
return fmt.Errorf("parseCodeActivityLog scan: %w", err)
|
||||||
}
|
}
|
||||||
a := make([]*CodeActivityAuthor, 0, len(authors))
|
a := make([]*CodeActivityAuthor, 0, len(authors))
|
||||||
for _, v := range authors {
|
for _, v := range authors {
|
||||||
@@ -134,8 +152,7 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time.
|
|||||||
}).
|
}).
|
||||||
RunWithStderr(ctx)
|
RunWithStderr(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetCodeActivityStats: %w", err)
|
return fmt.Errorf("parseCodeActivityLog: %w", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return stats, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,8 @@ func TestRepository_GetCodeActivityStats(t *testing.T) {
|
|||||||
assert.Equal(t, "tris.git@shoddynet.org", code.Authors[1].Email)
|
assert.Equal(t, "tris.git@shoddynet.org", code.Authors[1].Email)
|
||||||
assert.EqualValues(t, 3, code.Authors[1].Commits)
|
assert.EqualValues(t, 3, code.Authors[1].Commits)
|
||||||
assert.EqualValues(t, 5, code.Authors[0].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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/git/foreachref"
|
"gitea.dev/modules/git/foreachref"
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -232,3 +235,122 @@ func (repo *Repository) GetAnnotatedTag(ctx context.Context, sha string) (*Tag,
|
|||||||
}
|
}
|
||||||
return tag, nil
|
return tag, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsTagExist returns true if given tag exists in the repository.
|
||||||
|
func (repo *Repository) IsTagExist(ctx context.Context, name string) bool {
|
||||||
|
if repo == nil || name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.IsReferenceExist(ctx, TagPrefix+name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
|
||||||
|
func (repo *Repository) GetTagType(ctx context.Context, id ObjectID) (string, error) {
|
||||||
|
batch, cancel, err := repo.CatFileBatch()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
info, err := batch.QueryInfo(id.String())
|
||||||
|
if err != nil {
|
||||||
|
if IsErrNotExist(err) {
|
||||||
|
return "", ErrNotExist{ID: id.String()}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return info.Type, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) {
|
||||||
|
t, ok := repo.tagCache.Get(tagID.String())
|
||||||
|
if ok {
|
||||||
|
log.Debug("Hit cache: %s", tagID)
|
||||||
|
tagClone := *t
|
||||||
|
tagClone.Name = name // This is necessary because lightweight tags may have same id
|
||||||
|
return &tagClone, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tp, err := repo.GetTagType(ctx, tagID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
|
||||||
|
commitIDStr, err := repo.GetTagCommitID(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
// every tag should have a commit ID so return all errors
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commitID, err := NewIDFromString(commitIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If type is "commit, the tag is a lightweight tag
|
||||||
|
if ObjectType(tp) == ObjectCommit {
|
||||||
|
commit, err := repo.GetCommit(ctx, commitIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag := &Tag{
|
||||||
|
Name: name,
|
||||||
|
ID: tagID,
|
||||||
|
Object: commitID,
|
||||||
|
Type: tp,
|
||||||
|
Tagger: commit.Committer,
|
||||||
|
CommitMessage: commit.CommitMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.tagCache.Set(tagID.String(), tag)
|
||||||
|
return tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tag is an annotated tag with a message.
|
||||||
|
batch, cancel, err := repo.CatFileBatch()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
info, rd, err := batch.QueryContent(tagID.String())
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
|
||||||
|
return nil, ErrNotExist{ID: tagID.String()}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
typ, size := info.Type, info.Size
|
||||||
|
if typ != "tag" {
|
||||||
|
if err := DiscardFull(rd, size+1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, ErrNotExist{ID: tagID.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// then we need to parse the tag and load the commit
|
||||||
|
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(tagID.Type(), data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.Name = name
|
||||||
|
tag.ID = tagID
|
||||||
|
tag.Type = tp
|
||||||
|
|
||||||
|
repo.tagCache.Set(tagID.String(), tag)
|
||||||
|
return tag, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
// 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"
|
|
||||||
|
|
||||||
"gitea.dev/modules/log"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsTagExist returns true if given tag exists in the repository.
|
|
||||||
func (repo *Repository) IsTagExist(_ context.Context, name string) bool {
|
|
||||||
_, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
|
|
||||||
func (repo *Repository) GetTagType(_ context.Context, id ObjectID) (string, error) {
|
|
||||||
// Get tag type
|
|
||||||
obj, err := repo.gogitRepo.Object(plumbing.AnyObject, plumbing.Hash(id.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
if err == plumbing.ErrReferenceNotFound {
|
|
||||||
return "", &ErrNotExist{ID: id.String()}
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj.Type().String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) {
|
|
||||||
t, ok := repo.tagCache.Get(tagID.String())
|
|
||||||
if ok {
|
|
||||||
log.Debug("Hit cache: %s", tagID)
|
|
||||||
tagClone := *t
|
|
||||||
tagClone.Name = name // This is necessary because lightweight tags may have same id
|
|
||||||
return &tagClone, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tp, err := repo.GetTagType(ctx, tagID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
|
|
||||||
commitIDStr, err := repo.GetTagCommitID(ctx, name)
|
|
||||||
if err != nil {
|
|
||||||
// every tag should have a commit ID so return all errors
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
commitID, err := NewIDFromString(commitIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If type is "commit, the tag is a lightweight tag
|
|
||||||
if ObjectType(tp) == ObjectCommit {
|
|
||||||
commit, err := repo.GetCommit(ctx, commitIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tag := &Tag{
|
|
||||||
Name: name,
|
|
||||||
ID: tagID,
|
|
||||||
Object: commitID,
|
|
||||||
Type: tp,
|
|
||||||
Tagger: commit.Committer,
|
|
||||||
CommitMessage: CommitMessage{MessageRaw: commit.CommitMessage.MessageRaw},
|
|
||||||
}
|
|
||||||
|
|
||||||
repo.tagCache.Set(tagID.String(), tag)
|
|
||||||
return tag, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gogitTag, err := repo.gogitRepo.TagObject(plumbing.Hash(tagID.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
if err == plumbing.ErrReferenceNotFound {
|
|
||||||
return nil, &ErrNotExist{ID: tagID.String()}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tag := &Tag{
|
|
||||||
Name: name,
|
|
||||||
ID: tagID,
|
|
||||||
Object: commitID.Type().MustID(gogitTag.Target[:]),
|
|
||||||
Type: tp,
|
|
||||||
Tagger: &gogitTag.Tagger,
|
|
||||||
CommitMessage: CommitMessage{MessageRaw: gogitTag.Message},
|
|
||||||
}
|
|
||||||
|
|
||||||
repo.tagCache.Set(tagID.String(), tag)
|
|
||||||
return tag, nil
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"gitea.dev/modules/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsTagExist returns true if given tag exists in the repository.
|
|
||||||
func (repo *Repository) IsTagExist(ctx context.Context, name string) bool {
|
|
||||||
if repo == nil || name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo.IsReferenceExist(ctx, TagPrefix+name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
|
|
||||||
func (repo *Repository) GetTagType(ctx context.Context, id ObjectID) (string, error) {
|
|
||||||
batch, cancel, err := repo.CatFileBatch()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer cancel()
|
|
||||||
info, err := batch.QueryInfo(id.String())
|
|
||||||
if err != nil {
|
|
||||||
if IsErrNotExist(err) {
|
|
||||||
return "", ErrNotExist{ID: id.String()}
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return info.Type, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) {
|
|
||||||
t, ok := repo.tagCache.Get(tagID.String())
|
|
||||||
if ok {
|
|
||||||
log.Debug("Hit cache: %s", tagID)
|
|
||||||
tagClone := *t
|
|
||||||
tagClone.Name = name // This is necessary because lightweight tags may have same id
|
|
||||||
return &tagClone, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tp, err := repo.GetTagType(ctx, tagID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
|
|
||||||
commitIDStr, err := repo.GetTagCommitID(ctx, name)
|
|
||||||
if err != nil {
|
|
||||||
// every tag should have a commit ID so return all errors
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
commitID, err := NewIDFromString(commitIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If type is "commit, the tag is a lightweight tag
|
|
||||||
if ObjectType(tp) == ObjectCommit {
|
|
||||||
commit, err := repo.GetCommit(ctx, commitIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tag := &Tag{
|
|
||||||
Name: name,
|
|
||||||
ID: tagID,
|
|
||||||
Object: commitID,
|
|
||||||
Type: tp,
|
|
||||||
Tagger: commit.Committer,
|
|
||||||
CommitMessage: commit.CommitMessage,
|
|
||||||
}
|
|
||||||
|
|
||||||
repo.tagCache.Set(tagID.String(), tag)
|
|
||||||
return tag, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// The tag is an annotated tag with a message.
|
|
||||||
batch, cancel, err := repo.CatFileBatch()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
info, rd, err := batch.QueryContent(tagID.String())
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) || IsErrNotExist(err) {
|
|
||||||
return nil, ErrNotExist{ID: tagID.String()}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
typ, size := info.Type, info.Size
|
|
||||||
if typ != "tag" {
|
|
||||||
if err := DiscardFull(rd, size+1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, ErrNotExist{ID: tagID.String()}
|
|
||||||
}
|
|
||||||
|
|
||||||
// then we need to parse the tag and load the commit
|
|
||||||
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(tagID.Type(), data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tag.Name = name
|
|
||||||
tag.ID = tagID
|
|
||||||
tag.Type = tp
|
|
||||||
|
|
||||||
repo.tagCache.Set(tagID.String(), tag)
|
|
||||||
return tag, nil
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ package git
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -68,3 +69,87 @@ func (repo *Repository) CommitTree(ctx context.Context, author, committer *Signa
|
|||||||
}
|
}
|
||||||
return NewIDFromString(strings.TrimSpace(stdout))
|
return NewIDFromString(strings.TrimSpace(stdout))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *Repository) getTree(ctx context.Context, id ObjectID) (*Tree, error) {
|
||||||
|
batch, cancel, err := repo.CatFileBatch()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
info, rd, err := batch.QueryContent(id.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch info.Type {
|
||||||
|
case "tag":
|
||||||
|
data, err := io.ReadAll(io.LimitReader(rd, info.Size))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag, err := parseTagData(id.Type(), data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commit, err := repo.getCommitWithBatch(batch, tag.Object)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tree := commit.Tree()
|
||||||
|
return tree, nil
|
||||||
|
case "commit":
|
||||||
|
commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := rd.Discard(1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tree := commit.Tree()
|
||||||
|
return tree, nil
|
||||||
|
case "tree":
|
||||||
|
tree := newTree(id)
|
||||||
|
objectFormat, err := repo.GetObjectFormat(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, info.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tree.entriesParsed = true
|
||||||
|
return tree, nil
|
||||||
|
default:
|
||||||
|
if err := DiscardFull(rd, info.Size+1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
ID: id.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTree find the tree object in the repository.
|
||||||
|
func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) {
|
||||||
|
objectFormat, err := repo.GetObjectFormat(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(idStr) != objectFormat.FullLength() {
|
||||||
|
res, err := repo.GetRefCommitID(ctx, idStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(res) > 0 {
|
||||||
|
idStr = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id, err := NewIDFromString(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.getTree(ctx, id)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"gitea.dev/modules/git/gitcmd"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *Repository) getTree(_ context.Context, id ObjectID) (*Tree, error) {
|
|
||||||
gogitTree, err := repo.gogitRepo.TreeObject(plumbing.Hash(id.RawValue()))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, plumbing.ErrObjectNotFound) {
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
ID: id.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tree := newTree(id)
|
|
||||||
tree.resolvedGogitTreeObject = gogitTree
|
|
||||||
return tree, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTree find the tree object in the repository.
|
|
||||||
func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) {
|
|
||||||
objectFormat, err := repo.GetObjectFormat(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(idStr) != objectFormat.FullLength() {
|
|
||||||
res, _, err := gitcmd.NewCommand("rev-parse", "--verify").
|
|
||||||
AddDynamicArguments(idStr).
|
|
||||||
WithRepo(repo).
|
|
||||||
RunStdString(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(res) > 0 {
|
|
||||||
idStr = res[:len(res)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
id, err := NewIDFromString(idStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id.RawValue()))
|
|
||||||
if err == nil {
|
|
||||||
id = ParseGogitHash(commitObject.TreeHash)
|
|
||||||
}
|
|
||||||
treeObject, err := repo.getTree(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return treeObject, nil
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *Repository) getTree(ctx context.Context, id ObjectID) (*Tree, error) {
|
|
||||||
batch, cancel, err := repo.CatFileBatch()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
info, rd, err := batch.QueryContent(id.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch info.Type {
|
|
||||||
case "tag":
|
|
||||||
data, err := io.ReadAll(io.LimitReader(rd, info.Size))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tag, err := parseTagData(id.Type(), data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err := repo.getCommitWithBatch(batch, tag.Object)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tree := commit.Tree()
|
|
||||||
return tree, nil
|
|
||||||
case "commit":
|
|
||||||
commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := rd.Discard(1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tree := commit.Tree()
|
|
||||||
return tree, nil
|
|
||||||
case "tree":
|
|
||||||
tree := newTree(id)
|
|
||||||
objectFormat, err := repo.GetObjectFormat(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, info.Size)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tree.entriesParsed = true
|
|
||||||
return tree, nil
|
|
||||||
default:
|
|
||||||
if err := DiscardFull(rd, info.Size+1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
ID: id.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTree find the tree object in the repository.
|
|
||||||
func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) {
|
|
||||||
objectFormat, err := repo.GetObjectFormat(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(idStr) != objectFormat.FullLength() {
|
|
||||||
res, err := repo.GetRefCommitID(ctx, idStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(res) > 0 {
|
|
||||||
idStr = res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
id, err := NewIDFromString(idStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return repo.getTree(ctx, id)
|
|
||||||
}
|
|
||||||
@@ -5,11 +5,14 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Helper to get a signature from the commit line, which looks like:
|
// Helper to get a signature from the commit line, which looks like:
|
||||||
@@ -47,3 +50,25 @@ func parseSignatureFromCommitLine(line string) *Signature {
|
|||||||
}
|
}
|
||||||
return sig
|
return sig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signature represents the Author, Committer or Tagger information.
|
||||||
|
type Signature struct {
|
||||||
|
Name string // the committer name, it can be anything
|
||||||
|
Email string // the committer email, it can be anything
|
||||||
|
When time.Time // the timestamp of the signature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signature) String() string {
|
||||||
|
return fmt.Sprintf("%s <%s>", s.Name, s.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode writes the signature for git commit object
|
||||||
|
func (s *Signature) Encode(w io.Writer) error {
|
||||||
|
_, err := fmt.Fprintf(w, "%s <%s> %d %s", s.Name, s.Email, max(0, s.When.Unix()), s.When.Format("-0700"))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode parses the signature for git commit object
|
||||||
|
func (s *Signature) Decode(b []byte) {
|
||||||
|
*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Signature represents the Author or Committer information.
|
|
||||||
type Signature = object.Signature
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.dev/modules/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Signature represents the Author, Committer or Tagger information.
|
|
||||||
type Signature struct {
|
|
||||||
Name string // the committer name, it can be anything
|
|
||||||
Email string // the committer email, it can be anything
|
|
||||||
When time.Time // the timestamp of the signature
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Signature) String() string {
|
|
||||||
return fmt.Sprintf("%s <%s>", s.Name, s.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode writes the signature for git commit object (same as gogit's object.Signature Encode method)
|
|
||||||
func (s *Signature) Encode(w io.Writer) error {
|
|
||||||
_, err := fmt.Fprintf(w, "%s <%s> %d %s", s.Name, s.Email, max(0, s.When.Unix()), s.When.Format("-0700"))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode parses the signature for git commit object (same as gogit's object.Signature Decode method)
|
|
||||||
func (s *Signature) Decode(b []byte) {
|
|
||||||
*s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b))
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ package git
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
@@ -76,3 +77,97 @@ func (repo *Repository) GetTreePathLatestCommit(ctx context.Context, refName, tr
|
|||||||
}
|
}
|
||||||
return repo.GetCommit(ctx, strings.TrimSpace(stdout))
|
return repo.GetCommit(ctx, strings.TrimSpace(stdout))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tree represents a flat directory listing.
|
||||||
|
type Tree struct {
|
||||||
|
TreeCommon
|
||||||
|
|
||||||
|
entries Entries
|
||||||
|
entriesParsed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEntries returns all entries of current tree.
|
||||||
|
func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, error) {
|
||||||
|
if t.entriesParsed {
|
||||||
|
return t.entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
batch, cancel, err := gitRepo.CatFileBatch()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
info, rd, err := batch.QueryContent(t.ID.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Type == "commit" {
|
||||||
|
treeID, err := ReadTreeID(rd, info.Size)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, rd, err = batch.QueryContent(treeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.Type == "tree" {
|
||||||
|
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t.entriesParsed = true
|
||||||
|
return t.entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a tree just use ls-tree instead
|
||||||
|
if err := DiscardFull(rd, info.Size+1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithRepo(gitRepo).RunStdBytes(ctx)
|
||||||
|
if runErr != nil {
|
||||||
|
if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) {
|
||||||
|
return nil, ErrNotExist{
|
||||||
|
ID: t.ID.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
t.entries, err = parseTreeEntries(stdout, t)
|
||||||
|
if err == nil {
|
||||||
|
t.entriesParsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.entries, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
|
||||||
|
// extraArgs could be "-l" to get the size, which is slower
|
||||||
|
func (t *Tree) listEntriesRecursive(ctx context.Context, gitRepo *Repository, extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
|
||||||
|
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
|
||||||
|
AddArguments(extraArgs...).
|
||||||
|
AddDynamicArguments(t.ID.String()).
|
||||||
|
WithRepo(gitRepo).
|
||||||
|
RunStdBytes(ctx)
|
||||||
|
if runErr != nil {
|
||||||
|
return nil, runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: the "name" field is abused, here it is a full path
|
||||||
|
// FIXME: this ptree is not right, fortunately it isn't really used
|
||||||
|
return parseTreeEntries(stdout, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size
|
||||||
|
func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) {
|
||||||
|
return t.listEntriesRecursive(ctx, gitRepo, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size
|
||||||
|
func (t *Tree) ListEntriesRecursiveWithSize(ctx context.Context, gitRepo *Repository) (Entries, error) {
|
||||||
|
return t.listEntriesRecursive(ctx, gitRepo, gitcmd.TrustedCmdArgs{"--long"})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,11 @@
|
|||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// GetBlobByPath get the blob object according the path
|
// GetBlobByPath get the blob object according the path
|
||||||
func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) {
|
func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) {
|
||||||
@@ -19,3 +23,38 @@ func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath s
|
|||||||
|
|
||||||
return nil, ErrNotExist{"", relpath}
|
return nil, ErrNotExist{"", relpath}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTreeEntryByPath get the tree entries according the sub dir
|
||||||
|
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
|
||||||
|
if len(relpath) == 0 {
|
||||||
|
return &TreeEntry{
|
||||||
|
ptree: t,
|
||||||
|
ID: t.ID,
|
||||||
|
name: "",
|
||||||
|
entryMode: EntryModeTree,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relpath = path.Clean(relpath)
|
||||||
|
parts := strings.Split(relpath, "/")
|
||||||
|
|
||||||
|
tree := t
|
||||||
|
for _, name := range parts[:len(parts)-1] {
|
||||||
|
tree, err = tree.SubTree(ctx, gitRepo, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name := parts[len(parts)-1]
|
||||||
|
entries, err := tree.ListEntries(ctx, gitRepo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, v := range entries {
|
||||||
|
if v.Name() == name {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ErrNotExist{"", relpath}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
|
||||||
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (*TreeEntry, error) {
|
|
||||||
if len(relpath) == 0 {
|
|
||||||
return &TreeEntry{
|
|
||||||
ID: t.ID,
|
|
||||||
ptree: t,
|
|
||||||
name: "",
|
|
||||||
entryMode: EntryModeTree,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
relpath = path.Clean(relpath)
|
|
||||||
parts := strings.Split(relpath, "/")
|
|
||||||
var err error
|
|
||||||
tree := t
|
|
||||||
for i, name := range parts {
|
|
||||||
if i == len(parts)-1 {
|
|
||||||
entries, err := tree.ListEntries(ctx, gitRepo)
|
|
||||||
if err != nil {
|
|
||||||
if err == plumbing.ErrObjectNotFound {
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
RelPath: relpath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, v := range entries {
|
|
||||||
if v.Name() == name {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tree, err = tree.SubTree(ctx, gitRepo, name)
|
|
||||||
if err != nil {
|
|
||||||
if err == plumbing.ErrObjectNotFound {
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
RelPath: relpath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, ErrNotExist{"", relpath}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
//go:build !gogit
|
|
||||||
|
|
||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetTreeEntryByPath get the tree entries according the sub dir
|
|
||||||
func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
|
|
||||||
if len(relpath) == 0 {
|
|
||||||
return &TreeEntry{
|
|
||||||
ptree: t,
|
|
||||||
ID: t.ID,
|
|
||||||
name: "",
|
|
||||||
entryMode: EntryModeTree,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
relpath = path.Clean(relpath)
|
|
||||||
parts := strings.Split(relpath, "/")
|
|
||||||
|
|
||||||
tree := t
|
|
||||||
for _, name := range parts[:len(parts)-1] {
|
|
||||||
tree, err = tree.SubTree(ctx, gitRepo, name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
name := parts[len(parts)-1]
|
|
||||||
entries, err := tree.ListEntries(ctx, gitRepo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, v := range entries {
|
|
||||||
if v.Name() == name {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, ErrNotExist{"", relpath}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user