mirror of
https://github.com/go-gitea/gitea.git
synced 2026-03-26 09:28:56 +01:00
Merge branch 'main' into feature/workflow-graph
Signed-off-by: Semenets V. Pavel <p.semenets@gmail.com>
This commit is contained in:
commit
39d9a64a8a
@ -13,7 +13,7 @@
|
||||
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {},
|
||||
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
|
||||
"ghcr.io/devcontainers/features/python:1": {
|
||||
"version": "3.13"
|
||||
"version": "3.14"
|
||||
},
|
||||
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
|
||||
},
|
||||
|
||||
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@ -46,7 +46,7 @@ modifies/internal:
|
||||
- ".gitpod.yml"
|
||||
- ".markdownlint.yaml"
|
||||
- ".spectral.yaml"
|
||||
- "stylelint.config.ts"
|
||||
- "stylelint.config.*"
|
||||
- ".yamllint.yaml"
|
||||
- ".github/**"
|
||||
- ".gitea/**"
|
||||
@ -89,4 +89,4 @@ topic/code-linting:
|
||||
- ".markdownlint.yaml"
|
||||
- ".spectral.yaml"
|
||||
- ".yamllint.yaml"
|
||||
- "stylelint.config.ts"
|
||||
- "stylelint.config.*"
|
||||
|
||||
2
.github/workflows/cron-licenses.yml
vendored
2
.github/workflows/cron-licenses.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
- run: make generate-gitignore
|
||||
timeout-minutes: 40
|
||||
- name: push translations to repo
|
||||
uses: appleboy/git-push-action@v1.0.0
|
||||
uses: appleboy/git-push-action@v1.2.0
|
||||
with:
|
||||
author_email: "teabot@gitea.io"
|
||||
author_name: GiteaBot
|
||||
|
||||
2
.github/workflows/cron-translations.yml
vendored
2
.github/workflows/cron-translations.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
- name: update locales
|
||||
run: ./build/update-locales.sh
|
||||
- name: push translations to repo
|
||||
uses: appleboy/git-push-action@v1.0.0
|
||||
uses: appleboy/git-push-action@v1.2.0
|
||||
with:
|
||||
author_email: "teabot@gitea.io"
|
||||
author_name: GiteaBot
|
||||
|
||||
4
.github/workflows/pull-compliance.yml
vendored
4
.github/workflows/pull-compliance.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- run: uv python install 3.12
|
||||
- run: uv python install 3.14
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- run: uv python install 3.12
|
||||
- run: uv python install 3.14
|
||||
- run: make deps-py
|
||||
- run: make lint-yaml
|
||||
|
||||
|
||||
@ -48,6 +48,10 @@ linters:
|
||||
desc: do not use the ini package, use gitea's config system instead
|
||||
- pkg: gitea.com/go-chi/cache
|
||||
desc: do not use the go-chi cache package, use gitea's cache system
|
||||
- pkg: github.com/pkg/errors
|
||||
desc: use builtin errors package instead
|
||||
- pkg: github.com/go-ap/errors
|
||||
desc: use builtin errors package instead
|
||||
nolintlint:
|
||||
allow-unused: false
|
||||
require-explanation: true
|
||||
|
||||
52
CHANGELOG.md
52
CHANGELOG.md
@ -4,7 +4,39 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.25.3](https://github.com/go-gitea/gitea/releases/tag/1.25.3) - 2025-12-17
|
||||
## [1.25.4](https://github.com/go-gitea/gitea/releases/tag/v1.25.4) - 2026-01-15
|
||||
|
||||
* SECURITY
|
||||
* Release attachments must belong to the intended repo (#36347) (#36375)
|
||||
* Fix permission check on org project operations (#36318) (#36373)
|
||||
* Clean watches when make a repository private and check permission when send release emails (#36319) (#36370)
|
||||
* Add more check for stopwatch read or list (#36340) (#36368)
|
||||
* Fix openid setting check (#36346) (#36361)
|
||||
* Fix cancel auto merge bug (#36341) (#36356)
|
||||
* Fix delete attachment check (#36320) (#36355)
|
||||
* LFS locks must belong to the intended repo (#36344) (#36349)
|
||||
* Fix bug on notification read (#36339) #36387
|
||||
* ENHANCEMENTS
|
||||
* Add more routes to the "expensive" list (#36290)
|
||||
* Make "commit statuses" API accept slashes in "ref" (#36264) (#36275)
|
||||
* BUGFIXES
|
||||
* Fix git http service handling (#36396)
|
||||
* Fix markdown newline handling during IME composition (#36421) (#36424)
|
||||
* Fix missing repository id when migrating release attachments (#36389)
|
||||
* Fix bug when compare in the pull request (#36363) (#36372)
|
||||
* Fix incorrect text content detection (#36364) (#36369)
|
||||
* Fill missing `has_code` in repository api (#36338) (#36359)
|
||||
* Fix notifications pagination query parameters (#36351) (#36358)
|
||||
* Fix some trivial problems (#36336) (#36337)
|
||||
* Prevent panic when GitLab release has more links than sources (#36295) (#36305)
|
||||
* Fix stats bug when syncing release (#36285) (#36294)
|
||||
* Always honor user's choice for "delete branch after merge" (#36281) (#36286)
|
||||
* Use the requested host for LFS links (#36242) (#36258)
|
||||
* Fix panic when get editor config file (#36241) (#36247)
|
||||
* Fix regression in writing authorized principals (#36213) (#36218)
|
||||
* Fix WebAuthn error checking (#36219) (#36235)
|
||||
|
||||
## [1.25.3](https://github.com/go-gitea/gitea/releases/tag/v1.25.3) - 2025-12-17
|
||||
|
||||
* SECURITY
|
||||
* Bump toolchain to go1.25.5, misc fixes (#36082)
|
||||
@ -31,7 +63,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* Fix error handling in mailer and wiki services (#36041) (#36053)
|
||||
* Fix bugs when comparing and creating pull request (#36166) (#36144)
|
||||
|
||||
## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/1.25.2) - 2025-11-23
|
||||
## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/v1.25.2) - 2025-11-23
|
||||
|
||||
* SECURITY
|
||||
* Upgrade golang.org/x/crypto to 0.45.0 (#35985) (#35988)
|
||||
@ -418,7 +450,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* Hide href attribute of a tag if there is no target_url (#34556) (#34684)
|
||||
* Fix tag target (#34781) #34783
|
||||
|
||||
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26
|
||||
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/v1.24.0) - 2025-05-26
|
||||
|
||||
* BREAKING
|
||||
* Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
|
||||
@ -788,7 +820,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* Bump x/net (#32896) (#32900)
|
||||
* Only activity tab needs heatmap data loading (#34652)
|
||||
|
||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
|
||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/v1.23.8) - 2025-05-11
|
||||
|
||||
* SECURITY
|
||||
* Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
|
||||
@ -815,7 +847,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* Bump go version in go.mod (#34160)
|
||||
* remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
|
||||
|
||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
|
||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/v1.23.7) - 2025-04-07
|
||||
|
||||
* Enhancements
|
||||
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
|
||||
@ -913,7 +945,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
* BUGFIXES
|
||||
* Fix a bug caused by status webhook template #33512
|
||||
|
||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
|
||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/v1.23.2) - 2025-02-04
|
||||
|
||||
* BREAKING
|
||||
* Add tests for webhook and fix some webhook bugs (#33396) (#33442)
|
||||
@ -3443,7 +3475,7 @@ Key highlights of this release encompass significant changes categorized under `
|
||||
* Improve decryption failure message (#24573) (#24575)
|
||||
* Makefile: Use portable !, not GNUish -not, with find(1). (#24565) (#24572)
|
||||
|
||||
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/1.19.3) - 2023-05-03
|
||||
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/v1.19.3) - 2023-05-03
|
||||
|
||||
* SECURITY
|
||||
* Use golang 1.20.4 to fix CVE-2023-24539, CVE-2023-24540, and CVE-2023-29400
|
||||
@ -3456,7 +3488,7 @@ Key highlights of this release encompass significant changes categorized under `
|
||||
* Fix incorrect CurrentUser check for docker rootless (#24435)
|
||||
* Getting the tag list does not require being signed in (#24413) (#24416)
|
||||
|
||||
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/1.19.2) - 2023-04-26
|
||||
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/v1.19.2) - 2023-04-26
|
||||
|
||||
* SECURITY
|
||||
* Require repo scope for PATs for private repos and basic authentication (#24362) (#24364)
|
||||
@ -3955,7 +3987,7 @@ Key highlights of this release encompass significant changes categorized under `
|
||||
* Display attachments of review comment when comment content is blank (#23035) (#23046)
|
||||
* Return empty url for submodule tree entries (#23043) (#23048)
|
||||
|
||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20
|
||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/v1.18.4) - 2023-02-20
|
||||
|
||||
* SECURITY
|
||||
* Provide the ability to set password hash algorithm parameters (#22942) (#22943)
|
||||
@ -4382,7 +4414,7 @@ Key highlights of this release encompass significant changes categorized under `
|
||||
* Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867)
|
||||
* Fix UI mis-align for PR commit history (#20845) (#20859)
|
||||
|
||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17
|
||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/v1.17.1) - 2022-08-17
|
||||
|
||||
* SECURITY
|
||||
* Correctly escape within tribute.js (#20831) (#20832)
|
||||
|
||||
@ -80,7 +80,7 @@ The more detailed and specific you are, the faster we can fix the issue. \
|
||||
It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://demo.gitea.com>, as perhaps your problem has already been fixed on a current version. \
|
||||
Please follow the guidelines described in [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) for your report.
|
||||
|
||||
Please be kind, remember that Gitea comes at no cost to you, and you're getting free help.
|
||||
Please be kind—remember that Gitea comes at no cost to you, and you're getting free help.
|
||||
|
||||
### Types of issues
|
||||
|
||||
|
||||
46
Makefile
46
Makefile
@ -32,14 +32,14 @@ XGO_VERSION := go-1.25.x
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.9
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10
|
||||
|
||||
DOCKER_IMAGE ?= gitea/gitea
|
||||
DOCKER_TAG ?= latest
|
||||
@ -100,7 +100,7 @@ GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Enable typescript support in Node.js before 22.18
|
||||
# TODO: Remove this once we can raise the minimum Node.js version to 22.18 (alpine >= 3.23)
|
||||
NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v 2>/dev/null | cut -c2- | tr '.' ' '))
|
||||
NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v 2>/dev/null | cut -c2- | sed 's/-.*//' | tr '.' ' '))
|
||||
ifeq ($(shell test "$(NODE_VERSION)" -lt "022018000"; echo $$?),0)
|
||||
NODE_VARS := NODE_OPTIONS="--experimental-strip-types"
|
||||
else
|
||||
@ -211,16 +211,6 @@ help: Makefile ## print Makefile help information.
|
||||
@printf " \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test"
|
||||
@printf " \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite"
|
||||
|
||||
.PHONY: go-check
|
||||
go-check:
|
||||
$(eval MIN_GO_VERSION_STR := $(shell grep -Eo '^go\s+[0-9]+\.[0-9]+' go.mod | cut -d' ' -f2))
|
||||
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
|
||||
$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
|
||||
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
|
||||
echo "Gitea requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: git-check
|
||||
git-check:
|
||||
@if git lfs >/dev/null 2>&1 ; then : ; else \
|
||||
@ -228,20 +218,6 @@ git-check:
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: node-check
|
||||
node-check:
|
||||
$(eval MIN_NODE_VERSION_STR := $(shell grep -Eo '"node":.*[0-9.]+"' package.json | sed -n 's/.*[^0-9.]\([0-9.]*\)"/\1/p'))
|
||||
$(eval MIN_NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell echo '$(MIN_NODE_VERSION_STR)' | tr '.' ' ')))
|
||||
$(eval PNPM_MISSING := $(shell hash pnpm > /dev/null 2>&1 || echo 1))
|
||||
@if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" ]; then \
|
||||
echo "Gitea requires Node.js $(MIN_NODE_VERSION_STR) or greater to build. You can get it at https://nodejs.org/en/download/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ "$(PNPM_MISSING)" = "1" ]; then \
|
||||
echo "Gitea requires pnpm to build. You can install it at https://pnpm.io/installation"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean ## delete backend, frontend and integration files
|
||||
rm -rf $(WEBPACK_DEST_ENTRIES) node_modules
|
||||
@ -341,11 +317,13 @@ lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backen
|
||||
lint-js: node_modules ## lint js files
|
||||
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES)
|
||||
$(NODE_VARS) pnpm exec vue-tsc
|
||||
$(NODE_VARS) pnpm exec knip --no-progress --cache
|
||||
|
||||
.PHONY: lint-js-fix
|
||||
lint-js-fix: node_modules ## lint js files and fix issues
|
||||
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES) --fix
|
||||
$(NODE_VARS) pnpm exec vue-tsc
|
||||
$(NODE_VARS) pnpm exec knip --no-progress --cache --fix
|
||||
|
||||
.PHONY: lint-css
|
||||
lint-css: node_modules ## lint css files
|
||||
@ -426,12 +404,12 @@ watch: ## watch everything and continuously rebuild
|
||||
@bash tools/watch.sh
|
||||
|
||||
.PHONY: watch-frontend
|
||||
watch-frontend: node-check node_modules ## watch frontend files and continuously rebuild
|
||||
watch-frontend: node_modules ## watch frontend files and continuously rebuild
|
||||
@rm -rf $(WEBPACK_DEST_ENTRIES)
|
||||
NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret
|
||||
|
||||
.PHONY: watch-backend
|
||||
watch-backend: go-check ## watch backend files and continuously rebuild
|
||||
watch-backend: ## watch backend files and continuously rebuild
|
||||
GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
|
||||
|
||||
.PHONY: test
|
||||
@ -749,7 +727,7 @@ build: frontend backend ## build everything
|
||||
frontend: $(WEBPACK_DEST) ## build frontend files
|
||||
|
||||
.PHONY: backend
|
||||
backend: go-check generate-backend $(EXECUTABLE) ## build backend files
|
||||
backend: generate-backend $(EXECUTABLE) ## build backend files
|
||||
|
||||
# We generate the backend before the frontend in case we in future we want to generate things in the frontend from generated files in backend
|
||||
.PHONY: generate
|
||||
@ -860,7 +838,7 @@ node_modules: pnpm-lock.yaml
|
||||
update: update-js update-py ## update js and py dependencies
|
||||
|
||||
.PHONY: update-js
|
||||
update-js: node-check | node_modules ## update js dependencies
|
||||
update-js: node_modules ## update js dependencies
|
||||
$(NODE_VARS) pnpm exec updates -u -f package.json
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
$(NODE_VARS) pnpm install
|
||||
@ -869,7 +847,7 @@ update-js: node-check | node_modules ## update js dependencies
|
||||
@touch node_modules
|
||||
|
||||
.PHONY: update-py
|
||||
update-py: node-check | node_modules ## update py dependencies
|
||||
update-py: node_modules ## update py dependencies
|
||||
$(NODE_VARS) pnpm exec updates -u -f pyproject.toml
|
||||
rm -rf .venv uv.lock
|
||||
uv sync
|
||||
@ -879,14 +857,14 @@ update-py: node-check | node_modules ## update py dependencies
|
||||
webpack: $(WEBPACK_DEST) ## build webpack files
|
||||
|
||||
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml
|
||||
@$(MAKE) -s node-check node_modules
|
||||
@$(MAKE) -s node_modules
|
||||
@rm -rf $(WEBPACK_DEST_ENTRIES)
|
||||
@echo "Running webpack..."
|
||||
@BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret
|
||||
@touch $(WEBPACK_DEST)
|
||||
|
||||
.PHONY: svg
|
||||
svg: node-check | node_modules ## build svg files
|
||||
svg: node_modules ## build svg files
|
||||
rm -rf $(SVG_DEST_DIR)
|
||||
node tools/generate-svg.ts
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
`build` 目标分为两个子目标:
|
||||
|
||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。
|
||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
|
||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本以及 [pnpm](https://pnpm.io/installation)。
|
||||
|
||||
需要互联网连接来下载 go 和 npm 模块。从包含预构建前端文件的官方源代码压缩包构建时,不会触发 `frontend` 目标,因此可以在没有 Node.js 的情况下构建。
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
`build` 目標分為兩個子目標:
|
||||
|
||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。
|
||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
|
||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本以及 [pnpm](https://pnpm.io/installation)。
|
||||
|
||||
需要互聯網連接來下載 go 和 npm 模塊。從包含預構建前端文件的官方源代碼壓縮包構建時,不會觸發 `frontend` 目標,因此可以在沒有 Node.js 的情況下構建。
|
||||
|
||||
|
||||
10
assets/go-licenses.json
generated
10
assets/go-licenses.json
generated
@ -409,16 +409,6 @@
|
||||
"path": "github.com/dimiro1/reply/LICENSE",
|
||||
"licenseText": "MIT License\n\nCopyright (c) Discourse\nCopyright (c) Claudemiro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/djherbis/buffer",
|
||||
"path": "github.com/djherbis/buffer/LICENSE.txt",
|
||||
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Dustin H\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/djherbis/nio/v3",
|
||||
"path": "github.com/djherbis/nio/v3/LICENSE.txt",
|
||||
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Dustin H\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/dlclark/regexp2",
|
||||
"path": "github.com/dlclark/regexp2/LICENSE",
|
||||
|
||||
36
cmd/hook.go
36
cmd/hook.go
@ -163,6 +163,14 @@ func (n *nilWriter) WriteString(s string) (int, error) {
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) {
|
||||
fields := strings.Split(line, " ")
|
||||
if len(fields) != 3 {
|
||||
return "", "", "", false
|
||||
}
|
||||
return fields[0], fields[1], git.RefName(fields[2]), true
|
||||
}
|
||||
|
||||
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
|
||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||
return nil
|
||||
@ -228,14 +236,11 @@ Gitea or set your environment appropriately.`, "")
|
||||
continue
|
||||
}
|
||||
|
||||
fields := bytes.Fields(scanner.Bytes())
|
||||
if len(fields) != 3 {
|
||||
oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
oldCommitID := string(fields[0])
|
||||
newCommitID := string(fields[1])
|
||||
refFullName := git.RefName(fields[2])
|
||||
total++
|
||||
lastline++
|
||||
|
||||
@ -313,7 +318,7 @@ func runHookPostReceive(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
// First of all run update-server-info no matter what
|
||||
if _, _, err := gitcmd.NewCommand("update-server-info").RunStdString(ctx); err != nil {
|
||||
if err := gitcmd.NewCommand("update-server-info").RunWithStderr(ctx); err != nil {
|
||||
return fmt.Errorf("failed to call 'git update-server-info': %w", err)
|
||||
}
|
||||
|
||||
@ -378,16 +383,13 @@ Gitea or set your environment appropriately.`, "")
|
||||
continue
|
||||
}
|
||||
|
||||
fields := bytes.Fields(scanner.Bytes())
|
||||
if len(fields) != 3 {
|
||||
var ok bool
|
||||
oldCommitIDs[count], newCommitIDs[count], refFullNames[count], ok = parseGitHookCommitRefLine(scanner.Text())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, ".")
|
||||
oldCommitIDs[count] = string(fields[0])
|
||||
newCommitIDs[count] = string(fields[1])
|
||||
refFullNames[count] = git.RefName(fields[2])
|
||||
|
||||
commitID, _ := git.NewIDFromString(newCommitIDs[count])
|
||||
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
|
||||
masterPushed = true
|
||||
@ -594,8 +596,8 @@ Gitea or set your environment appropriately.`, "")
|
||||
hookOptions.RefFullNames = make([]git.RefName, 0, hookBatchSize)
|
||||
|
||||
for {
|
||||
// note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed
|
||||
rs, err = readPktLine(ctx, reader, pktLineTypeUnknow)
|
||||
// note: pktLineTypeUnknown means pktLineTypeFlush and pktLineTypeData all allowed
|
||||
rs, err = readPktLine(ctx, reader, pktLineTypeUnknown)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -614,7 +616,7 @@ Gitea or set your environment appropriately.`, "")
|
||||
|
||||
if hasPushOptions {
|
||||
for {
|
||||
rs, err = readPktLine(ctx, reader, pktLineTypeUnknow)
|
||||
rs, err = readPktLine(ctx, reader, pktLineTypeUnknown)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -711,8 +713,8 @@ Gitea or set your environment appropriately.`, "")
|
||||
type pktLineType int64
|
||||
|
||||
const (
|
||||
// UnKnow type
|
||||
pktLineTypeUnknow pktLineType = 0
|
||||
// Unknown type
|
||||
pktLineTypeUnknown pktLineType = 0
|
||||
// flush-pkt "0000"
|
||||
pktLineTypeFlush pktLineType = iota
|
||||
// data line
|
||||
|
||||
@ -39,3 +39,17 @@ func TestPktLine(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("0007a\nb"), w.Bytes())
|
||||
}
|
||||
|
||||
func TestParseGitHookCommitRefLine(t *testing.T) {
|
||||
oldCommitID, newCommitID, refName, ok := parseGitHookCommitRefLine("a b c")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "a", oldCommitID)
|
||||
assert.Equal(t, "b", newCommitID)
|
||||
assert.Equal(t, "c", string(refName))
|
||||
|
||||
_, _, _, ok = parseGitHookCommitRefLine("a\tb\tc")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, _, _, ok = parseGitHookCommitRefLine("a b")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
26
cmd/web.go
26
cmd/web.go
@ -8,14 +8,13 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
@ -23,6 +22,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/routers/install"
|
||||
@ -234,22 +234,22 @@ func serveInstalled(c *cli.Command) error {
|
||||
}
|
||||
|
||||
func servePprof() {
|
||||
// FIXME: it shouldn't use the global DefaultServeMux, and it should use a proper context
|
||||
http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
mux.Handle("/debug/fgprof", fgprof.Handler())
|
||||
// FIXME: it should use a proper context
|
||||
_, _, finished := process.GetManager().AddTypedContext(context.TODO(), "Web: PProf Server", process.SystemProcessType, true)
|
||||
// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment, it's not worth introducing a configurable option for it.
|
||||
log.Info("Starting pprof server on localhost:6060")
|
||||
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
|
||||
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", mux))
|
||||
finished()
|
||||
}
|
||||
|
||||
func runWeb(ctx context.Context, cmd *cli.Command) error {
|
||||
defer func() {
|
||||
if panicked := recover(); panicked != nil {
|
||||
log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2))
|
||||
}
|
||||
}()
|
||||
|
||||
if subCmdName, valid := isValidDefaultSubCommand(cmd); !valid {
|
||||
return fmt.Errorf("unknown command: %s", subCmdName)
|
||||
}
|
||||
@ -269,6 +269,10 @@ func runWeb(ctx context.Context, cmd *cli.Command) error {
|
||||
createPIDFile(cmd.String("pid"))
|
||||
}
|
||||
|
||||
// init the HTML renderer and load templates, if error happens, it will report the error immediately and exit with error log
|
||||
// in dev mode, it won't exit, but watch the template files for changes
|
||||
_ = templates.PageRenderer()
|
||||
|
||||
if !setting.InstallLock {
|
||||
if err := serveInstall(cmd); err != nil {
|
||||
return err
|
||||
|
||||
@ -737,11 +737,8 @@ LEVEL = Info
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Git Operation timeout in seconds
|
||||
;[git.timeout]
|
||||
;DEFAULT = 360
|
||||
;MIGRATE = 600
|
||||
;MIRROR = 300
|
||||
;CLONE = 300
|
||||
;PULL = 300
|
||||
;GC = 60
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@ -2488,8 +2485,9 @@ LEVEL = Info
|
||||
;[highlight.mapping]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Extension mapping to highlight class
|
||||
;; e.g. .toml=ini
|
||||
;; Extension mapping to highlight class, for example:
|
||||
;; .toml = ini
|
||||
;; .my-js = JavaScript
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@ -18,7 +18,18 @@ import {defineConfig, globalIgnores} from 'eslint/config';
|
||||
|
||||
const jsExts = ['js', 'mjs', 'cjs'] as const;
|
||||
const tsExts = ['ts', 'mts', 'cts'] as const;
|
||||
const restrictedSyntax = ['WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'];
|
||||
|
||||
const restrictedGlobals = [
|
||||
{name: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'},
|
||||
{name: 'fetch', message: 'Use `modules/fetch.ts` instead.'},
|
||||
];
|
||||
|
||||
const restrictedProperties = [
|
||||
{object: 'window', property: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'},
|
||||
{object: 'globalThis', property: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'},
|
||||
{object: 'window', property: 'fetch', message: 'Use `modules/fetch.ts` instead.'},
|
||||
{object: 'globalThis', property: 'fetch', message: 'Use `modules/fetch.ts` instead.'},
|
||||
];
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores([
|
||||
@ -32,10 +43,6 @@ export default defineConfig([
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
@ -69,7 +76,7 @@ export default defineConfig([
|
||||
'import-x/resolver': {'typescript': true},
|
||||
},
|
||||
rules: {
|
||||
'@eslint-community/eslint-comments/disable-enable-pair': [2],
|
||||
'@eslint-community/eslint-comments/disable-enable-pair': [0],
|
||||
'@eslint-community/eslint-comments/no-aggregating-enable': [2],
|
||||
'@eslint-community/eslint-comments/no-duplicate-disable': [2],
|
||||
'@eslint-community/eslint-comments/no-restricted-disable': [0],
|
||||
@ -233,7 +240,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}],
|
||||
'@typescript-eslint/no-use-before-define': [2, {functions: false, classes: true, variables: true, allowNamedExports: true, typedefs: false, enums: false, ignoreTypeReferences: true}],
|
||||
'@typescript-eslint/no-useless-constructor': [0],
|
||||
'@typescript-eslint/no-useless-default-assignment': [0], // https://github.com/typescript-eslint/typescript-eslint/issues/11847
|
||||
'@typescript-eslint/no-useless-default-assignment': [2],
|
||||
'@typescript-eslint/no-useless-empty-export': [0],
|
||||
'@typescript-eslint/no-wrapper-object-types': [2],
|
||||
'@typescript-eslint/non-nullable-type-assertion-style': [0],
|
||||
@ -264,6 +271,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/restrict-template-expressions': [0],
|
||||
'@typescript-eslint/return-await': [0],
|
||||
'@typescript-eslint/strict-boolean-expressions': [0],
|
||||
'@typescript-eslint/strict-void-return': [0],
|
||||
'@typescript-eslint/switch-exhaustiveness-check': [0],
|
||||
'@typescript-eslint/triple-slash-reference': [2],
|
||||
'@typescript-eslint/typedef': [0],
|
||||
@ -362,7 +370,7 @@ export default defineConfig([
|
||||
'import-x/no-self-import': [2],
|
||||
'import-x/no-unassigned-import': [0],
|
||||
'import-x/no-unresolved': [2, {commonjs: true, ignore: ['\\?.+$']}],
|
||||
// 'import-x/no-unused-modules': [2, {unusedExports: true}], // not compatible with eslint 9
|
||||
'import-x/no-unused-modules': [0], // incompatible with eslint 9
|
||||
'import-x/no-useless-path-segments': [2, {commonjs: true}],
|
||||
'import-x/no-webpack-loader-syntax': [2],
|
||||
'import-x/order': [0],
|
||||
@ -558,9 +566,10 @@ export default defineConfig([
|
||||
'no-redeclare': [0], // must be disabled for typescript overloads
|
||||
'no-regex-spaces': [2],
|
||||
'no-restricted-exports': [0],
|
||||
'no-restricted-globals': [2, 'addEventListener', 'blur', 'close', 'closed', 'confirm', 'defaultStatus', 'defaultstatus', 'error', 'event', 'external', 'find', 'focus', 'frameElement', 'frames', 'history', 'innerHeight', 'innerWidth', 'isFinite', 'isNaN', 'length', 'locationbar', 'menubar', 'moveBy', 'moveTo', 'name', 'onblur', 'onerror', 'onfocus', 'onload', 'onresize', 'onunload', 'open', 'opener', 'opera', 'outerHeight', 'outerWidth', 'pageXOffset', 'pageYOffset', 'parent', 'print', 'removeEventListener', 'resizeBy', 'resizeTo', 'screen', 'screenLeft', 'screenTop', 'screenX', 'screenY', 'scroll', 'scrollbars', 'scrollBy', 'scrollTo', 'scrollX', 'scrollY', 'status', 'statusbar', 'stop', 'toolbar', 'top'],
|
||||
'no-restricted-globals': [2, ...restrictedGlobals],
|
||||
'no-restricted-properties': [2, ...restrictedProperties],
|
||||
'no-restricted-imports': [0],
|
||||
'no-restricted-syntax': [2, ...restrictedSyntax, {selector: 'CallExpression[callee.name="fetch"]', message: 'use modules/fetch.ts instead'}],
|
||||
'no-restricted-syntax': [2, 'WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'],
|
||||
'no-return-assign': [0],
|
||||
'no-script-url': [2],
|
||||
'no-self-assign': [2, {props: true}],
|
||||
@ -926,12 +935,6 @@ export default defineConfig([
|
||||
'vue/require-typed-ref': [2],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['web_src/js/modules/fetch.ts', 'web_src/js/standalone/**/*'],
|
||||
rules: {
|
||||
'no-restricted-syntax': [2, ...restrictedSyntax],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.ts', 'web_src/js/test/setup.ts'],
|
||||
plugins: {vitest},
|
||||
@ -989,38 +992,19 @@ export default defineConfig([
|
||||
'vitest/valid-title': [2],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['web_src/js/types.ts'],
|
||||
rules: {
|
||||
'import-x/no-unused-modules': [0],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.d.ts'],
|
||||
rules: {
|
||||
'import-x/no-unused-modules': [0],
|
||||
'@typescript-eslint/consistent-type-definitions': [0],
|
||||
'@typescript-eslint/consistent-type-imports': [0],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.config.*'],
|
||||
rules: {
|
||||
'import-x/no-unused-modules': [0],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['web_src/**/*', 'docs/**/*'],
|
||||
languageOptions: {globals: globals.browser},
|
||||
files: ['*', 'tools/**/*'],
|
||||
languageOptions: {globals: globals.nodeBuiltin},
|
||||
},
|
||||
{
|
||||
files: ['web_src/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
__webpack_public_path__: true,
|
||||
process: false, // https://github.com/webpack/webpack/issues/15833
|
||||
},
|
||||
},
|
||||
languageOptions: {globals: {...globals.browser, ...globals.webpack}},
|
||||
},
|
||||
]);
|
||||
|
||||
10
go.mod
10
go.mod
@ -2,7 +2,7 @@ module code.gitea.io/gitea
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.25.5
|
||||
toolchain go1.25.6
|
||||
|
||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||
// But some CAs use negative serial number, just relax the check. related:
|
||||
@ -28,7 +28,7 @@ require (
|
||||
github.com/ProtonMail/go-crypto v1.3.0
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
|
||||
github.com/alecthomas/chroma/v2 v2.21.1
|
||||
github.com/alecthomas/chroma/v2 v2.23.0
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10
|
||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
@ -39,8 +39,6 @@ require (
|
||||
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21
|
||||
github.com/chi-middleware/proxy v1.1.1
|
||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
||||
github.com/djherbis/buffer v1.2.0
|
||||
github.com/djherbis/nio/v3 v3.0.1
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3
|
||||
@ -95,7 +93,6 @@ require (
|
||||
github.com/olivere/elastic/v7 v7.0.32
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/quasoft/websspi v1.1.2
|
||||
@ -113,7 +110,7 @@ require (
|
||||
github.com/wneessen/go-mail v0.7.2
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/yohcop/openid-go v1.0.1
|
||||
github.com/yuin/goldmark v1.7.13
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
gitlab.com/gitlab-org/api/client-go v0.142.4
|
||||
@ -251,6 +248,7 @@ require (
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pjbgf/sha1cd v0.4.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
|
||||
13
go.sum
13
go.sum
@ -98,8 +98,8 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA=
|
||||
github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/chroma/v2 v2.23.0 h1:u/Orux1J0eLuZDeQ44froV8smumheieI0EofhbyKhhk=
|
||||
github.com/alecthomas/chroma/v2 v2.23.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
@ -260,11 +260,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA=
|
||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21/go.mod h1:xJvkyD6Y2rZapGvPJLYo9dyx1s5dxBEDPa8T3YTuOk0=
|
||||
github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o=
|
||||
github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ=
|
||||
github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE=
|
||||
github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4=
|
||||
github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
@ -792,8 +787,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
|
||||
|
||||
18
knip.config.ts
Normal file
18
knip.config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type {KnipConfig} from 'knip';
|
||||
|
||||
export default {
|
||||
entry: [
|
||||
'*.ts',
|
||||
'tools/**/*.ts',
|
||||
'tests/e2e/**/*.ts',
|
||||
],
|
||||
ignoreDependencies: [
|
||||
// dependencies used in Makefile or tools
|
||||
'@primer/octicons',
|
||||
'markdownlint-cli',
|
||||
'nolyfill',
|
||||
'spectral-cli-bundle',
|
||||
'vue-tsc',
|
||||
'webpack-cli',
|
||||
],
|
||||
} satisfies KnipConfig;
|
||||
@ -114,7 +114,7 @@ func TestFindRenamedBranch(t *testing.T) {
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, "master", branch.To)
|
||||
|
||||
_, exist, err = git_model.FindRenamedBranch(t.Context(), 1, "unknow")
|
||||
_, exist, err = git_model.FindRenamedBranch(t.Context(), 1, "unknown")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exist)
|
||||
}
|
||||
|
||||
@ -101,10 +101,10 @@ func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// GetLFSLockByID returns release by given id.
|
||||
func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) {
|
||||
// GetLFSLockByIDAndRepo returns lfs lock by given id and repository id.
|
||||
func GetLFSLockByIDAndRepo(ctx context.Context, id, repoID int64) (*LFSLock, error) {
|
||||
lock := new(LFSLock)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(lock)
|
||||
has, err := db.GetEngine(ctx).ID(id).And("repo_id = ?", repoID).Get(lock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@ -153,7 +153,7 @@ func CountLFSLockByRepoID(ctx context.Context, repoID int64) (int64, error) {
|
||||
// DeleteLFSLockByID deletes a lock by given ID.
|
||||
func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
|
||||
lock, err := GetLFSLockByID(ctx, id)
|
||||
lock, err := GetLFSLockByIDAndRepo(ctx, id, repo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
82
models/git/lfs_lock_test.go
Normal file
82
models/git/lfs_lock_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createTestLock(t *testing.T, repo *repo_model.Repository, owner *user_model.User) *LFSLock {
|
||||
t.Helper()
|
||||
|
||||
path := fmt.Sprintf("%s-%d-%d", t.Name(), repo.ID, time.Now().UnixNano())
|
||||
lock, err := CreateLFSLock(t.Context(), repo, &LFSLock{
|
||||
OwnerID: owner.ID,
|
||||
Path: path,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return lock
|
||||
}
|
||||
|
||||
func TestGetLFSLockByIDAndRepo(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
lockRepo1 := createTestLock(t, repo1, user2)
|
||||
lockRepo3 := createTestLock(t, repo3, user4)
|
||||
|
||||
fetched, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo1.ID, fetched.ID)
|
||||
assert.Equal(t, repo1.ID, fetched.RepoID)
|
||||
|
||||
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo3.ID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrLFSLockNotExist(err))
|
||||
|
||||
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo1.ID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrLFSLockNotExist(err))
|
||||
}
|
||||
|
||||
func TestDeleteLFSLockByIDRequiresRepoMatch(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
lockRepo1 := createTestLock(t, repo1, user2)
|
||||
lockRepo3 := createTestLock(t, repo3, user4)
|
||||
|
||||
_, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo1, user2, true)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrLFSLockNotExist(err))
|
||||
|
||||
existing, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo3.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo3.ID, existing.ID)
|
||||
|
||||
deleted, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo3, user4, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo3.ID, deleted.ID)
|
||||
|
||||
deleted, err = DeleteLFSLockByID(t.Context(), lockRepo1.ID, repo1, user2, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, lockRepo1.ID, deleted.ID)
|
||||
}
|
||||
@ -20,6 +20,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
@ -233,11 +234,17 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
|
||||
return lang.TrString("repo.issues.role." + string(r) + "_helper")
|
||||
}
|
||||
|
||||
type SpecialDoerNameType string
|
||||
|
||||
const SpecialDoerNameCodeOwners SpecialDoerNameType = "CODEOWNERS"
|
||||
|
||||
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
|
||||
type CommentMetaData struct {
|
||||
ProjectColumnID int64 `json:"project_column_id,omitempty"`
|
||||
ProjectColumnTitle string `json:"project_column_title,omitempty"`
|
||||
ProjectTitle string `json:"project_title,omitempty"`
|
||||
|
||||
SpecialDoerName SpecialDoerNameType `json:"special_doer_name,omitempty"` // e.g. "CODEOWNERS" for CODEOWNERS-triggered review requests
|
||||
}
|
||||
|
||||
// Comment represents a comment in commit and issue page.
|
||||
@ -764,6 +771,37 @@ func (c *Comment) CodeCommentLink(ctx context.Context) string {
|
||||
return fmt.Sprintf("%s/files#%s", c.Issue.Link(), c.HashTag())
|
||||
}
|
||||
|
||||
func (c *Comment) MetaSpecialDoerTr(locale translation.Locale) template.HTML {
|
||||
if c.CommentMetaData == nil {
|
||||
return ""
|
||||
}
|
||||
if c.CommentMetaData.SpecialDoerName == SpecialDoerNameCodeOwners {
|
||||
return locale.Tr("repo.issues.review.codeowners_rules")
|
||||
}
|
||||
return htmlutil.HTMLFormat("%s", c.CommentMetaData.SpecialDoerName)
|
||||
}
|
||||
|
||||
func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdStr template.HTML) template.HTML {
|
||||
if c.AssigneeID > 0 {
|
||||
// it guarantees LoadAssigneeUserAndTeam has been called, and c.Assignee is Ghost user but not nil if the user doesn't exist
|
||||
if c.RemovedAssignee {
|
||||
if c.PosterID == c.AssigneeID {
|
||||
return locale.Tr("repo.issues.review.remove_review_request_self", createdStr)
|
||||
}
|
||||
return locale.Tr("repo.issues.review.remove_review_request", c.Assignee.GetDisplayName(), createdStr)
|
||||
}
|
||||
return locale.Tr("repo.issues.review.add_review_request", c.Assignee.GetDisplayName(), createdStr)
|
||||
}
|
||||
teamName := "Ghost Team"
|
||||
if c.AssigneeTeam != nil {
|
||||
teamName = c.AssigneeTeam.Name
|
||||
}
|
||||
if c.RemovedAssignee {
|
||||
return locale.Tr("repo.issues.review.remove_review_request", teamName, createdStr)
|
||||
}
|
||||
return locale.Tr("repo.issues.review.add_review_request", teamName, createdStr)
|
||||
}
|
||||
|
||||
// CreateComment creates comment with context
|
||||
func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
|
||||
@ -780,6 +818,11 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
|
||||
ProjectTitle: opts.ProjectTitle,
|
||||
}
|
||||
}
|
||||
if opts.SpecialDoerName != "" {
|
||||
commentMetaData = &CommentMetaData{
|
||||
SpecialDoerName: opts.SpecialDoerName,
|
||||
}
|
||||
}
|
||||
|
||||
comment := &Comment{
|
||||
Type: opts.Type,
|
||||
@ -976,6 +1019,7 @@ type CreateCommentOptions struct {
|
||||
RefIsPull bool
|
||||
IsForcePush bool
|
||||
Invalidated bool
|
||||
SpecialDoerName SpecialDoerNameType // e.g. "CODEOWNERS" for CODEOWNERS-triggered review requests
|
||||
}
|
||||
|
||||
// GetCommentByID returns the comment by given ID.
|
||||
|
||||
@ -666,9 +666,10 @@ func HasWorkInProgressPrefix(title string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch.
|
||||
// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch.
|
||||
// Sometimes a conflict may not list any files
|
||||
func (pr *PullRequest) IsFilesConflicted() bool {
|
||||
return len(pr.ConflictedFiles) > 0
|
||||
return pr.Status == PullRequestStatusConflict
|
||||
}
|
||||
|
||||
// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
|
||||
|
||||
@ -130,7 +130,7 @@ func TestLoadRequestedReviewers(t *testing.T) {
|
||||
user1, err := user_model.GetUserByID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
comment, err := issues_model.AddReviewRequest(t.Context(), issue, user1, &user_model.User{})
|
||||
comment, err := issues_model.AddReviewRequest(t.Context(), issue, user1, &user_model.User{}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, comment)
|
||||
|
||||
|
||||
@ -643,7 +643,7 @@ func InsertReviews(ctx context.Context, reviews []*Review) error {
|
||||
}
|
||||
|
||||
// AddReviewRequest add a review request from one reviewer
|
||||
func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User) (*Comment, error) {
|
||||
func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User, isCodeOwners bool) (*Comment, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
@ -702,6 +702,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
|
||||
RemovedAssignee: false, // Use RemovedAssignee as !isRequest
|
||||
AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
|
||||
ReviewID: review.ID,
|
||||
SpecialDoerName: util.Iif(isCodeOwners, SpecialDoerNameCodeOwners, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -767,7 +768,7 @@ func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64)
|
||||
}
|
||||
|
||||
// AddTeamReviewRequest add a review request from one team
|
||||
func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) {
|
||||
func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User, isCodeOwners bool) (*Comment, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
|
||||
review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID)
|
||||
if err != nil && !IsErrReviewNotExist(err) {
|
||||
@ -812,6 +813,7 @@ func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organizat
|
||||
RemovedAssignee: false, // Use RemovedAssignee as !isRequest
|
||||
AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
|
||||
ReviewID: review.ID,
|
||||
SpecialDoerName: util.Iif(isCodeOwners, SpecialDoerNameCodeOwners, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateComment(): %w", err)
|
||||
|
||||
@ -321,14 +321,28 @@ func TestAddReviewRequest(t *testing.T) {
|
||||
pull.HasMerged = false
|
||||
assert.NoError(t, pull.UpdateCols(t.Context(), "has_merged"))
|
||||
issue.IsClosed = true
|
||||
_, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{})
|
||||
_, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}, false)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
|
||||
|
||||
pull.HasMerged = true
|
||||
assert.NoError(t, pull.UpdateCols(t.Context(), "has_merged"))
|
||||
issue.IsClosed = false
|
||||
_, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{})
|
||||
_, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}, false)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
|
||||
|
||||
// Test CODEOWNERS review request stores metadata correctly
|
||||
pull2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
assert.NoError(t, pull2.LoadIssue(t.Context()))
|
||||
issue2 := pull2.Issue
|
||||
assert.NoError(t, issue2.LoadRepo(t.Context()))
|
||||
reviewer2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 7})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
comment, err := issues_model.AddReviewRequest(t.Context(), issue2, reviewer2, doer, true)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, comment)
|
||||
assert.NotNil(t, comment.CommentMetaData)
|
||||
assert.Equal(t, issues_model.SpecialDoerNameCodeOwners, comment.CommentMetaData.SpecialDoerName)
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Stopwatch represents a stopwatch for time tracking.
|
||||
@ -232,3 +234,14 @@ func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (
|
||||
})
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// RemoveStopwatchesByRepoID removes all stopwatches for a user in a specific repository
|
||||
// this function should be called before removing all the issues of the repository
|
||||
func RemoveStopwatchesByRepoID(ctx context.Context, userID, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Where("`stopwatch`.user_id = ?", userID).
|
||||
And(builder.In("`stopwatch`.issue_id",
|
||||
builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repoID}))).
|
||||
Delete(new(Stopwatch))
|
||||
return err
|
||||
}
|
||||
|
||||
@ -399,6 +399,7 @@ func prepareMigrationTasks() []*migration {
|
||||
|
||||
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
|
||||
newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness),
|
||||
newMigration(325, "Fix missed repo_id when migrate attachments", v1_26.FixMissedRepoIDWhenMigrateAttachments),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@ -5,14 +5,10 @@ package v1_21
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
@ -163,16 +159,13 @@ func migratePushMirrors(x *xorm.Engine) error {
|
||||
}
|
||||
|
||||
func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
|
||||
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
|
||||
if exist, _ := util.IsExist(repoPath); !exist {
|
||||
ctx := context.Background()
|
||||
relativePath := repo_model.RelativePath(ownerName, repoName)
|
||||
if exist, _ := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(relativePath)); !exist {
|
||||
return "", nil
|
||||
}
|
||||
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
|
||||
}
|
||||
|
||||
u, err := giturl.ParseGitURL(remoteURL)
|
||||
u, err := gitrepo.GitRemoteGetURL(ctx, repo_model.StorageRepo(relativePath), remoteName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
18
models/migrations/v1_26/v325.go
Normal file
18
models/migrations/v1_26/v325.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func FixMissedRepoIDWhenMigrateAttachments(x *xorm.Engine) error {
|
||||
_, err := x.Exec("UPDATE `attachment` SET `repo_id` = (SELECT `repo_id` FROM `issue` WHERE `issue`.`id` = `attachment`.`issue_id`) WHERE `issue_id` > 0 AND (`repo_id` IS NULL OR `repo_id` = 0);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.Exec("UPDATE `attachment` SET `repo_id` = (SELECT `repo_id` FROM `release` WHERE `release`.`id` = `attachment`.`release_id`) WHERE `release_id` > 0 AND (`repo_id` IS NULL OR `repo_id` = 0);")
|
||||
return err
|
||||
}
|
||||
45
models/migrations/v1_26/v325_test.go
Normal file
45
models/migrations/v1_26/v325_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_FixMissedRepoIDWhenMigrateAttachments(t *testing.T) {
|
||||
type Attachment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UUID string `xorm:"uuid UNIQUE"`
|
||||
RepoID int64 `xorm:"INDEX"` // this should not be zero
|
||||
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
|
||||
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
|
||||
UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added
|
||||
CommentID int64 `xorm:"INDEX"`
|
||||
Name string
|
||||
DownloadCount int64 `xorm:"DEFAULT 0"`
|
||||
Size int64 `xorm:"DEFAULT 0"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
type Release struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
// Prepare and load the testing database
|
||||
x, deferrable := base.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release))
|
||||
defer deferrable()
|
||||
|
||||
require.NoError(t, FixMissedRepoIDWhenMigrateAttachments(x))
|
||||
}
|
||||
@ -6,11 +6,10 @@ package v1_9
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
@ -34,16 +33,6 @@ func FixReleaseSha1OnReleaseTable(ctx context.Context, x *xorm.Engine) error {
|
||||
Name string
|
||||
}
|
||||
|
||||
// UserPath returns the path absolute path of user repositories.
|
||||
UserPath := func(userName string) string {
|
||||
return filepath.Join(setting.RepoRootPath, strings.ToLower(userName))
|
||||
}
|
||||
|
||||
// RepoPath returns repository path by given user and repository name.
|
||||
RepoPath := func(userName, repoName string) string {
|
||||
return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git")
|
||||
}
|
||||
|
||||
// Update release sha1
|
||||
const batchSize = 100
|
||||
sess := x.NewSession()
|
||||
@ -99,7 +88,7 @@ func FixReleaseSha1OnReleaseTable(ctx context.Context, x *xorm.Engine) error {
|
||||
userCache[repo.OwnerID] = user
|
||||
}
|
||||
|
||||
gitRepo, err = git.OpenRepository(ctx, RepoPath(user.Name, repo.Name))
|
||||
gitRepo, err = gitrepo.OpenRepository(ctx, repo_model.StorageRepo(repo_model.RelativePath(user.Name, repo.Name)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -213,6 +213,18 @@ func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
|
||||
return column, nil
|
||||
}
|
||||
|
||||
func GetColumnByIDAndProjectID(ctx context.Context, columnID, projectID int64) (*Column, error) {
|
||||
column := new(Column)
|
||||
has, err := db.GetEngine(ctx).ID(columnID).And("project_id=?", projectID).Get(column)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectColumnNotExist{ColumnID: columnID}
|
||||
}
|
||||
|
||||
return column, nil
|
||||
}
|
||||
|
||||
// UpdateColumn updates a project column
|
||||
func UpdateColumn(ctx context.Context, column *Column) error {
|
||||
var fieldToUpdate []string
|
||||
|
||||
@ -302,6 +302,18 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func GetProjectByIDAndOwner(ctx context.Context, id, ownerID int64) (*Project, error) {
|
||||
p := new(Project)
|
||||
has, err := db.GetEngine(ctx).ID(id).And("owner_id = ?", ownerID).Get(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectNotExist{ID: id}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetProjectForRepoByID returns the projects in a repository
|
||||
func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) {
|
||||
p := new(Project)
|
||||
|
||||
@ -70,6 +70,6 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository,
|
||||
"repo": helper.opts.DeprecatedRepoName,
|
||||
})
|
||||
}
|
||||
rctx = rctx.WithHelper(helper)
|
||||
rctx = rctx.WithHelper(helper).WithEnableHeadingIDGeneration(true)
|
||||
return rctx
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository,
|
||||
"markupAllowShortIssuePattern": "true",
|
||||
})
|
||||
}
|
||||
rctx = rctx.WithHelper(helper)
|
||||
rctx = rctx.WithHelper(helper).WithEnableHeadingIDGeneration(true)
|
||||
helper.ctx = rctx
|
||||
return rctx
|
||||
}
|
||||
|
||||
@ -166,6 +166,11 @@ func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, file
|
||||
return attach, nil
|
||||
}
|
||||
|
||||
func GetUnlinkedAttachmentsByUserID(ctx context.Context, userID int64) ([]*Attachment, error) {
|
||||
attachments := make([]*Attachment, 0, 10)
|
||||
return attachments, db.GetEngine(ctx).Where("uploader_id = ? AND issue_id = 0 AND release_id = 0 AND comment_id = 0", userID).Find(&attachments)
|
||||
}
|
||||
|
||||
// DeleteAttachment deletes the given attachment and optionally the associated file.
|
||||
func DeleteAttachment(ctx context.Context, a *Attachment, remove bool) error {
|
||||
_, err := DeleteAttachments(ctx, []*Attachment{a}, remove)
|
||||
|
||||
@ -101,3 +101,19 @@ func TestGetAttachmentsByUUIDs(t *testing.T) {
|
||||
assert.Equal(t, int64(1), attachList[0].IssueID)
|
||||
assert.Equal(t, int64(5), attachList[1].IssueID)
|
||||
}
|
||||
|
||||
func TestGetUnlinkedAttachmentsByUserID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
attachments, err := repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 8)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, attachments, 1)
|
||||
assert.Equal(t, int64(10), attachments[0].ID)
|
||||
assert.Zero(t, attachments[0].IssueID)
|
||||
assert.Zero(t, attachments[0].ReleaseID)
|
||||
assert.Zero(t, attachments[0].CommentID)
|
||||
|
||||
attachments, err = repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, attachments)
|
||||
}
|
||||
|
||||
@ -93,15 +93,25 @@ func init() {
|
||||
db.RegisterModel(new(Release))
|
||||
}
|
||||
|
||||
// LoadAttributes load repo and publisher attributes for a release
|
||||
func (r *Release) LoadAttributes(ctx context.Context) error {
|
||||
var err error
|
||||
if r.Repo == nil {
|
||||
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// LegacyAttachmentMissingRepoIDCutoff marks the date when repo_id started to be written during uploads
|
||||
// (2026-01-16T00:00:00Z). Older rows might have repo_id=0 and should be tolerated once.
|
||||
const LegacyAttachmentMissingRepoIDCutoff timeutil.TimeStamp = 1768521600
|
||||
|
||||
func (r *Release) LoadRepo(ctx context.Context) (err error) {
|
||||
if r.Repo != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadAttributes load repo and publisher attributes for a release
|
||||
func (r *Release) LoadAttributes(ctx context.Context) (err error) {
|
||||
if err := r.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Publisher == nil {
|
||||
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
|
||||
if err != nil {
|
||||
@ -168,6 +178,11 @@ func UpdateReleaseNumCommits(ctx context.Context, rel *Release) error {
|
||||
|
||||
// AddReleaseAttachments adds a release attachments
|
||||
func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) {
|
||||
rel, err := GetReleaseByID(ctx, releaseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check attachments
|
||||
attachments, err := GetAttachmentsByUUIDs(ctx, attachmentUUIDs)
|
||||
if err != nil {
|
||||
@ -175,6 +190,17 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
|
||||
}
|
||||
|
||||
for i := range attachments {
|
||||
if attachments[i].RepoID == 0 && attachments[i].CreatedUnix < LegacyAttachmentMissingRepoIDCutoff {
|
||||
attachments[i].RepoID = rel.RepoID
|
||||
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("repo_id").Update(attachments[i]); err != nil {
|
||||
return fmt.Errorf("update attachment repo_id [%d]: %w", attachments[i].ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if attachments[i].RepoID != rel.RepoID {
|
||||
return util.NewPermissionDeniedErrorf("attachment belongs to different repository")
|
||||
}
|
||||
|
||||
if attachments[i].ReleaseID != 0 {
|
||||
return util.NewPermissionDeniedErrorf("release permission denied")
|
||||
}
|
||||
|
||||
@ -6,7 +6,9 @@ package repo
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -37,3 +39,54 @@ func Test_FindTagsByCommitIDs(t *testing.T) {
|
||||
assert.Equal(t, "delete-tag", rels[1].TagName)
|
||||
assert.Equal(t, "v1.0", rels[2].TagName)
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsRejectsDifferentRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
uuid := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12" // attachment 2 belongs to repo 2
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{uuid})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), uuid)
|
||||
assert.NoError(t, err)
|
||||
assert.Zero(t, attach.ReleaseID, "attachment should not be linked to release on failure")
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsAllowsLegacyMissingRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
legacyUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20" // attachment 10 has repo_id 0
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{legacyUUID})
|
||||
assert.NoError(t, err)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), legacyUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, attach.RepoID)
|
||||
assert.EqualValues(t, 1, attach.ReleaseID)
|
||||
}
|
||||
|
||||
func TestAddReleaseAttachmentsRejectsRecentZeroRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
recentUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd3800aa"
|
||||
attachment := &Attachment{
|
||||
UUID: recentUUID,
|
||||
RepoID: 0,
|
||||
IssueID: 0,
|
||||
ReleaseID: 0,
|
||||
CommentID: 0,
|
||||
Name: "recent-zero",
|
||||
CreatedUnix: LegacyAttachmentMissingRepoIDCutoff + 1,
|
||||
}
|
||||
assert.NoError(t, db.Insert(t.Context(), attachment))
|
||||
|
||||
err := AddReleaseAttachments(t.Context(), 1, []string{recentUUID})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||
|
||||
attach, err := GetAttachmentByUUID(t.Context(), recentUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.Zero(t, attach.ReleaseID)
|
||||
assert.Zero(t, attach.RepoID)
|
||||
}
|
||||
|
||||
@ -176,3 +176,13 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
|
||||
}
|
||||
return watchRepoMode(ctx, watch, WatchModeAuto)
|
||||
}
|
||||
|
||||
// ClearRepoWatches clears all watches for a repository and from the user that watched it.
|
||||
// Used when a repository is set to private.
|
||||
func ClearRepoWatches(ctx context.Context, repoID int64) error {
|
||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_watches = 0 WHERE id = ?", repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.DeleteBeans(ctx, Watch{RepoID: repoID})
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsWatching(t *testing.T) {
|
||||
@ -119,3 +120,21 @@ func TestWatchIfAuto(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, watchers, prevCount)
|
||||
}
|
||||
|
||||
func TestClearRepoWatches(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
const repoID int64 = 1
|
||||
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, watchers)
|
||||
|
||||
assert.NoError(t, repo_model.ClearRepoWatches(t.Context(), repoID))
|
||||
|
||||
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, watchers)
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
assert.Zero(t, repo.NumWatches)
|
||||
}
|
||||
|
||||
@ -102,7 +102,13 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
|
||||
}
|
||||
|
||||
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
|
||||
func ToggleUserOpenIDVisibility(ctx context.Context, id int64) (err error) {
|
||||
_, err = db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ?", id)
|
||||
return err
|
||||
func ToggleUserOpenIDVisibility(ctx context.Context, id int64, user *User) error {
|
||||
affected, err := db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ? AND uid = ?", id, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n, _ := affected.RowsAffected(); n != 1 {
|
||||
return util.NewNotExistErrorf("OpenID is unknown")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -33,12 +34,14 @@ func TestGetUserOpenIDs(t *testing.T) {
|
||||
|
||||
func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, oids, 1)
|
||||
assert.True(t, oids[0].Show)
|
||||
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
@ -46,7 +49,7 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
require.Len(t, oids, 1)
|
||||
|
||||
assert.False(t, oids[0].Show)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||
@ -55,3 +58,13 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
assert.True(t, oids[0].Show)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToggleUserOpenIDVisibilityRequiresOwnership(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
unauthorizedUser, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), int64(1), unauthorizedUser)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
||||
|
||||
@ -4,12 +4,13 @@
|
||||
package analyze
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"path"
|
||||
|
||||
"github.com/go-enry/go-enry/v2"
|
||||
)
|
||||
|
||||
// GetCodeLanguage detects code language based on file name and content
|
||||
// It can be slow when the content is used for detection
|
||||
func GetCodeLanguage(filename string, content []byte) string {
|
||||
if language, ok := enry.GetLanguageByExtension(filename); ok {
|
||||
return language
|
||||
@ -23,5 +24,5 @@ func GetCodeLanguage(filename string, content []byte) string {
|
||||
return enry.OtherLanguage
|
||||
}
|
||||
|
||||
return enry.GetLanguage(filepath.Base(filename), content)
|
||||
return enry.GetLanguage(path.Base(filename), content)
|
||||
}
|
||||
|
||||
@ -6,9 +6,7 @@ package assetfs
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@ -25,7 +23,7 @@ import (
|
||||
// Layer represents a layer in a layered asset file-system. It has a name and works like http.FileSystem
|
||||
type Layer struct {
|
||||
name string
|
||||
fs http.FileSystem
|
||||
fs fs.FS
|
||||
localPath string
|
||||
}
|
||||
|
||||
@ -34,7 +32,7 @@ func (l *Layer) Name() string {
|
||||
}
|
||||
|
||||
// Open opens the named file. The caller is responsible for closing the file.
|
||||
func (l *Layer) Open(name string) (http.File, error) {
|
||||
func (l *Layer) Open(name string) (fs.File, error) {
|
||||
return l.fs.Open(name)
|
||||
}
|
||||
|
||||
@ -48,12 +46,12 @@ func Local(name, base string, sub ...string) *Layer {
|
||||
panic(fmt.Sprintf("Unable to get absolute path for %q: %v", base, err))
|
||||
}
|
||||
root := util.FilePathJoinAbs(base, sub...)
|
||||
return &Layer{name: name, fs: http.Dir(root), localPath: root}
|
||||
return &Layer{name: name, fs: os.DirFS(root), localPath: root}
|
||||
}
|
||||
|
||||
// Bindata returns a new Layer with the given name, it serves files from the given bindata asset.
|
||||
func Bindata(name string, fs fs.FS) *Layer {
|
||||
return &Layer{name: name, fs: http.FS(fs)}
|
||||
return &Layer{name: name, fs: fs}
|
||||
}
|
||||
|
||||
// LayeredFS is a layered asset file-system. It works like http.FileSystem, but it can have multiple layers.
|
||||
@ -69,7 +67,7 @@ func Layered(layers ...*Layer) *LayeredFS {
|
||||
}
|
||||
|
||||
// Open opens the named file. The caller is responsible for closing the file.
|
||||
func (l *LayeredFS) Open(name string) (http.File, error) {
|
||||
func (l *LayeredFS) Open(name string) (fs.File, error) {
|
||||
for _, layer := range l.layers {
|
||||
f, err := layer.Open(name)
|
||||
if err == nil || !os.IsNotExist(err) {
|
||||
@ -89,40 +87,34 @@ func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) {
|
||||
func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) {
|
||||
name := util.PathJoinRel(elems...)
|
||||
for _, layer := range l.layers {
|
||||
f, err := layer.Open(name)
|
||||
bs, err := fs.ReadFile(layer, name)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return nil, layer.name, err
|
||||
}
|
||||
bs, err := io.ReadAll(f)
|
||||
_ = f.Close()
|
||||
return bs, layer.name, err
|
||||
}
|
||||
return nil, "", fs.ErrNotExist
|
||||
}
|
||||
|
||||
func shouldInclude(info fs.FileInfo, fileMode ...bool) bool {
|
||||
if util.IsCommonHiddenFileName(info.Name()) {
|
||||
func shouldInclude(dirEntry fs.DirEntry, fileMode ...bool) bool {
|
||||
if util.IsCommonHiddenFileName(dirEntry.Name()) {
|
||||
return false
|
||||
}
|
||||
if len(fileMode) == 0 {
|
||||
return true
|
||||
} else if len(fileMode) == 1 {
|
||||
return fileMode[0] == !info.Mode().IsDir()
|
||||
return fileMode[0] == !dirEntry.IsDir()
|
||||
}
|
||||
panic("too many arguments for fileMode in shouldInclude")
|
||||
}
|
||||
|
||||
func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
|
||||
f, err := layer.Open(name)
|
||||
if os.IsNotExist(err) {
|
||||
func readDirOptional(layer *Layer, name string) (entries []fs.DirEntry, err error) {
|
||||
if entries, err = fs.ReadDir(layer, name); os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return f.Readdir(-1)
|
||||
return entries, err
|
||||
}
|
||||
|
||||
// ListFiles lists files/directories in the given directory. The fileMode controls the returned files.
|
||||
@ -133,13 +125,13 @@ func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
|
||||
func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) {
|
||||
fileSet := make(container.Set[string])
|
||||
for _, layer := range l.layers {
|
||||
infos, err := readDir(layer, name)
|
||||
entries, err := readDirOptional(layer, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, info := range infos {
|
||||
if shouldInclude(info, fileMode...) {
|
||||
fileSet.Add(info.Name())
|
||||
for _, entry := range entries {
|
||||
if shouldInclude(entry, fileMode...) {
|
||||
fileSet.Add(entry.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,16 +155,16 @@ func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, err
|
||||
var list func(dir string) error
|
||||
list = func(dir string) error {
|
||||
for _, layer := range layers {
|
||||
infos, err := readDir(layer, dir)
|
||||
entries, err := readDirOptional(layer, dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, info := range infos {
|
||||
path := util.PathJoinRelX(dir, info.Name())
|
||||
if shouldInclude(info, fileMode...) {
|
||||
for _, entry := range entries {
|
||||
path := util.PathJoinRelX(dir, entry.Name())
|
||||
if shouldInclude(entry, fileMode...) {
|
||||
fileSet.Add(path)
|
||||
}
|
||||
if info.IsDir() {
|
||||
if entry.IsDir() {
|
||||
if err = list(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -91,7 +92,13 @@ loop:
|
||||
}
|
||||
|
||||
for _, userStopwatches := range usersStopwatches {
|
||||
apiSWs, err := convert.ToStopWatches(ctx, userStopwatches.StopWatches)
|
||||
u, err := user_model.GetUserByID(ctx, userStopwatches.UserID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get user %d: %v", userStopwatches.UserID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
apiSWs, err := convert.ToStopWatches(ctx, u, userStopwatches.StopWatches)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrIssueNotExist(err) {
|
||||
log.Error("Unable to APIFormat stopwatches: %v", err)
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@ -20,7 +20,7 @@ import (
|
||||
type BatchChecker struct {
|
||||
attributesNum int
|
||||
repo *git.Repository
|
||||
stdinWriter *os.File
|
||||
stdinWriter io.WriteCloser
|
||||
stdOut *nulSeparatedAttributeWriter
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@ -60,10 +60,7 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string)
|
||||
},
|
||||
}
|
||||
|
||||
stdinReader, stdinWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdinWriter, stdinWriterClose := cmd.MakeStdinPipe()
|
||||
checker.stdinWriter = stdinWriter
|
||||
|
||||
lw := new(nulSeparatedAttributeWriter)
|
||||
@ -71,23 +68,19 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string)
|
||||
lw.closed = make(chan struct{})
|
||||
checker.stdOut = lw
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
_ = stdinReader.Close()
|
||||
_ = lw.Close()
|
||||
}()
|
||||
stdErr := new(bytes.Buffer)
|
||||
err := cmd.WithEnv(envs).
|
||||
WithDir(repo.Path).
|
||||
WithStdin(stdinReader).
|
||||
WithStdout(lw).
|
||||
WithStderr(stdErr).
|
||||
Run(ctx)
|
||||
cmd.WithEnv(envs).
|
||||
WithDir(repo.Path).
|
||||
WithStdoutCopy(lw)
|
||||
|
||||
if err != nil && !git.IsErrCanceledOrKilled(err) {
|
||||
go func() {
|
||||
defer stdinWriterClose()
|
||||
defer checker.cancel()
|
||||
defer lw.Close()
|
||||
|
||||
err := cmd.RunWithStderr(ctx)
|
||||
if err != nil && !gitcmd.IsErrorCanceledOrKilled(err) {
|
||||
log.Error("Attribute checker for commit %s exits with error: %v", treeish, err)
|
||||
}
|
||||
checker.cancel()
|
||||
}()
|
||||
|
||||
return checker, nil
|
||||
|
||||
@ -68,18 +68,14 @@ func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish strin
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
stdOut := new(bytes.Buffer)
|
||||
stdErr := new(bytes.Buffer)
|
||||
|
||||
if err := cmd.WithEnv(append(os.Environ(), envs...)).
|
||||
stdout, _, err := cmd.WithEnv(append(os.Environ(), envs...)).
|
||||
WithDir(gitRepo.Path).
|
||||
WithStdout(stdOut).
|
||||
WithStderr(stdErr).
|
||||
Run(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String())
|
||||
RunStdBytes(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to run check-attr: %w", err)
|
||||
}
|
||||
|
||||
fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
|
||||
fields := bytes.Split(stdout, []byte{'\000'})
|
||||
if len(fields)%3 != 1 {
|
||||
return nil, errors.New("wrong number of fields in return from check-attr")
|
||||
}
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
)
|
||||
|
||||
type Batch struct {
|
||||
cancel context.CancelFunc
|
||||
Reader *bufio.Reader
|
||||
Writer WriteCloserError
|
||||
}
|
||||
|
||||
// NewBatch creates a new batch for the given repository, the Close must be invoked before release the batch
|
||||
func NewBatch(ctx context.Context, repoPath string) (*Batch, error) {
|
||||
// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
|
||||
if err := ensureValidGitRepository(ctx, repoPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var batch Batch
|
||||
batch.Writer, batch.Reader, batch.cancel = catFileBatch(ctx, repoPath)
|
||||
return &batch, nil
|
||||
}
|
||||
|
||||
func NewBatchCheck(ctx context.Context, repoPath string) (*Batch, error) {
|
||||
// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first!
|
||||
if err := ensureValidGitRepository(ctx, repoPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var check Batch
|
||||
check.Writer, check.Reader, check.cancel = catFileBatchCheck(ctx, repoPath)
|
||||
return &check, nil
|
||||
}
|
||||
|
||||
func (b *Batch) Close() {
|
||||
if b.cancel != nil {
|
||||
b.cancel()
|
||||
b.Reader = nil
|
||||
b.Writer = nil
|
||||
b.cancel = nil
|
||||
}
|
||||
}
|
||||
@ -1,324 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/djherbis/buffer"
|
||||
"github.com/djherbis/nio/v3"
|
||||
)
|
||||
|
||||
// WriteCloserError wraps an io.WriteCloser with an additional CloseWithError function
|
||||
type WriteCloserError interface {
|
||||
io.WriteCloser
|
||||
CloseWithError(err error) error
|
||||
}
|
||||
|
||||
// ensureValidGitRepository runs git rev-parse in the repository path - thus ensuring that the repository is a valid repository.
|
||||
// Run before opening git cat-file.
|
||||
// This is needed otherwise the git cat-file will hang for invalid repositories.
|
||||
func ensureValidGitRepository(ctx context.Context, repoPath string) error {
|
||||
stderr := strings.Builder{}
|
||||
err := gitcmd.NewCommand("rev-parse").
|
||||
WithDir(repoPath).
|
||||
WithStderr(&stderr).
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
return gitcmd.ConcatenateError(err, (&stderr).String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// catFileBatchCheck opens git cat-file --batch-check in the provided repo and returns a stdin pipe, a stdout reader and cancel function
|
||||
func catFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError, *bufio.Reader, func()) {
|
||||
batchStdinReader, batchStdinWriter := io.Pipe()
|
||||
batchStdoutReader, batchStdoutWriter := io.Pipe()
|
||||
ctx, ctxCancel := context.WithCancel(ctx)
|
||||
closed := make(chan struct{})
|
||||
cancel := func() {
|
||||
ctxCancel()
|
||||
_ = batchStdoutReader.Close()
|
||||
_ = batchStdinWriter.Close()
|
||||
<-closed
|
||||
}
|
||||
|
||||
// Ensure cancel is called as soon as the provided context is cancelled
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
stderr := strings.Builder{}
|
||||
err := gitcmd.NewCommand("cat-file", "--batch-check").
|
||||
WithDir(repoPath).
|
||||
WithStdin(batchStdinReader).
|
||||
WithStdout(batchStdoutWriter).
|
||||
WithStderr(&stderr).
|
||||
WithUseContextTimeout(true).
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
_ = batchStdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
|
||||
_ = batchStdinReader.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
|
||||
} else {
|
||||
_ = batchStdoutWriter.Close()
|
||||
_ = batchStdinReader.Close()
|
||||
}
|
||||
close(closed)
|
||||
}()
|
||||
|
||||
// For simplicities sake we'll use a buffered reader to read from the cat-file --batch-check
|
||||
batchReader := bufio.NewReader(batchStdoutReader)
|
||||
|
||||
return batchStdinWriter, batchReader, cancel
|
||||
}
|
||||
|
||||
// catFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe, a stdout reader and cancel function
|
||||
func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufio.Reader, func()) {
|
||||
// We often want to feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
|
||||
// so let's create a batch stdin and stdout
|
||||
batchStdinReader, batchStdinWriter := io.Pipe()
|
||||
batchStdoutReader, batchStdoutWriter := nio.Pipe(buffer.New(32 * 1024))
|
||||
ctx, ctxCancel := context.WithCancel(ctx)
|
||||
closed := make(chan struct{})
|
||||
cancel := func() {
|
||||
ctxCancel()
|
||||
_ = batchStdinWriter.Close()
|
||||
_ = batchStdoutReader.Close()
|
||||
<-closed
|
||||
}
|
||||
|
||||
// Ensure cancel is called as soon as the provided context is cancelled
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
stderr := strings.Builder{}
|
||||
err := gitcmd.NewCommand("cat-file", "--batch").
|
||||
WithDir(repoPath).
|
||||
WithStdin(batchStdinReader).
|
||||
WithStdout(batchStdoutWriter).
|
||||
WithStderr(&stderr).
|
||||
WithUseContextTimeout(true).
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
_ = batchStdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
|
||||
_ = batchStdinReader.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
|
||||
} else {
|
||||
_ = batchStdoutWriter.Close()
|
||||
_ = batchStdinReader.Close()
|
||||
}
|
||||
close(closed)
|
||||
}()
|
||||
|
||||
// For simplicities sake we'll us a buffered reader to read from the cat-file --batch
|
||||
batchReader := bufio.NewReaderSize(batchStdoutReader, 32*1024)
|
||||
|
||||
return batchStdinWriter, batchReader, cancel
|
||||
}
|
||||
|
||||
// ReadBatchLine reads the header line from cat-file --batch
|
||||
// We expect: <oid> SP <type> SP <size> LF
|
||||
// then leaving the rest of the stream "<contents> LF" to be read
|
||||
func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
|
||||
typ, err = rd.ReadString('\n')
|
||||
if err != nil {
|
||||
return sha, typ, size, err
|
||||
}
|
||||
if len(typ) == 1 {
|
||||
typ, err = rd.ReadString('\n')
|
||||
if err != nil {
|
||||
return sha, typ, size, err
|
||||
}
|
||||
}
|
||||
idx := strings.IndexByte(typ, ' ')
|
||||
if idx < 0 {
|
||||
log.Debug("missing space typ: %s", typ)
|
||||
return sha, typ, size, ErrNotExist{ID: string(sha)}
|
||||
}
|
||||
sha = []byte(typ[:idx])
|
||||
typ = typ[idx+1:]
|
||||
|
||||
idx = strings.IndexByte(typ, ' ')
|
||||
if idx < 0 {
|
||||
return sha, typ, size, ErrNotExist{ID: string(sha)}
|
||||
}
|
||||
|
||||
sizeStr := typ[idx+1 : len(typ)-1]
|
||||
typ = typ[:idx]
|
||||
|
||||
size, err = strconv.ParseInt(sizeStr, 10, 64)
|
||||
return sha, typ, size, err
|
||||
}
|
||||
|
||||
// ReadTagObjectID reads a tag object ID hash from a cat-file --batch stream, throwing away the rest of the stream.
|
||||
func ReadTagObjectID(rd *bufio.Reader, size int64) (string, error) {
|
||||
var id string
|
||||
var n int64
|
||||
headerLoop:
|
||||
for {
|
||||
line, err := rd.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n += int64(len(line))
|
||||
idx := bytes.Index(line, []byte{' '})
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if string(line[:idx]) == "object" {
|
||||
id = string(line[idx+1 : len(line)-1])
|
||||
break headerLoop
|
||||
}
|
||||
}
|
||||
|
||||
// Discard the rest of the tag
|
||||
return id, DiscardFull(rd, size-n+1)
|
||||
}
|
||||
|
||||
// ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream.
|
||||
func ReadTreeID(rd *bufio.Reader, size int64) (string, error) {
|
||||
var id string
|
||||
var n int64
|
||||
headerLoop:
|
||||
for {
|
||||
line, err := rd.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n += int64(len(line))
|
||||
idx := bytes.Index(line, []byte{' '})
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if string(line[:idx]) == "tree" {
|
||||
id = string(line[idx+1 : len(line)-1])
|
||||
break headerLoop
|
||||
}
|
||||
}
|
||||
|
||||
// Discard the rest of the commit
|
||||
return id, DiscardFull(rd, size-n+1)
|
||||
}
|
||||
|
||||
// git tree files are a list:
|
||||
// <mode-in-ascii> SP <fname> NUL <binary Hash>
|
||||
//
|
||||
// Unfortunately this 20-byte notation is somewhat in conflict to all other git tools
|
||||
// Therefore we need some method to convert these binary hashes to hex hashes
|
||||
|
||||
// constant hextable to help quickly convert between binary and hex representation
|
||||
const hextable = "0123456789abcdef"
|
||||
|
||||
// BinToHexHeash converts a binary Hash into a hex encoded one. Input and output can be the
|
||||
// same byte slice to support in place conversion without allocations.
|
||||
// This is at least 100x quicker that hex.EncodeToString
|
||||
func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
|
||||
for i := objectFormat.FullLength()/2 - 1; i >= 0; i-- {
|
||||
v := sha[i]
|
||||
vhi, vlo := v>>4, v&0x0f
|
||||
shi, slo := hextable[vhi], hextable[vlo]
|
||||
out[i*2], out[i*2+1] = shi, slo
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream
|
||||
// This carefully avoids allocations - except where fnameBuf is too small.
|
||||
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
|
||||
//
|
||||
// Each line is composed of:
|
||||
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH>
|
||||
//
|
||||
// We don't attempt to convert the raw HASH to save a lot of time
|
||||
func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
||||
var readBytes []byte
|
||||
|
||||
// Read the Mode & fname
|
||||
readBytes, err = rd.ReadSlice('\x00')
|
||||
if err != nil {
|
||||
return mode, fname, sha, n, err
|
||||
}
|
||||
idx := bytes.IndexByte(readBytes, ' ')
|
||||
if idx < 0 {
|
||||
log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes)
|
||||
return mode, fname, sha, n, &ErrNotExist{}
|
||||
}
|
||||
|
||||
n += idx + 1
|
||||
copy(modeBuf, readBytes[:idx])
|
||||
if len(modeBuf) >= idx {
|
||||
modeBuf = modeBuf[:idx]
|
||||
} else {
|
||||
modeBuf = append(modeBuf, readBytes[len(modeBuf):idx]...)
|
||||
}
|
||||
mode = modeBuf
|
||||
|
||||
readBytes = readBytes[idx+1:]
|
||||
|
||||
// Deal with the fname
|
||||
copy(fnameBuf, readBytes)
|
||||
if len(fnameBuf) > len(readBytes) {
|
||||
fnameBuf = fnameBuf[:len(readBytes)]
|
||||
} else {
|
||||
fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...)
|
||||
}
|
||||
for err == bufio.ErrBufferFull {
|
||||
readBytes, err = rd.ReadSlice('\x00')
|
||||
fnameBuf = append(fnameBuf, readBytes...)
|
||||
}
|
||||
n += len(fnameBuf)
|
||||
if err != nil {
|
||||
return mode, fname, sha, n, err
|
||||
}
|
||||
fnameBuf = fnameBuf[:len(fnameBuf)-1]
|
||||
fname = fnameBuf
|
||||
|
||||
// Deal with the binary hash
|
||||
idx = 0
|
||||
length := objectFormat.FullLength() / 2
|
||||
for idx < length {
|
||||
var read int
|
||||
read, err = rd.Read(shaBuf[idx:length])
|
||||
n += read
|
||||
if err != nil {
|
||||
return mode, fname, sha, n, err
|
||||
}
|
||||
idx += read
|
||||
}
|
||||
sha = shaBuf
|
||||
return mode, fname, sha, n, err
|
||||
}
|
||||
|
||||
func DiscardFull(rd *bufio.Reader, discard int64) error {
|
||||
if discard > math.MaxInt32 {
|
||||
n, err := rd.Discard(math.MaxInt32)
|
||||
discard -= int64(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for discard > 0 {
|
||||
n, err := rd.Discard(int(discard))
|
||||
discard -= int64(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -6,8 +6,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -25,38 +23,28 @@ type Blob struct {
|
||||
|
||||
// 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() (io.ReadCloser, error) {
|
||||
wr, rd, cancel, err := b.repo.CatFileBatch(b.repo.Ctx)
|
||||
func (b *Blob) DataAsync() (_ io.ReadCloser, retErr error) {
|
||||
batch, cancel, err := b.repo.CatFileBatch(b.repo.Ctx)
|
||||
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()
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = wr.Write([]byte(b.ID.String() + "\n"))
|
||||
info, contentReader, err := batch.QueryContent(b.ID.String())
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
_, _, size, err := ReadBatchLine(rd)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
b.gotSize = true
|
||||
b.size = size
|
||||
|
||||
if size < 4096 {
|
||||
bs, err := io.ReadAll(io.LimitReader(rd, size))
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = rd.Discard(1)
|
||||
return io.NopCloser(bytes.NewReader(bs)), err
|
||||
}
|
||||
|
||||
b.size = info.Size
|
||||
return &blobReader{
|
||||
rd: rd,
|
||||
n: size,
|
||||
rd: contentReader,
|
||||
n: info.Size,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
@ -67,30 +55,24 @@ func (b *Blob) Size() int64 {
|
||||
return b.size
|
||||
}
|
||||
|
||||
wr, rd, cancel, err := b.repo.CatFileBatchCheck(b.repo.Ctx)
|
||||
batch, cancel, err := b.repo.CatFileBatch(b.repo.Ctx)
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
|
||||
return 0
|
||||
}
|
||||
defer cancel()
|
||||
_, err = wr.Write([]byte(b.ID.String() + "\n"))
|
||||
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.Path, err)
|
||||
return 0
|
||||
}
|
||||
_, _, b.size, err = ReadBatchLine(rd)
|
||||
if err != nil {
|
||||
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
|
||||
return 0
|
||||
}
|
||||
|
||||
b.gotSize = true
|
||||
|
||||
b.size = info.Size
|
||||
return b.size
|
||||
}
|
||||
|
||||
type blobReader struct {
|
||||
rd *bufio.Reader
|
||||
rd BufferedReader
|
||||
n int64
|
||||
cancel func()
|
||||
}
|
||||
|
||||
52
modules/git/catfile_batch.go
Normal file
52
modules/git/catfile_batch.go
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
type BufferedReader interface {
|
||||
io.Reader
|
||||
Buffered() int
|
||||
Peek(n int) ([]byte, error)
|
||||
Discard(n int) (int, error)
|
||||
ReadString(sep byte) (string, error)
|
||||
ReadSlice(sep byte) ([]byte, error)
|
||||
ReadBytes(sep byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type CatFileObject struct {
|
||||
ID string
|
||||
Type string
|
||||
Size int64
|
||||
}
|
||||
|
||||
type CatFileBatch interface {
|
||||
// QueryInfo queries the object info from the git repository by its object name using "git cat-file --batch" family commands.
|
||||
// "git cat-file" accepts "<rev>" for the object name, it can be a ref name, object id, etc. https://git-scm.com/docs/gitrevisions
|
||||
// In Gitea, we only use the simple ref name or object id, no other complex rev syntax like "suffix" or "git describe" although they are supported by git.
|
||||
QueryInfo(obj string) (*CatFileObject, error)
|
||||
|
||||
// QueryContent is similar to QueryInfo, it queries the object info and additionally returns a reader for its content.
|
||||
// FIXME: this design still follows the old pattern: the returned BufferedReader is very fragile,
|
||||
// callers should carefully maintain its lifecycle and discard all unread data.
|
||||
// TODO: It needs to be refactored to a fully managed Reader stream in the future, don't let callers manually Close or Discard
|
||||
QueryContent(obj string) (*CatFileObject, BufferedReader, error)
|
||||
}
|
||||
|
||||
type CatFileBatchCloser interface {
|
||||
CatFileBatch
|
||||
Close()
|
||||
}
|
||||
|
||||
// NewBatch creates a "batch object provider (CatFileBatch)" for the given repository path to retrieve object info and content efficiently.
|
||||
// The CatFileBatch and the readers create by it should only be used in the same goroutine.
|
||||
func NewBatch(ctx context.Context, repoPath string) (CatFileBatchCloser, error) {
|
||||
if DefaultFeatures().SupportCatFileBatchCommand {
|
||||
return newCatFileBatchCommand(ctx, repoPath)
|
||||
}
|
||||
return newCatFileBatchLegacy(ctx, repoPath)
|
||||
}
|
||||
66
modules/git/catfile_batch_command.go
Normal file
66
modules/git/catfile_batch_command.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// catFileBatchCommand implements the CatFileBatch interface using the "cat-file --batch-command" command
|
||||
// for git version >= 2.36
|
||||
// ref: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt---batch-command
|
||||
type catFileBatchCommand struct {
|
||||
ctx context.Context
|
||||
repoPath string
|
||||
batch *catFileBatchCommunicator
|
||||
}
|
||||
|
||||
var _ CatFileBatch = (*catFileBatchCommand)(nil)
|
||||
|
||||
func newCatFileBatchCommand(ctx context.Context, repoPath string) (*catFileBatchCommand, error) {
|
||||
if _, err := os.Stat(repoPath); err != nil {
|
||||
return nil, util.NewNotExistErrorf("repo %q doesn't exist", filepath.Base(repoPath))
|
||||
}
|
||||
return &catFileBatchCommand{ctx: ctx, repoPath: repoPath}, nil
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommand) getBatch() *catFileBatchCommunicator {
|
||||
if b.batch != nil {
|
||||
return b.batch
|
||||
}
|
||||
b.batch = newCatFileBatch(b.ctx, b.repoPath, gitcmd.NewCommand("cat-file", "--batch-command"))
|
||||
return b.batch
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommand) QueryContent(obj string) (*CatFileObject, BufferedReader, error) {
|
||||
_, err := b.getBatch().reqWriter.Write([]byte("contents " + obj + "\n"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
info, err := catFileBatchParseInfoLine(b.getBatch().respReader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return info, b.getBatch().respReader, nil
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommand) QueryInfo(obj string) (*CatFileObject, error) {
|
||||
_, err := b.getBatch().reqWriter.Write([]byte("info " + obj + "\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return catFileBatchParseInfoLine(b.getBatch().respReader)
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommand) Close() {
|
||||
if b.batch != nil {
|
||||
b.batch.Close()
|
||||
b.batch = nil
|
||||
}
|
||||
}
|
||||
81
modules/git/catfile_batch_legacy.go
Normal file
81
modules/git/catfile_batch_legacy.go
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// catFileBatchLegacy implements the CatFileBatch interface using the "cat-file --batch" command and "cat-file --batch-check" command
|
||||
// for git version < 2.36
|
||||
// to align with "--batch-command", it creates the two commands for querying object contents and object info separately
|
||||
// ref: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt---batch
|
||||
type catFileBatchLegacy struct {
|
||||
ctx context.Context
|
||||
repoPath string
|
||||
batchContent *catFileBatchCommunicator
|
||||
batchCheck *catFileBatchCommunicator
|
||||
}
|
||||
|
||||
var _ CatFileBatchCloser = (*catFileBatchLegacy)(nil)
|
||||
|
||||
func newCatFileBatchLegacy(ctx context.Context, repoPath string) (*catFileBatchLegacy, error) {
|
||||
if _, err := os.Stat(repoPath); err != nil {
|
||||
return nil, util.NewNotExistErrorf("repo %q doesn't exist", filepath.Base(repoPath))
|
||||
}
|
||||
return &catFileBatchLegacy{ctx: ctx, repoPath: repoPath}, nil
|
||||
}
|
||||
|
||||
func (b *catFileBatchLegacy) getBatchContent() *catFileBatchCommunicator {
|
||||
if b.batchContent != nil {
|
||||
return b.batchContent
|
||||
}
|
||||
b.batchContent = newCatFileBatch(b.ctx, b.repoPath, gitcmd.NewCommand("cat-file", "--batch"))
|
||||
return b.batchContent
|
||||
}
|
||||
|
||||
func (b *catFileBatchLegacy) getBatchCheck() *catFileBatchCommunicator {
|
||||
if b.batchCheck != nil {
|
||||
return b.batchCheck
|
||||
}
|
||||
b.batchCheck = newCatFileBatch(b.ctx, b.repoPath, gitcmd.NewCommand("cat-file", "--batch-check"))
|
||||
return b.batchCheck
|
||||
}
|
||||
|
||||
func (b *catFileBatchLegacy) QueryContent(obj string) (*CatFileObject, BufferedReader, error) {
|
||||
_, err := io.WriteString(b.getBatchContent().reqWriter, obj+"\n")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
info, err := catFileBatchParseInfoLine(b.getBatchContent().respReader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return info, b.getBatchContent().respReader, nil
|
||||
}
|
||||
|
||||
func (b *catFileBatchLegacy) QueryInfo(obj string) (*CatFileObject, error) {
|
||||
_, err := io.WriteString(b.getBatchCheck().reqWriter, obj+"\n")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return catFileBatchParseInfoLine(b.getBatchCheck().respReader)
|
||||
}
|
||||
|
||||
func (b *catFileBatchLegacy) Close() {
|
||||
if b.batchContent != nil {
|
||||
b.batchContent.Close()
|
||||
b.batchContent = nil
|
||||
}
|
||||
if b.batchCheck != nil {
|
||||
b.batchCheck.Close()
|
||||
b.batchCheck = nil
|
||||
}
|
||||
}
|
||||
243
modules/git/catfile_batch_reader.go
Normal file
243
modules/git/catfile_batch_reader.go
Normal file
@ -0,0 +1,243 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
type catFileBatchCommunicator struct {
|
||||
cancel context.CancelFunc
|
||||
reqWriter io.Writer
|
||||
respReader *bufio.Reader
|
||||
debugGitCmd *gitcmd.Command
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommunicator) Close() {
|
||||
if b.cancel != nil {
|
||||
b.cancel()
|
||||
b.cancel = nil
|
||||
}
|
||||
}
|
||||
|
||||
// newCatFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe, a stdout reader and cancel function
|
||||
func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) (ret *catFileBatchCommunicator) {
|
||||
ctx, ctxCancel := context.WithCancelCause(ctx)
|
||||
|
||||
// We often want to feed the commits in order into cat-file --batch, followed by their trees and subtrees as necessary.
|
||||
stdinWriter, stdoutReader, pipeClose := cmdCatFile.MakeStdinStdoutPipe()
|
||||
ret = &catFileBatchCommunicator{
|
||||
debugGitCmd: cmdCatFile,
|
||||
cancel: func() { ctxCancel(nil) },
|
||||
reqWriter: stdinWriter,
|
||||
respReader: bufio.NewReaderSize(stdoutReader, 32*1024), // use a buffered reader for rich operations
|
||||
}
|
||||
|
||||
err := cmdCatFile.WithDir(repoPath).StartWithStderr(ctx)
|
||||
if err != nil {
|
||||
log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err)
|
||||
// ideally here it should return the error, but it would require refactoring all callers
|
||||
// so just return a dummy communicator that does nothing, almost the same behavior as before, not bad
|
||||
ctxCancel(err)
|
||||
pipeClose()
|
||||
return ret
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := cmdCatFile.WaitWithStderr()
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err)
|
||||
}
|
||||
ctxCancel(err)
|
||||
pipeClose()
|
||||
}()
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// catFileBatchParseInfoLine reads the header line from cat-file --batch
|
||||
// We expect: <oid> SP <type> SP <size> LF
|
||||
// then leaving the rest of the stream "<contents> LF" to be read
|
||||
func catFileBatchParseInfoLine(rd BufferedReader) (*CatFileObject, error) {
|
||||
typ, err := rd.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(typ) == 1 {
|
||||
typ, err = rd.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
idx := strings.IndexByte(typ, ' ')
|
||||
if idx < 0 {
|
||||
return nil, ErrNotExist{}
|
||||
}
|
||||
sha := typ[:idx]
|
||||
typ = typ[idx+1:]
|
||||
|
||||
idx = strings.IndexByte(typ, ' ')
|
||||
if idx < 0 {
|
||||
return nil, ErrNotExist{ID: sha}
|
||||
}
|
||||
|
||||
sizeStr := typ[idx+1 : len(typ)-1]
|
||||
typ = typ[:idx]
|
||||
|
||||
size, err := strconv.ParseInt(sizeStr, 10, 64)
|
||||
return &CatFileObject{ID: sha, Type: typ, Size: size}, err
|
||||
}
|
||||
|
||||
// ReadTagObjectID reads a tag object ID hash from a cat-file --batch stream, throwing away the rest of the stream.
|
||||
func ReadTagObjectID(rd BufferedReader, size int64) (string, error) {
|
||||
var id string
|
||||
var n int64
|
||||
headerLoop:
|
||||
for {
|
||||
line, err := rd.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n += int64(len(line))
|
||||
idx := bytes.Index(line, []byte{' '})
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if string(line[:idx]) == "object" {
|
||||
id = string(line[idx+1 : len(line)-1])
|
||||
break headerLoop
|
||||
}
|
||||
}
|
||||
|
||||
// Discard the rest of the tag
|
||||
return id, DiscardFull(rd, size-n+1)
|
||||
}
|
||||
|
||||
// ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream.
|
||||
func ReadTreeID(rd BufferedReader, size int64) (string, error) {
|
||||
var id string
|
||||
var n int64
|
||||
headerLoop:
|
||||
for {
|
||||
line, err := rd.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n += int64(len(line))
|
||||
idx := bytes.Index(line, []byte{' '})
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if string(line[:idx]) == "tree" {
|
||||
id = string(line[idx+1 : len(line)-1])
|
||||
break headerLoop
|
||||
}
|
||||
}
|
||||
|
||||
// Discard the rest of the commit
|
||||
return id, DiscardFull(rd, size-n+1)
|
||||
}
|
||||
|
||||
// git tree files are a list:
|
||||
// <mode-in-ascii> SP <fname> NUL <binary Hash>
|
||||
//
|
||||
// Unfortunately this 20-byte notation is somewhat in conflict to all other git tools
|
||||
// Therefore we need some method to convert these binary hashes to hex hashes
|
||||
|
||||
// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream
|
||||
// This carefully avoids allocations - except where fnameBuf is too small.
|
||||
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
|
||||
//
|
||||
// Each line is composed of:
|
||||
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH>
|
||||
//
|
||||
// We don't attempt to convert the raw HASH to save a lot of time
|
||||
func ParseCatFileTreeLine(objectFormat ObjectFormat, rd BufferedReader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
||||
var readBytes []byte
|
||||
|
||||
// Read the Mode & fname
|
||||
readBytes, err = rd.ReadSlice('\x00')
|
||||
if err != nil {
|
||||
return mode, fname, sha, n, err
|
||||
}
|
||||
idx := bytes.IndexByte(readBytes, ' ')
|
||||
if idx < 0 {
|
||||
log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes)
|
||||
return mode, fname, sha, n, &ErrNotExist{}
|
||||
}
|
||||
|
||||
n += idx + 1
|
||||
copy(modeBuf, readBytes[:idx])
|
||||
if len(modeBuf) >= idx {
|
||||
modeBuf = modeBuf[:idx]
|
||||
} else {
|
||||
modeBuf = append(modeBuf, readBytes[len(modeBuf):idx]...)
|
||||
}
|
||||
mode = modeBuf
|
||||
|
||||
readBytes = readBytes[idx+1:]
|
||||
|
||||
// Deal with the fname
|
||||
copy(fnameBuf, readBytes)
|
||||
if len(fnameBuf) > len(readBytes) {
|
||||
fnameBuf = fnameBuf[:len(readBytes)]
|
||||
} else {
|
||||
fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...)
|
||||
}
|
||||
for err == bufio.ErrBufferFull {
|
||||
readBytes, err = rd.ReadSlice('\x00')
|
||||
fnameBuf = append(fnameBuf, readBytes...)
|
||||
}
|
||||
n += len(fnameBuf)
|
||||
if err != nil {
|
||||
return mode, fname, sha, n, err
|
||||
}
|
||||
fnameBuf = fnameBuf[:len(fnameBuf)-1]
|
||||
fname = fnameBuf
|
||||
|
||||
// Deal with the binary hash
|
||||
idx = 0
|
||||
length := objectFormat.FullLength() / 2
|
||||
for idx < length {
|
||||
var read int
|
||||
read, err = rd.Read(shaBuf[idx:length])
|
||||
n += read
|
||||
if err != nil {
|
||||
return mode, fname, sha, n, err
|
||||
}
|
||||
idx += read
|
||||
}
|
||||
sha = shaBuf
|
||||
return mode, fname, sha, n, err
|
||||
}
|
||||
|
||||
func DiscardFull(rd BufferedReader, discard int64) error {
|
||||
if discard > math.MaxInt32 {
|
||||
n, err := rd.Discard(math.MaxInt32)
|
||||
discard -= int64(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for discard > 0 {
|
||||
n, err := rd.Discard(int(discard))
|
||||
discard -= int64(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
89
modules/git/catfile_batch_test.go
Normal file
89
modules/git/catfile_batch_test.go
Normal file
@ -0,0 +1,89 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCatFileBatch(t *testing.T) {
|
||||
defer test.MockVariableValue(&DefaultFeatures().SupportCatFileBatchCommand)()
|
||||
DefaultFeatures().SupportCatFileBatchCommand = false
|
||||
t.Run("LegacyCheck", testCatFileBatch)
|
||||
DefaultFeatures().SupportCatFileBatchCommand = true
|
||||
t.Run("BatchCommand", testCatFileBatch)
|
||||
}
|
||||
|
||||
func testCatFileBatch(t *testing.T) {
|
||||
t.Run("CorruptedGitRepo", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
batch, err := NewBatch(t.Context(), tmpDir)
|
||||
// as long as the directory exists, no error, because we can't really know whether the git repo is valid until we run commands
|
||||
require.NoError(t, err)
|
||||
defer batch.Close()
|
||||
|
||||
_, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
|
||||
require.Error(t, err)
|
||||
_, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare"))
|
||||
require.NoError(t, err)
|
||||
defer batch.Close()
|
||||
|
||||
t.Run("QueryInfo", func(t *testing.T) {
|
||||
info, err := batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "e2129701f1a4d54dc44f03c93bca0a2aec7c5449", info.ID)
|
||||
assert.Equal(t, "blob", info.Type)
|
||||
assert.EqualValues(t, 6, info.Size)
|
||||
})
|
||||
|
||||
t.Run("QueryContent", func(t *testing.T) {
|
||||
info, rd, err := batch.QueryContent("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "e2129701f1a4d54dc44f03c93bca0a2aec7c5449", info.ID)
|
||||
assert.Equal(t, "blob", info.Type)
|
||||
assert.EqualValues(t, 6, info.Size)
|
||||
|
||||
content, err := io.ReadAll(io.LimitReader(rd, info.Size))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file1\n", string(content))
|
||||
})
|
||||
|
||||
t.Run("QueryTerminated", func(t *testing.T) {
|
||||
var c *catFileBatchCommunicator
|
||||
switch b := batch.(type) {
|
||||
case *catFileBatchLegacy:
|
||||
c = b.batchCheck
|
||||
_, _ = c.reqWriter.Write([]byte("in-complete-line-"))
|
||||
case *catFileBatchCommand:
|
||||
c = b.batch
|
||||
_, _ = c.reqWriter.Write([]byte("info"))
|
||||
default:
|
||||
t.FailNow()
|
||||
return
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Go(func() {
|
||||
buf := make([]byte, 100)
|
||||
_, _ = c.respReader.Read(buf)
|
||||
n, errRead := c.respReader.Read(buf)
|
||||
assert.Zero(t, n)
|
||||
assert.ErrorIs(t, errRead, io.EOF) // the pipe is closed due to command being killed
|
||||
})
|
||||
c.debugGitCmd.DebugKill()
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
@ -120,7 +120,7 @@ func CommitChanges(ctx context.Context, repoPath string, opts CommitChangesOptio
|
||||
|
||||
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
|
||||
// No stderr but exit status 1 means nothing to commit.
|
||||
if err != nil && err.Error() == "exit status 1" {
|
||||
if gitcmd.IsErrorExitCode(err, 1) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@ -315,7 +315,7 @@ func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, err
|
||||
WithDir(repoPath).
|
||||
RunStdString(ctx)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "exit status 128") {
|
||||
if gitcmd.IsErrorExitCode(err, 128) {
|
||||
return "", ErrNotExist{shortID, ""}
|
||||
}
|
||||
return "", err
|
||||
|
||||
@ -30,28 +30,57 @@ func cloneRepo(tb testing.TB, url string) (string, error) {
|
||||
}
|
||||
|
||||
func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
|
||||
type expectedEntryInfo struct {
|
||||
CommitID string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// these test case are specific to the repo1 test repo
|
||||
testCases := []struct {
|
||||
CommitID string
|
||||
Path string
|
||||
ExpectedIDs map[string]string
|
||||
ExpectedIDs map[string]expectedEntryInfo
|
||||
ExpectedTreeCommit string
|
||||
}{
|
||||
{"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", "", map[string]string{
|
||||
"file1.txt": "95bb4d39648ee7e325106df01a621c530863a653",
|
||||
"file2.txt": "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
|
||||
{"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", "", map[string]expectedEntryInfo{
|
||||
"file1.txt": {
|
||||
CommitID: "95bb4d39648ee7e325106df01a621c530863a653",
|
||||
Size: 6,
|
||||
},
|
||||
"file2.txt": {
|
||||
CommitID: "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
|
||||
Size: 6,
|
||||
},
|
||||
}, "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2"},
|
||||
{"2839944139e0de9737a044f78b0e4b40d989a9e3", "", map[string]string{
|
||||
"file1.txt": "2839944139e0de9737a044f78b0e4b40d989a9e3",
|
||||
"branch1.txt": "9c9aef8dd84e02bc7ec12641deb4c930a7c30185",
|
||||
{"2839944139e0de9737a044f78b0e4b40d989a9e3", "", map[string]expectedEntryInfo{
|
||||
"file1.txt": {
|
||||
CommitID: "2839944139e0de9737a044f78b0e4b40d989a9e3",
|
||||
Size: 15,
|
||||
},
|
||||
"branch1.txt": {
|
||||
CommitID: "9c9aef8dd84e02bc7ec12641deb4c930a7c30185",
|
||||
Size: 8,
|
||||
},
|
||||
}, "2839944139e0de9737a044f78b0e4b40d989a9e3"},
|
||||
{"5c80b0245c1c6f8343fa418ec374b13b5d4ee658", "branch2", map[string]string{
|
||||
"branch2.txt": "5c80b0245c1c6f8343fa418ec374b13b5d4ee658",
|
||||
{"5c80b0245c1c6f8343fa418ec374b13b5d4ee658", "branch2", map[string]expectedEntryInfo{
|
||||
"branch2.txt": {
|
||||
CommitID: "5c80b0245c1c6f8343fa418ec374b13b5d4ee658",
|
||||
Size: 8,
|
||||
},
|
||||
}, "5c80b0245c1c6f8343fa418ec374b13b5d4ee658"},
|
||||
{"feaf4ba6bc635fec442f46ddd4512416ec43c2c2", "", map[string]string{
|
||||
"file1.txt": "95bb4d39648ee7e325106df01a621c530863a653",
|
||||
"file2.txt": "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
|
||||
"foo": "37991dec2c8e592043f47155ce4808d4580f9123",
|
||||
{"feaf4ba6bc635fec442f46ddd4512416ec43c2c2", "", map[string]expectedEntryInfo{
|
||||
"file1.txt": {
|
||||
CommitID: "95bb4d39648ee7e325106df01a621c530863a653",
|
||||
Size: 6,
|
||||
},
|
||||
"file2.txt": {
|
||||
CommitID: "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
|
||||
Size: 6,
|
||||
},
|
||||
"foo": {
|
||||
CommitID: "37991dec2c8e592043f47155ce4808d4580f9123",
|
||||
Size: 0,
|
||||
},
|
||||
}, "feaf4ba6bc635fec442f46ddd4512416ec43c2c2"},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
@ -93,11 +122,12 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
|
||||
for _, commitInfo := range commitsInfo {
|
||||
entry := commitInfo.Entry
|
||||
commit := commitInfo.Commit
|
||||
expectedID, ok := testCase.ExpectedIDs[entry.Name()]
|
||||
expectedInfo, ok := testCase.ExpectedIDs[entry.Name()]
|
||||
if !assert.True(t, ok) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, expectedID, commit.ID.String())
|
||||
assert.Equal(t, expectedInfo.CommitID, commit.ID.String())
|
||||
assert.Equal(t, expectedInfo.Size, entry.Size(), entry.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,11 +5,9 @@ package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -17,34 +15,64 @@ import (
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// RawDiffType type of a raw diff.
|
||||
// RawDiffType output format: diff or patch
|
||||
type RawDiffType string
|
||||
|
||||
// RawDiffType possible values.
|
||||
const (
|
||||
RawDiffNormal RawDiffType = "diff"
|
||||
RawDiffPatch RawDiffType = "patch"
|
||||
)
|
||||
|
||||
// GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
|
||||
func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer io.Writer) error {
|
||||
return GetRepoRawDiffForFile(repo, "", commitID, diffType, "", writer)
|
||||
}
|
||||
|
||||
// GetRepoRawDiffForFile dumps diff results of file in given commit ID to io.Writer according given repository
|
||||
func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
|
||||
commit, err := repo.GetCommit(endCommit)
|
||||
func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer io.Writer) (retErr error) {
|
||||
diffOutput, diffFinish, err := getRepoRawDiffForFile(repo.Ctx, repo, "", commitID, diffType, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err := diffFinish()
|
||||
if retErr == nil {
|
||||
retErr = err // only return command's error if no previous error
|
||||
}
|
||||
}()
|
||||
_, err = io.Copy(writer, diffOutput)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetFileDiffCutAroundLine cuts the old or new part of the diff of a file around a specific line number
|
||||
func GetFileDiffCutAroundLine(
|
||||
repo *Repository, startCommit, endCommit, treePath string,
|
||||
line int64, old bool, numbersOfLine int,
|
||||
) (_ string, retErr error) {
|
||||
diffOutput, diffFinish, err := getRepoRawDiffForFile(repo.Ctx, repo, startCommit, endCommit, RawDiffNormal, treePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
err := diffFinish()
|
||||
if retErr == nil {
|
||||
retErr = err // only return command's error if no previous error
|
||||
}
|
||||
}()
|
||||
return CutDiffAroundLine(diffOutput, line, old, numbersOfLine)
|
||||
}
|
||||
|
||||
// getRepoRawDiffForFile returns an io.Reader for the diff results of file in given commit ID
|
||||
// and a "finish" function to wait for the git command and clean up resources after reading is done.
|
||||
func getRepoRawDiffForFile(ctx context.Context, repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string) (io.Reader, func() gitcmd.RunStdError, error) {
|
||||
commit, err := repo.GetCommit(endCommit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var files []string
|
||||
if len(file) > 0 {
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
cmd := gitcmd.NewCommand()
|
||||
cmd := gitcmd.NewCommand().WithDir(repo.Path)
|
||||
switch diffType {
|
||||
case RawDiffNormal:
|
||||
if len(startCommit) != 0 {
|
||||
@ -56,7 +84,7 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
|
||||
} else {
|
||||
c, err := commit.Parent(0)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
cmd.AddArguments("diff").
|
||||
AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold).
|
||||
@ -71,23 +99,25 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
|
||||
} else {
|
||||
c, err := commit.Parent(0)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
|
||||
cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid diffType: %s", diffType)
|
||||
return nil, nil, util.NewInvalidArgumentErrorf("invalid diff type: %s", diffType)
|
||||
}
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
if err = cmd.WithDir(repo.Path).
|
||||
WithStdout(writer).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx); err != nil {
|
||||
return fmt.Errorf("Run: %w - %s", err, stderr)
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
err = cmd.StartWithStderr(ctx)
|
||||
if err != nil {
|
||||
stdoutReaderClose()
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil
|
||||
return stdoutReader, func() gitcmd.RunStdError {
|
||||
stdoutReaderClose()
|
||||
return cmd.WaitWithStderr()
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseDiffHunkString parse the diff hunk content and return
|
||||
@ -290,30 +320,15 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str
|
||||
}
|
||||
oldCommitID = startCommitID
|
||||
}
|
||||
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
log.Error("Unable to create os.Pipe for %s", repo.Path)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
|
||||
affectedFiles := make([]string, 0, 32)
|
||||
|
||||
// Run `git diff --name-only` to get the names of the changed files
|
||||
err = gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID).
|
||||
WithEnv(env).
|
||||
WithDir(repo.Path).
|
||||
WithStdout(stdoutWriter).
|
||||
WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error {
|
||||
// Close the writer end of the pipe to begin processing
|
||||
_ = stdoutWriter.Close()
|
||||
defer func() {
|
||||
// Close the reader on return to terminate the git command if necessary
|
||||
_ = stdoutReader.Close()
|
||||
}()
|
||||
cmd := gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID)
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := cmd.WithEnv(env).WithDir(repo.Path).
|
||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
// Now scan the output from the command
|
||||
scanner := bufio.NewScanner(stdoutReader)
|
||||
for scanner.Scan() {
|
||||
|
||||
@ -4,8 +4,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -143,10 +141,3 @@ func IsErrMoreThanOne(err error) bool {
|
||||
func (err *ErrMoreThanOne) Error() string {
|
||||
return fmt.Sprintf("ErrMoreThanOne Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
|
||||
}
|
||||
|
||||
func IsErrCanceledOrKilled(err error) bool {
|
||||
// When "cancel()" a git command's context, the returned error of "Run()" could be one of them:
|
||||
// - context.Canceled
|
||||
// - *exec.ExitError: "signal: killed"
|
||||
return err != nil && (errors.Is(err, context.Canceled) || err.Error() == "signal: killed")
|
||||
}
|
||||
|
||||
@ -30,9 +30,11 @@ type Parser struct {
|
||||
func NewParser(r io.Reader, format Format) *Parser {
|
||||
scanner := bufio.NewScanner(r)
|
||||
|
||||
// default MaxScanTokenSize = 64 kiB may be too small for some references,
|
||||
// so allow the buffer to grow up to 4x if needed
|
||||
scanner.Buffer(nil, 4*bufio.MaxScanTokenSize)
|
||||
// default Scanner.MaxScanTokenSize = 64 kiB may be too small for some references,
|
||||
// so allow the buffer to be large enough in case the ref has long content (e.g.: a tag with long message)
|
||||
// as long as it doesn't exceed some reasonable limit (4 MiB here, or MAX_DISPLAY_FILE_SIZE=8MiB), it is OK
|
||||
// there are still some choices: 1. add a config option for the limit; 2. don't use scanner and write our own parser to fully handle large contents
|
||||
scanner.Buffer(nil, 4*1024*1024)
|
||||
|
||||
// in addition to the reference delimiter we specified in the --format,
|
||||
// `git for-each-ref` will always add a newline after every reference.
|
||||
|
||||
@ -12,25 +12,27 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
const RequiredVersion = "2.0.0" // the minimum Git version required
|
||||
const RequiredVersion = "2.6.0" // the minimum Git version required
|
||||
|
||||
type Features struct {
|
||||
gitVersion *version.Version
|
||||
|
||||
UsingGogit bool
|
||||
SupportProcReceive bool // >= 2.29
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
SupportedObjectFormats []ObjectFormat // sha1, sha256
|
||||
SupportCheckAttrOnBare bool // >= 2.40
|
||||
UsingGogit bool
|
||||
SupportProcReceive bool // >= 2.29
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
SupportedObjectFormats []ObjectFormat // sha1, sha256
|
||||
SupportCheckAttrOnBare bool // >= 2.40
|
||||
SupportCatFileBatchCommand bool // >= 2.36, support `git cat-file --batch-command`
|
||||
SupportGitMergeTree bool // >= 2.40 // we also need "--merge-base"
|
||||
}
|
||||
|
||||
var defaultFeatures *Features
|
||||
@ -75,6 +77,8 @@ func loadGitVersionFeatures() (*Features, error) {
|
||||
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
|
||||
}
|
||||
features.SupportCheckAttrOnBare = features.CheckVersionAtLeast("2.40")
|
||||
features.SupportCatFileBatchCommand = features.CheckVersionAtLeast("2.36")
|
||||
features.SupportGitMergeTree = features.CheckVersionAtLeast("2.40") // we also need "--merge-base"
|
||||
return features, nil
|
||||
}
|
||||
|
||||
@ -137,10 +141,6 @@ func InitSimple() error {
|
||||
log.Warn("git module has been initialized already, duplicate init may work but it's better to fix it")
|
||||
}
|
||||
|
||||
if setting.Git.Timeout.Default > 0 {
|
||||
gitcmd.SetDefaultCommandExecutionTimeout(time.Duration(setting.Git.Timeout.Default) * time.Second)
|
||||
}
|
||||
|
||||
if err := gitcmd.SetExecutablePath(setting.Git.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -176,3 +176,25 @@ func InitFull() (err error) {
|
||||
|
||||
return syncGitConfig(context.Background())
|
||||
}
|
||||
|
||||
// RunGitTests helps to init the git module and run tests.
|
||||
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right, setting.Git.HomePath is initialized in this package but used in gitcmd package
|
||||
func RunGitTests(m interface{ Run() int }) {
|
||||
fatalf := func(exitCode int, format string, args ...any) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, format, args...)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
||||
if err != nil {
|
||||
fatalf(1, "unable to create temp dir: %s", err.Error())
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
setting.Git.HomePath = gitHomePath
|
||||
if err = InitFull(); err != nil {
|
||||
fatalf(1, "failed to call Init: %s", err.Error())
|
||||
}
|
||||
if exitCode := m.Run(); exitCode != 0 {
|
||||
fatalf(exitCode, "run test failed, ExitCode=%d", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,42 +4,14 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testRun(m *testing.M) error {
|
||||
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create temp dir: %w", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
setting.Git.HomePath = gitHomePath
|
||||
|
||||
if err = InitFull(); err != nil {
|
||||
return fmt.Errorf("failed to call Init: %w", err)
|
||||
}
|
||||
|
||||
exitCode := m.Run()
|
||||
if exitCode != 0 {
|
||||
return fmt.Errorf("run test failed, ExitCode=%d", exitCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := testRun(m); err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
RunGitTests(m)
|
||||
}
|
||||
|
||||
func TestParseGitVersion(t *testing.T) {
|
||||
|
||||
@ -13,7 +13,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -29,24 +28,32 @@ import (
|
||||
// In most cases, it shouldn't be used. Use AddXxx function instead
|
||||
type TrustedCmdArgs []internal.CmdArg
|
||||
|
||||
// defaultCommandExecutionTimeout default command execution timeout duration
|
||||
var defaultCommandExecutionTimeout = 360 * time.Second
|
||||
|
||||
func SetDefaultCommandExecutionTimeout(timeout time.Duration) {
|
||||
defaultCommandExecutionTimeout = timeout
|
||||
}
|
||||
|
||||
// DefaultLocale is the default LC_ALL to run git commands in.
|
||||
const DefaultLocale = "C"
|
||||
|
||||
// Command represents a command with its subcommands or arguments.
|
||||
type Command struct {
|
||||
callerInfo string
|
||||
prog string
|
||||
args []string
|
||||
brokenArgs []string
|
||||
cmd *exec.Cmd // for debug purpose only
|
||||
preErrors []error
|
||||
configArgs []string
|
||||
opts runOpts
|
||||
|
||||
cmd *exec.Cmd
|
||||
|
||||
cmdCtx context.Context
|
||||
cmdCancel process.CancelCauseFunc
|
||||
cmdFinished process.FinishedFunc
|
||||
cmdStartTime time.Time
|
||||
|
||||
parentPipeFiles []*os.File
|
||||
parentPipeReaders []*os.File
|
||||
childrenPipeFiles []*os.File
|
||||
|
||||
// only os.Pipe and in-memory buffers can work with Stdin safely, see https://github.com/golang/go/issues/77227 if the command would exit unexpectedly
|
||||
cmdStdin io.Reader
|
||||
cmdStdout io.Writer
|
||||
cmdStderr io.Writer
|
||||
|
||||
cmdManagedStderr *bytes.Buffer
|
||||
}
|
||||
|
||||
func logArgSanitize(arg string) string {
|
||||
@ -97,6 +104,10 @@ func NewCommand(args ...internal.CmdArg) *Command {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Command) handlePreErrorBrokenCommand(arg string) {
|
||||
c.preErrors = append(c.preErrors, util.ErrorWrap(ErrBrokenCommand, `broken git command argument %q`, arg))
|
||||
}
|
||||
|
||||
// isSafeArgumentValue checks if the argument is safe to be used as a value (not an option)
|
||||
func isSafeArgumentValue(s string) bool {
|
||||
return s == "" || s[0] != '-'
|
||||
@ -124,7 +135,7 @@ func (c *Command) AddArguments(args ...internal.CmdArg) *Command {
|
||||
// The values are treated as dynamic argument values. It equals to: AddArguments("--opt") then AddDynamicArguments(val).
|
||||
func (c *Command) AddOptionValues(opt internal.CmdArg, args ...string) *Command {
|
||||
if !isValidArgumentOption(string(opt)) {
|
||||
c.brokenArgs = append(c.brokenArgs, string(opt))
|
||||
c.handlePreErrorBrokenCommand(string(opt))
|
||||
return c
|
||||
}
|
||||
c.args = append(c.args, string(opt))
|
||||
@ -136,12 +147,12 @@ func (c *Command) AddOptionValues(opt internal.CmdArg, args ...string) *Command
|
||||
// For example: AddOptionFormat("--opt=%s %s", val1, val2) means 1 argument: {"--opt=val1 val2"}.
|
||||
func (c *Command) AddOptionFormat(opt string, args ...any) *Command {
|
||||
if !isValidArgumentOption(opt) {
|
||||
c.brokenArgs = append(c.brokenArgs, opt)
|
||||
c.handlePreErrorBrokenCommand(opt)
|
||||
return c
|
||||
}
|
||||
// a quick check to make sure the format string matches the number of arguments, to find low-level mistakes ASAP
|
||||
if strings.Count(strings.ReplaceAll(opt, "%%", ""), "%") != len(args) {
|
||||
c.brokenArgs = append(c.brokenArgs, opt)
|
||||
c.handlePreErrorBrokenCommand(opt)
|
||||
return c
|
||||
}
|
||||
s := fmt.Sprintf(opt, args...)
|
||||
@ -155,10 +166,10 @@ func (c *Command) AddOptionFormat(opt string, args ...any) *Command {
|
||||
func (c *Command) AddDynamicArguments(args ...string) *Command {
|
||||
for _, arg := range args {
|
||||
if !isSafeArgumentValue(arg) {
|
||||
c.brokenArgs = append(c.brokenArgs, arg)
|
||||
c.handlePreErrorBrokenCommand(arg)
|
||||
}
|
||||
}
|
||||
if len(c.brokenArgs) != 0 {
|
||||
if len(c.preErrors) != 0 {
|
||||
return c
|
||||
}
|
||||
c.args = append(c.args, args...)
|
||||
@ -178,7 +189,7 @@ func (c *Command) AddDashesAndList(list ...string) *Command {
|
||||
func (c *Command) AddConfig(key, value string) *Command {
|
||||
kv := key + "=" + value
|
||||
if !isSafeArgumentValue(kv) {
|
||||
c.brokenArgs = append(c.brokenArgs, key)
|
||||
c.handlePreErrorBrokenCommand(kv)
|
||||
} else {
|
||||
c.configArgs = append(c.configArgs, "-c", kv)
|
||||
}
|
||||
@ -195,11 +206,9 @@ func ToTrustedCmdArgs(args []string) TrustedCmdArgs {
|
||||
return ret
|
||||
}
|
||||
|
||||
// runOpts represents parameters to run the command. If UseContextTimeout is specified, then Timeout is ignored.
|
||||
type runOpts struct {
|
||||
Env []string
|
||||
Timeout time.Duration
|
||||
UseContextTimeout bool
|
||||
Env []string
|
||||
Timeout time.Duration
|
||||
|
||||
// Dir is the working dir for the git command, however:
|
||||
// FIXME: this could be incorrect in many cases, for example:
|
||||
@ -209,21 +218,7 @@ type runOpts struct {
|
||||
// The correct approach is to use `--git-dir" global argument
|
||||
Dir string
|
||||
|
||||
Stdout, Stderr io.Writer
|
||||
|
||||
// Stdin is used for passing input to the command
|
||||
// The caller must make sure the Stdin writer is closed properly to finish the Run function.
|
||||
// Otherwise, the Run function may hang for long time or forever, especially when the Git's context deadline is not the same as the caller's.
|
||||
// Some common mistakes:
|
||||
// * `defer stdinWriter.Close()` then call `cmd.Run()`: the Run() would never return if the command is killed by timeout
|
||||
// * `go { case <- parentContext.Done(): stdinWriter.Close() }` with `cmd.Run(DefaultTimeout)`: the command would have been killed by timeout but the Run doesn't return until stdinWriter.Close()
|
||||
// * `go { if stdoutReader.Read() err != nil: stdinWriter.Close() }` with `cmd.Run()`: the stdoutReader may never return error if the command is killed by timeout
|
||||
// In the future, ideally the git module itself should have full control of the stdin, to avoid such problems and make it easier to refactor to a better architecture.
|
||||
Stdin io.Reader
|
||||
|
||||
PipelineFunc func(context.Context, context.CancelFunc) error
|
||||
|
||||
callerInfo string
|
||||
PipelineFunc func(Context) error
|
||||
}
|
||||
|
||||
func commonBaseEnvs() []string {
|
||||
@ -254,7 +249,7 @@ func commonBaseEnvs() []string {
|
||||
// CommonGitCmdEnvs returns the common environment variables for a "git" command.
|
||||
func CommonGitCmdEnvs() []string {
|
||||
return append(commonBaseEnvs(), []string{
|
||||
"LC_ALL=" + DefaultLocale,
|
||||
"LC_ALL=C", // ensure git output is in English, error messages are parsed in English
|
||||
"GIT_TERMINAL_PROMPT=0", // avoid prompting for credentials interactively, supported since git v2.3
|
||||
}...)
|
||||
}
|
||||
@ -281,42 +276,102 @@ func (c *Command) WithTimeout(timeout time.Duration) *Command {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) WithStdout(stdout io.Writer) *Command {
|
||||
c.opts.Stdout = stdout
|
||||
func (c *Command) makeStdoutStderr(w *io.Writer) (PipeReader, func()) {
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
c.preErrors = append(c.preErrors, err)
|
||||
return &pipeNull{err}, func() {}
|
||||
}
|
||||
c.childrenPipeFiles = append(c.childrenPipeFiles, pw)
|
||||
c.parentPipeFiles = append(c.parentPipeFiles, pr)
|
||||
c.parentPipeReaders = append(c.parentPipeReaders, pr)
|
||||
*w /* stdout, stderr */ = pw
|
||||
return &pipeReader{f: pr}, func() { pr.Close() }
|
||||
}
|
||||
|
||||
// MakeStdinPipe creates a writer for the command's stdin.
|
||||
// The returned closer function must be called by the caller to close the pipe.
|
||||
func (c *Command) MakeStdinPipe() (writer PipeWriter, closer func()) {
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
c.preErrors = append(c.preErrors, err)
|
||||
return &pipeNull{err}, func() {}
|
||||
}
|
||||
c.childrenPipeFiles = append(c.childrenPipeFiles, pr)
|
||||
c.parentPipeFiles = append(c.parentPipeFiles, pw)
|
||||
c.cmdStdin = pr
|
||||
return &pipeWriter{pw}, func() { pw.Close() }
|
||||
}
|
||||
|
||||
// MakeStdoutPipe creates a reader for the command's stdout.
|
||||
// The returned closer function must be called by the caller to close the pipe.
|
||||
// After the pipe reader is closed, the unread data will be discarded.
|
||||
func (c *Command) MakeStdoutPipe() (reader PipeReader, closer func()) {
|
||||
return c.makeStdoutStderr(&c.cmdStdout)
|
||||
}
|
||||
|
||||
// MakeStderrPipe is like MakeStdoutPipe, but for stderr.
|
||||
func (c *Command) MakeStderrPipe() (reader PipeReader, closer func()) {
|
||||
return c.makeStdoutStderr(&c.cmdStderr)
|
||||
}
|
||||
|
||||
func (c *Command) MakeStdinStdoutPipe() (stdin PipeWriter, stdout PipeReader, closer func()) {
|
||||
stdin, stdinClose := c.MakeStdinPipe()
|
||||
stdout, stdoutClose := c.MakeStdoutPipe()
|
||||
return stdin, stdout, func() {
|
||||
stdinClose()
|
||||
stdoutClose()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Command) WithStdinBytes(stdin []byte) *Command {
|
||||
c.cmdStdin = bytes.NewReader(stdin)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) WithStderr(stderr io.Writer) *Command {
|
||||
c.opts.Stderr = stderr
|
||||
func (c *Command) WithStdoutBuffer(w PipeBufferWriter) *Command {
|
||||
c.cmdStdout = w
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) WithStdin(stdin io.Reader) *Command {
|
||||
c.opts.Stdin = stdin
|
||||
// WithStdinCopy and WithStdoutCopy are general functions that accept any io.Reader / io.Writer.
|
||||
// In this case, Golang exec.Cmd will start new internal goroutines to do io.Copy between pipes and provided Reader/Writer.
|
||||
// If the reader or writer is blocked and never returns, then the io.Copy won't finish, then exec.Cmd.Wait won't return, which may cause deadlocks.
|
||||
// A typical deadlock example is:
|
||||
// * `r,w:=io.Pipe(); cmd.Stdin=r; defer w.Close(); cmd.Run()`: the Run() will never return because stdin reader is blocked forever and w.Close() will never be called.
|
||||
// If the reader/writer won't block forever (for example: read from a file or buffer), then these functions are safe to use.
|
||||
func (c *Command) WithStdinCopy(w io.Reader) *Command {
|
||||
c.cmdStdin = w
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) WithPipelineFunc(f func(context.Context, context.CancelFunc) error) *Command {
|
||||
func (c *Command) WithStdoutCopy(w io.Writer) *Command {
|
||||
c.cmdStdout = w
|
||||
return c
|
||||
}
|
||||
|
||||
// WithPipelineFunc sets the pipeline function for the command.
|
||||
// The pipeline function will be called in the Run / Wait function after the command is started successfully.
|
||||
// The function can read/write from/to the command's stdio pipes (if any).
|
||||
// The pipeline function can cancel (kill) the command by calling ctx.CancelPipeline before the command finishes.
|
||||
// The returned error of Run / Wait can be joined errors from the pipeline function, context cause, and command exit error.
|
||||
// Caller can get the pipeline function's error (if any) by UnwrapPipelineError.
|
||||
func (c *Command) WithPipelineFunc(f func(ctx Context) error) *Command {
|
||||
c.opts.PipelineFunc = f
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) WithUseContextTimeout(useContextTimeout bool) *Command {
|
||||
c.opts.UseContextTimeout = useContextTimeout
|
||||
return c
|
||||
}
|
||||
|
||||
// WithParentCallerInfo can be used to set the caller info (usually function name) of the parent function of the caller.
|
||||
// For most cases, "Run" family functions can get its caller info automatically
|
||||
// But if you need to call "Run" family functions in a wrapper function: "FeatureFunc -> GeneralWrapperFunc -> RunXxx",
|
||||
// then you can to call this function in GeneralWrapperFunc to set the caller info of FeatureFunc.
|
||||
// The caller info can only be set once.
|
||||
func (c *Command) WithParentCallerInfo(optInfo ...string) *Command {
|
||||
if c.opts.callerInfo != "" {
|
||||
if c.callerInfo != "" {
|
||||
return c
|
||||
}
|
||||
if len(optInfo) > 0 {
|
||||
c.opts.callerInfo = optInfo[0]
|
||||
c.callerInfo = optInfo[0]
|
||||
return c
|
||||
}
|
||||
skip := 1 /*parent "wrap/run" functions*/ + 1 /*this function*/
|
||||
@ -325,135 +380,174 @@ func (c *Command) WithParentCallerInfo(optInfo ...string) *Command {
|
||||
if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 {
|
||||
callerInfo = callerInfo[pos+1:]
|
||||
}
|
||||
c.opts.callerInfo = callerInfo
|
||||
c.callerInfo = callerInfo
|
||||
return c
|
||||
}
|
||||
|
||||
// Run runs the command
|
||||
func (c *Command) Run(ctx context.Context) error {
|
||||
if len(c.brokenArgs) != 0 {
|
||||
log.Error("git command is broken: %s, broken args: %s", c.LogString(), strings.Join(c.brokenArgs, " "))
|
||||
return ErrBrokenCommand
|
||||
func (c *Command) Start(ctx context.Context) (retErr error) {
|
||||
if c.cmd != nil {
|
||||
// this is a programming error, it will cause serious deadlock problems, so it must be fixed.
|
||||
panic("git command has already been started")
|
||||
}
|
||||
|
||||
// We must not change the provided options
|
||||
timeout := c.opts.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultCommandExecutionTimeout
|
||||
defer func() {
|
||||
c.closePipeFiles(c.childrenPipeFiles)
|
||||
if retErr != nil {
|
||||
// release the pipes to avoid resource leak since the command failed to start
|
||||
c.closePipeFiles(c.parentPipeFiles)
|
||||
// if error occurs, we must also finish the task, otherwise, cmdFinished will be called in "Wait" function
|
||||
if c.cmdFinished != nil {
|
||||
c.cmdFinished()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if len(c.preErrors) != 0 {
|
||||
// In most cases, such error shouldn't happen. If it happens, log it as error level with more details
|
||||
err := errors.Join(c.preErrors...)
|
||||
log.Error("git command: %s, error: %s", c.LogString(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
cmdLogString := c.LogString()
|
||||
if c.opts.callerInfo == "" {
|
||||
if c.callerInfo == "" {
|
||||
c.WithParentCallerInfo()
|
||||
}
|
||||
// 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.opts.callerInfo, logArgSanitize(c.opts.Dir), cmdLogString)
|
||||
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.opts.Dir), cmdLogString)
|
||||
log.Debug("git.Command: %s", desc)
|
||||
|
||||
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
|
||||
defer span.End()
|
||||
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.opts.callerInfo)
|
||||
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo)
|
||||
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
|
||||
|
||||
var cancel context.CancelFunc
|
||||
var finished context.CancelFunc
|
||||
|
||||
if c.opts.UseContextTimeout {
|
||||
ctx, cancel, finished = process.GetManager().AddContext(ctx, desc)
|
||||
if c.opts.Timeout <= 0 {
|
||||
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContext(ctx, desc)
|
||||
} else {
|
||||
ctx, cancel, finished = process.GetManager().AddContextTimeout(ctx, timeout, desc)
|
||||
c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContextTimeout(ctx, c.opts.Timeout, desc)
|
||||
}
|
||||
defer finished()
|
||||
|
||||
startTime := time.Now()
|
||||
c.cmdStartTime = time.Now()
|
||||
|
||||
cmd := exec.CommandContext(ctx, c.prog, append(c.configArgs, c.args...)...)
|
||||
c.cmd = cmd // for debug purpose only
|
||||
c.cmd = exec.CommandContext(c.cmdCtx, c.prog, append(c.configArgs, c.args...)...)
|
||||
if c.opts.Env == nil {
|
||||
cmd.Env = os.Environ()
|
||||
c.cmd.Env = os.Environ()
|
||||
} else {
|
||||
cmd.Env = c.opts.Env
|
||||
c.cmd.Env = c.opts.Env
|
||||
}
|
||||
|
||||
process.SetSysProcAttribute(cmd)
|
||||
cmd.Env = append(cmd.Env, CommonGitCmdEnvs()...)
|
||||
cmd.Dir = c.opts.Dir
|
||||
cmd.Stdout = c.opts.Stdout
|
||||
cmd.Stderr = c.opts.Stderr
|
||||
cmd.Stdin = c.opts.Stdin
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
process.SetSysProcAttribute(c.cmd)
|
||||
c.cmd.Env = append(c.cmd.Env, CommonGitCmdEnvs()...)
|
||||
c.cmd.Dir = c.opts.Dir
|
||||
c.cmd.Stdout = c.cmdStdout
|
||||
c.cmd.Stdin = c.cmdStdin
|
||||
c.cmd.Stderr = c.cmdStderr
|
||||
return c.cmd.Start()
|
||||
}
|
||||
|
||||
func (c *Command) closePipeFiles(files []*os.File) {
|
||||
for _, f := range files {
|
||||
_ = f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Command) discardPipeReaders(files []*os.File) {
|
||||
for _, f := range files {
|
||||
_, _ = io.Copy(io.Discard, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Command) Wait() error {
|
||||
defer func() {
|
||||
// The reader in another goroutine might be still reading the stdout, so we shouldn't close the pipes here
|
||||
// MakeStdoutPipe returns a closer function to force callers to close the pipe correctly
|
||||
// Here we only need to mark the command as finished
|
||||
c.cmdFinished()
|
||||
}()
|
||||
|
||||
if c.opts.PipelineFunc != nil {
|
||||
err := c.opts.PipelineFunc(ctx, cancel)
|
||||
if err != nil {
|
||||
cancel()
|
||||
_ = cmd.Wait()
|
||||
return err
|
||||
errPipeline := c.opts.PipelineFunc(&cmdContext{Context: c.cmdCtx, cmd: c})
|
||||
|
||||
if context.Cause(c.cmdCtx) == nil {
|
||||
// if the context is not canceled explicitly, we need to discard the unread data,
|
||||
// and wait for the command to exit normally, and then get its exit code
|
||||
c.discardPipeReaders(c.parentPipeReaders)
|
||||
} // else: canceled command will be killed, and the exit code is caused by kill
|
||||
|
||||
// after the pipeline function returns, we can safely close the pipes, then wait for the command to exit
|
||||
c.closePipeFiles(c.parentPipeFiles)
|
||||
errWait := c.cmd.Wait()
|
||||
errCause := context.Cause(c.cmdCtx) // in case the cause is set during Wait(), get the final cancel cause
|
||||
|
||||
if unwrapped, ok := UnwrapPipelineError(errCause); ok {
|
||||
if unwrapped != errPipeline {
|
||||
panic("unwrapped context pipeline error should be the same one returned by pipeline function")
|
||||
}
|
||||
if unwrapped == nil {
|
||||
// the pipeline function declares that there is no error, and it cancels (kills) the command ahead,
|
||||
// so we should ignore the errors from "wait" and "cause"
|
||||
errWait, errCause = nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// some legacy code still need to access the error returned by pipeline function by "==" but not "errors.Is"
|
||||
// so we need to make sure the original error is able to be unwrapped by UnwrapPipelineError
|
||||
return errors.Join(wrapPipelineError(errPipeline), errCause, errWait)
|
||||
}
|
||||
|
||||
err := cmd.Wait()
|
||||
elapsed := time.Since(startTime)
|
||||
// there might be other goroutines using the context or pipes, so we just wait for the command to finish
|
||||
errWait := c.cmd.Wait()
|
||||
elapsed := time.Since(c.cmdStartTime)
|
||||
if elapsed > time.Second {
|
||||
log.Debug("slow git.Command.Run: %s (%s)", c, elapsed)
|
||||
log.Debug("slow git.Command.Run: %s (%s)", c, elapsed) // TODO: no need to log this for long-running commands
|
||||
}
|
||||
|
||||
// We need to check if the context is canceled by the program on Windows.
|
||||
// This is because Windows does not have signal checking when terminating the process.
|
||||
// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
|
||||
// `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled.
|
||||
if runtime.GOOS == "windows" &&
|
||||
err != nil &&
|
||||
(err.Error() == "" || err.Error() == "exit status 1") &&
|
||||
cmd.ProcessState.ExitCode() == 1 &&
|
||||
ctx.Err() == context.Canceled {
|
||||
return ctx.Err()
|
||||
}
|
||||
// Here the logic is different from "PipelineFunc" case,
|
||||
// because PipelineFunc can return error if it fails, it knows whether it succeeds or fails.
|
||||
// But in normal case, the caller just runs the git command, the command's exit code is the source of truth.
|
||||
// If the caller need to know whether the command error is caused by cancellation, it should check the "err" by itself.
|
||||
errCause := context.Cause(c.cmdCtx)
|
||||
return errors.Join(errCause, errWait)
|
||||
}
|
||||
|
||||
if err != nil && ctx.Err() != context.DeadlineExceeded {
|
||||
func (c *Command) StartWithStderr(ctx context.Context) RunStdError {
|
||||
if c.cmdStderr != nil {
|
||||
panic("caller-provided stderr receiver doesn't work with managed stderr buffer")
|
||||
}
|
||||
c.cmdManagedStderr = &bytes.Buffer{}
|
||||
c.cmdStderr = c.cmdManagedStderr
|
||||
err := c.Start(ctx)
|
||||
if err != nil {
|
||||
return &runStdError{err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Command) WaitWithStderr() RunStdError {
|
||||
if c.cmdManagedStderr == nil {
|
||||
panic("managed stderr buffer is not initialized")
|
||||
}
|
||||
errWait := c.Wait()
|
||||
if errWait == nil {
|
||||
// if no exec error but only stderr output, the stderr output is still saved in "c.cmdManagedStderr" and can be read later
|
||||
return nil
|
||||
}
|
||||
return &runStdError{err: errWait, stderr: util.UnsafeBytesToString(c.cmdManagedStderr.Bytes())}
|
||||
}
|
||||
|
||||
func (c *Command) RunWithStderr(ctx context.Context) RunStdError {
|
||||
if err := c.StartWithStderr(ctx); err != nil {
|
||||
return &runStdError{err: err}
|
||||
}
|
||||
return c.WaitWithStderr()
|
||||
}
|
||||
|
||||
func (c *Command) Run(ctx context.Context) (err error) {
|
||||
if err = c.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
type RunStdError interface {
|
||||
error
|
||||
Unwrap() error
|
||||
Stderr() string
|
||||
}
|
||||
|
||||
type runStdError struct {
|
||||
err error
|
||||
stderr string
|
||||
errMsg string
|
||||
}
|
||||
|
||||
func (r *runStdError) Error() string {
|
||||
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
|
||||
// But a lof of code only checks `strings.Contains(err.Error(), "git error")`
|
||||
if r.errMsg == "" {
|
||||
r.errMsg = ConcatenateError(r.err, r.stderr).Error()
|
||||
}
|
||||
return r.errMsg
|
||||
}
|
||||
|
||||
func (r *runStdError) Unwrap() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *runStdError) Stderr() string {
|
||||
return r.stderr
|
||||
}
|
||||
|
||||
func IsErrorExitCode(err error, code int) bool {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
return exitError.ExitCode() == code
|
||||
}
|
||||
return false
|
||||
return c.Wait()
|
||||
}
|
||||
|
||||
// RunStdString runs the command and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr).
|
||||
@ -467,22 +561,16 @@ func (c *Command) RunStdBytes(ctx context.Context) (stdout, stderr []byte, runEr
|
||||
return c.WithParentCallerInfo().runStdBytes(ctx)
|
||||
}
|
||||
|
||||
func (c *Command) runStdBytes(ctx context.Context) ( /*stdout*/ []byte /*stderr*/, []byte /*runErr*/, RunStdError) {
|
||||
if c.opts.Stdout != nil || c.opts.Stderr != nil {
|
||||
// we must panic here, otherwise there would be bugs if developers set Stdin/Stderr by mistake, and it would be very difficult to debug
|
||||
func (c *Command) runStdBytes(ctx context.Context) ([]byte, []byte, RunStdError) {
|
||||
if c.cmdStdout != nil || c.cmdStderr != nil {
|
||||
// it must panic here, otherwise there would be bugs if developers set other Stdin/Stderr by mistake, and it would be very difficult to debug
|
||||
panic("stdout and stderr field must be nil when using RunStdBytes")
|
||||
}
|
||||
stdoutBuf := &bytes.Buffer{}
|
||||
stderrBuf := &bytes.Buffer{}
|
||||
err := c.WithParentCallerInfo().
|
||||
WithStdout(stdoutBuf).
|
||||
WithStderr(stderrBuf).
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
|
||||
// But a lot of code depends on it, so we have to keep this behavior
|
||||
return nil, stderrBuf.Bytes(), &runStdError{err: err, stderr: util.UnsafeBytesToString(stderrBuf.Bytes())}
|
||||
}
|
||||
// even if there is no err, there could still be some stderr output
|
||||
return stdoutBuf.Bytes(), stderrBuf.Bytes(), nil
|
||||
err := c.WithParentCallerInfo().WithStdoutBuffer(stdoutBuf).RunWithStderr(ctx)
|
||||
return stdoutBuf.Bytes(), c.cmdManagedStderr.Bytes(), err
|
||||
}
|
||||
|
||||
func (c *Command) DebugKill() {
|
||||
_ = c.cmd.Process.Kill()
|
||||
}
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build race
|
||||
|
||||
package gitcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRunWithContextNoTimeout(t *testing.T) {
|
||||
maxLoops := 10
|
||||
|
||||
// 'git --version' does not block so it must be finished before the timeout triggered.
|
||||
cmd := NewCommand("--version")
|
||||
for i := 0; i < maxLoops; i++ {
|
||||
if err := cmd.Run(t.Context()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithContextTimeout(t *testing.T) {
|
||||
maxLoops := 10
|
||||
|
||||
// 'git hash-object --stdin' blocks on stdin so we can have the timeout triggered.
|
||||
cmd := NewCommand("hash-object", "--stdin")
|
||||
for i := 0; i < maxLoops; i++ {
|
||||
if err := cmd.WithTimeout(1 * time.Millisecond).Run(t.Context()); err != nil {
|
||||
if err != context.DeadlineExceeded {
|
||||
t.Fatalf("Testing %d/%d: %v", i, maxLoops, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,17 +4,22 @@
|
||||
package gitcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right.
|
||||
// "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package
|
||||
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "unable to create temp dir: %v", err)
|
||||
@ -42,7 +47,7 @@ func TestRunWithContextStd(t *testing.T) {
|
||||
assert.Equal(t, stderr, err.Stderr())
|
||||
assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr())
|
||||
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
|
||||
assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such\n", err.Error())
|
||||
assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such", err.Error())
|
||||
assert.Empty(t, stdout)
|
||||
}
|
||||
}
|
||||
@ -54,7 +59,7 @@ func TestRunWithContextStd(t *testing.T) {
|
||||
assert.Equal(t, string(stderr), err.Stderr())
|
||||
assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr())
|
||||
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
|
||||
assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such\n", err.Error())
|
||||
assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such", err.Error())
|
||||
assert.Empty(t, stdout)
|
||||
}
|
||||
}
|
||||
@ -97,3 +102,29 @@ func TestCommandString(t *testing.T) {
|
||||
cmd = NewCommand("url: https://a:b@c/", "/root/dir-a/dir-b")
|
||||
assert.Equal(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString())
|
||||
}
|
||||
|
||||
func TestRunStdError(t *testing.T) {
|
||||
e := &runStdError{stderr: "some error"}
|
||||
var err RunStdError = e
|
||||
|
||||
var asErr RunStdError
|
||||
require.ErrorAs(t, err, &asErr)
|
||||
require.Equal(t, "some error", asErr.Stderr())
|
||||
|
||||
require.ErrorAs(t, fmt.Errorf("wrapped %w", err), &asErr)
|
||||
}
|
||||
|
||||
func TestRunWithContextTimeout(t *testing.T) {
|
||||
t.Run("NoTimeout", func(t *testing.T) {
|
||||
// 'git --version' does not block so it must be finished before the timeout triggered.
|
||||
err := NewCommand("--version").Run(t.Context())
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("WithTimeout", func(t *testing.T) {
|
||||
cmd := NewCommand("hash-object", "--stdin")
|
||||
_, _, pipeClose := cmd.MakeStdinStdoutPipe()
|
||||
defer pipeClose()
|
||||
err := cmd.WithTimeout(1 * time.Millisecond).Run(t.Context())
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
})
|
||||
}
|
||||
|
||||
32
modules/git/gitcmd/context.go
Normal file
32
modules/git/gitcmd/context.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
context.Context
|
||||
|
||||
// CancelPipeline is a helper function to cancel the command context (kill the command) with a specific error cause,
|
||||
// it returns the same error for convenience to break the PipelineFunc easily
|
||||
CancelPipeline(err error) error
|
||||
|
||||
// In the future, this interface will be extended to support stdio pipe readers/writers
|
||||
}
|
||||
|
||||
type cmdContext struct {
|
||||
context.Context
|
||||
cmd *Command
|
||||
}
|
||||
|
||||
func (c *cmdContext) CancelPipeline(err error) error {
|
||||
// pipelineError is used to distinguish between:
|
||||
// * context canceled by pipeline caller with/without error (normal cancellation)
|
||||
// * context canceled by parent context (still context.Canceled error)
|
||||
// * other causes
|
||||
c.cmd.cmdCancel(pipelineError{err})
|
||||
return err
|
||||
}
|
||||
101
modules/git/gitcmd/error.go
Normal file
101
modules/git/gitcmd/error.go
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RunStdError interface {
|
||||
error
|
||||
Unwrap() error
|
||||
Stderr() string
|
||||
}
|
||||
|
||||
type runStdError struct {
|
||||
err error // usually the low-level error like `*exec.ExitError`
|
||||
stderr string // git command's stderr output
|
||||
errMsg string // the cached error message for Error() method
|
||||
}
|
||||
|
||||
func (r *runStdError) Error() string {
|
||||
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
|
||||
// But a lot of code only checks `strings.Contains(err.Error(), "git error")`
|
||||
if r.errMsg == "" {
|
||||
r.errMsg = fmt.Sprintf("%s - %s", r.err.Error(), strings.TrimSpace(r.stderr))
|
||||
}
|
||||
return r.errMsg
|
||||
}
|
||||
|
||||
func (r *runStdError) Unwrap() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *runStdError) Stderr() string {
|
||||
return r.stderr
|
||||
}
|
||||
|
||||
func ErrorAsStderr(err error) (string, bool) {
|
||||
var runErr RunStdError
|
||||
if errors.As(err, &runErr) {
|
||||
return runErr.Stderr(), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func StderrHasPrefix(err error, prefix string) bool {
|
||||
stderr, ok := ErrorAsStderr(err)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(stderr, prefix)
|
||||
}
|
||||
|
||||
func IsErrorExitCode(err error, code int) bool {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
return exitError.ExitCode() == code
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsErrorSignalKilled(err error) bool {
|
||||
var exitError *exec.ExitError
|
||||
return errors.As(err, &exitError) && exitError.String() == "signal: killed"
|
||||
}
|
||||
|
||||
func IsErrorCanceledOrKilled(err error) bool {
|
||||
// When "cancel()" a git command's context, the returned error of "Run()" could be one of them:
|
||||
// - context.Canceled
|
||||
// - *exec.ExitError: "signal: killed"
|
||||
// TODO: in the future, we need to use unified error type from gitcmd.Run to check whether it is manually canceled
|
||||
return errors.Is(err, context.Canceled) || IsErrorSignalKilled(err)
|
||||
}
|
||||
|
||||
type pipelineError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (e pipelineError) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func wrapPipelineError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return pipelineError{err}
|
||||
}
|
||||
|
||||
func UnwrapPipelineError(err error) (error, bool) { //nolint:revive // this is for error unwrapping
|
||||
var pe pipelineError
|
||||
if errors.As(err, &pe) {
|
||||
return pe.error, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
79
modules/git/gitcmd/pipe.go
Normal file
79
modules/git/gitcmd/pipe.go
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitcmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type PipeBufferReader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
type PipeBufferWriter interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
type PipeReader interface {
|
||||
io.ReadCloser
|
||||
internalOnly()
|
||||
}
|
||||
|
||||
type pipeReader struct {
|
||||
f *os.File
|
||||
}
|
||||
|
||||
func (r *pipeReader) internalOnly() {}
|
||||
|
||||
func (r *pipeReader) Read(p []byte) (n int, err error) {
|
||||
return r.f.Read(p)
|
||||
}
|
||||
|
||||
func (r *pipeReader) Close() error {
|
||||
return r.f.Close()
|
||||
}
|
||||
|
||||
type PipeWriter interface {
|
||||
io.WriteCloser
|
||||
internalOnly()
|
||||
}
|
||||
|
||||
type pipeWriter struct {
|
||||
f *os.File
|
||||
}
|
||||
|
||||
func (w *pipeWriter) internalOnly() {}
|
||||
|
||||
func (w *pipeWriter) Close() error {
|
||||
return w.f.Close()
|
||||
}
|
||||
|
||||
func (w *pipeWriter) Write(p []byte) (n int, err error) {
|
||||
return w.f.Write(p)
|
||||
}
|
||||
|
||||
func (w *pipeWriter) DrainBeforeClose() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type pipeNull struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *pipeNull) internalOnly() {}
|
||||
|
||||
func (p *pipeNull) Read([]byte) (n int, err error) {
|
||||
return 0, p.err
|
||||
}
|
||||
|
||||
func (p *pipeNull) Write([]byte) (n int, err error) {
|
||||
return 0, p.err
|
||||
}
|
||||
|
||||
func (p *pipeNull) Close() error {
|
||||
return nil
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gitcmd
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ConcatenateError concatenats an error with stderr string
|
||||
func ConcatenateError(err error, stderr string) error {
|
||||
if len(stderr) == 0 {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%w - %s", err, stderr)
|
||||
}
|
||||
@ -5,11 +5,9 @@ package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -42,15 +40,6 @@ type GrepOptions struct {
|
||||
}
|
||||
|
||||
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
|
||||
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create os pipe to grep: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
|
||||
/*
|
||||
The output is like this ( "^@" means \x00):
|
||||
|
||||
@ -83,14 +72,11 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
|
||||
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
|
||||
cmd.AddDashesAndList(opts.PathspecList...)
|
||||
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
|
||||
stderr := bytes.Buffer{}
|
||||
err = cmd.WithDir(repo.Path).
|
||||
WithStdout(stdoutWriter).
|
||||
WithStderr(&stderr).
|
||||
WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error {
|
||||
_ = stdoutWriter.Close()
|
||||
defer stdoutReader.Close()
|
||||
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := cmd.WithDir(repo.Path).
|
||||
WithPipelineFunc(func(ctx gitcmd.Context) error {
|
||||
isInBlock := false
|
||||
rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024))
|
||||
var res *GrepResult
|
||||
@ -116,8 +102,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
|
||||
}
|
||||
if line == "" {
|
||||
if len(results) >= opts.MaxResultLimit {
|
||||
cancel()
|
||||
break
|
||||
return ctx.CancelPipeline(nil)
|
||||
}
|
||||
isInBlock = false
|
||||
continue
|
||||
@ -133,17 +118,17 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Run(ctx)
|
||||
RunWithStderr(ctx)
|
||||
// git grep exits by cancel (killed), usually it is caused by the limit of results
|
||||
if gitcmd.IsErrorExitCode(err, -1) && stderr.Len() == 0 {
|
||||
if gitcmd.IsErrorExitCode(err, -1) && err.Stderr() == "" {
|
||||
return results, nil
|
||||
}
|
||||
// git grep exits with 1 if no results are found
|
||||
if gitcmd.IsErrorExitCode(err, 1) && stderr.Len() == 0 {
|
||||
if gitcmd.IsErrorExitCode(err, 1) && err.Stderr() == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String())
|
||||
return nil, fmt.Errorf("unable to run git grep: %w", err)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@ -22,33 +22,28 @@ import (
|
||||
func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, error) {
|
||||
// We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary.
|
||||
// so let's create a batch stdin and stdout
|
||||
batchStdinWriter, batchReader, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
writeID := func(id string) error {
|
||||
_, err := batchStdinWriter.Write([]byte(id + "\n"))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeID(commitID); err != nil {
|
||||
commitInfo, batchReader, err := batch.QueryContent(commitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shaBytes, typ, size, err := git.ReadBatchLine(batchReader)
|
||||
if typ != "commit" {
|
||||
if commitInfo.Type != "commit" {
|
||||
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
||||
return nil, git.ErrNotExist{ID: commitID}
|
||||
}
|
||||
|
||||
sha, err := git.NewIDFromString(string(shaBytes))
|
||||
sha, err := git.NewIDFromString(commitInfo.ID)
|
||||
if err != nil {
|
||||
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
||||
return nil, git.ErrNotExist{ID: commitID}
|
||||
}
|
||||
|
||||
commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, size))
|
||||
commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, commitInfo.Size))
|
||||
if err != nil {
|
||||
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
|
||||
return nil, err
|
||||
@ -144,20 +139,16 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
||||
// If content can not be read or file is too big just do detection by filename
|
||||
|
||||
if f.Size() <= bigFileSize {
|
||||
if err := writeID(f.ID.String()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _, size, err := git.ReadBatchLine(batchReader)
|
||||
info, _, err := batch.QueryContent(f.ID.String())
|
||||
if err != nil {
|
||||
log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sizeToRead := size
|
||||
sizeToRead := info.Size
|
||||
discard := int64(1)
|
||||
if size > fileSizeLimit {
|
||||
if info.Size > fileSizeLimit {
|
||||
sizeToRead = fileSizeLimit
|
||||
discard = size - fileSizeLimit + 1
|
||||
discard = info.Size - fileSizeLimit + 1
|
||||
}
|
||||
|
||||
_, err = contentBuf.ReadFrom(io.LimitReader(batchReader, sizeToRead))
|
||||
|
||||
@ -4,37 +4,11 @@
|
||||
package languagestats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func testRun(m *testing.M) error {
|
||||
gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create temp dir: %w", err)
|
||||
}
|
||||
defer util.RemoveAll(gitHomePath)
|
||||
setting.Git.HomePath = gitHomePath
|
||||
|
||||
if err = git.InitFull(); err != nil {
|
||||
return fmt.Errorf("failed to call Init: %w", err)
|
||||
}
|
||||
|
||||
exitCode := m.Run()
|
||||
if exitCode != 0 {
|
||||
return fmt.Errorf("run test failed, ExitCode=%d", exitCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := testRun(m); err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
git.RunGitTests(m)
|
||||
}
|
||||
|
||||
@ -15,25 +15,12 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
|
||||
"github.com/djherbis/buffer"
|
||||
"github.com/djherbis/nio/v3"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// LogNameStatusRepo opens git log --raw in the provided repo and returns a stdin pipe, a stdout reader and cancel function
|
||||
func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, paths ...string) (*bufio.Reader, func()) {
|
||||
// We often want to feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
|
||||
// so let's create a batch stdin and stdout
|
||||
stdoutReader, stdoutWriter := nio.Pipe(buffer.New(32 * 1024))
|
||||
|
||||
// Lets also create a context so that we can absolutely ensure that the command should die when we're done
|
||||
ctx, ctxCancel := context.WithCancel(ctx)
|
||||
|
||||
cancel := func() {
|
||||
ctxCancel()
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}
|
||||
|
||||
cmd := gitcmd.NewCommand()
|
||||
cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head)
|
||||
@ -63,24 +50,21 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p
|
||||
}
|
||||
cmd.AddDashesAndList(files...)
|
||||
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
ctx, ctxCancel := context.WithCancel(ctx)
|
||||
go func() {
|
||||
stderr := strings.Builder{}
|
||||
err := cmd.WithDir(repository).
|
||||
WithStdout(stdoutWriter).
|
||||
WithStderr(&stderr).
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
_ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
|
||||
return
|
||||
err := cmd.WithDir(repository).RunWithStderr(ctx)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Error("Unable to run git command %v: %v", cmd.LogString(), err)
|
||||
}
|
||||
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
|
||||
// For simplicities sake we'll us a buffered reader to read from the cat-file --batch
|
||||
bufReader := bufio.NewReaderSize(stdoutReader, 32*1024)
|
||||
|
||||
return bufReader, cancel
|
||||
return bufReader, func() {
|
||||
ctxCancel()
|
||||
stdoutReaderClose()
|
||||
}
|
||||
}
|
||||
|
||||
// LogNameStatusRepoParser parses a git log raw output from LogRawRepo
|
||||
|
||||
@ -46,8 +46,8 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) {
|
||||
entry.Size = optional.Some(size)
|
||||
}
|
||||
|
||||
entry.EntryMode, err = ParseEntryMode(string(entryMode))
|
||||
if err != nil || entry.EntryMode == EntryModeNoEntry {
|
||||
entry.EntryMode = ParseEntryMode(string(entryMode))
|
||||
if entry.EntryMode == EntryModeNoEntry {
|
||||
return nil, fmt.Errorf("invalid ls-tree output (invalid mode): %q, err: %w", line, err)
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -47,7 +46,7 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.Reader, sz int64) ([]*TreeEntry, error) {
|
||||
func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd BufferedReader, sz int64) ([]*TreeEntry, error) {
|
||||
fnameBuf := make([]byte, 4096)
|
||||
modeBuf := make([]byte, 40)
|
||||
shaBuf := make([]byte, objectFormat.FullLength())
|
||||
|
||||
@ -5,81 +5,34 @@ package pipeline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// CatFileBatchCheck runs cat-file with --batch-check
|
||||
func CatFileBatchCheck(ctx context.Context, shasToCheckReader *io.PipeReader, catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) {
|
||||
defer wg.Done()
|
||||
defer shasToCheckReader.Close()
|
||||
defer catFileCheckWriter.Close()
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
var errbuf strings.Builder
|
||||
cmd := gitcmd.NewCommand("cat-file", "--batch-check")
|
||||
if err := cmd.WithDir(tmpBasePath).
|
||||
WithStdin(shasToCheckReader).
|
||||
WithStdout(catFileCheckWriter).
|
||||
WithStderr(stderr).
|
||||
Run(ctx); err != nil {
|
||||
_ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check [%s]: %w - %s", tmpBasePath, err, errbuf.String()))
|
||||
}
|
||||
func CatFileBatchCheck(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error {
|
||||
cmd.AddArguments("cat-file", "--batch-check")
|
||||
return cmd.WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// CatFileBatchCheckAllObjects runs cat-file with --batch-check --batch-all
|
||||
func CatFileBatchCheckAllObjects(ctx context.Context, catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string, errChan chan<- error) {
|
||||
defer wg.Done()
|
||||
defer catFileCheckWriter.Close()
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
var errbuf strings.Builder
|
||||
cmd := gitcmd.NewCommand("cat-file", "--batch-check", "--batch-all-objects")
|
||||
if err := cmd.WithDir(tmpBasePath).
|
||||
WithStdout(catFileCheckWriter).
|
||||
WithStderr(stderr).
|
||||
Run(ctx); err != nil {
|
||||
log.Error("git cat-file --batch-check --batch-all-object [%s]: %v - %s", tmpBasePath, err, errbuf.String())
|
||||
err = fmt.Errorf("git cat-file --batch-check --batch-all-object [%s]: %w - %s", tmpBasePath, err, errbuf.String())
|
||||
_ = catFileCheckWriter.CloseWithError(err)
|
||||
errChan <- err
|
||||
}
|
||||
func CatFileBatchCheckAllObjects(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error {
|
||||
return cmd.AddArguments("cat-file", "--batch-check", "--batch-all-objects").WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// CatFileBatch runs cat-file --batch
|
||||
func CatFileBatch(ctx context.Context, shasToBatchReader *io.PipeReader, catFileBatchWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) {
|
||||
defer wg.Done()
|
||||
defer shasToBatchReader.Close()
|
||||
defer catFileBatchWriter.Close()
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
var errbuf strings.Builder
|
||||
if err := gitcmd.NewCommand("cat-file", "--batch").
|
||||
WithDir(tmpBasePath).
|
||||
WithStdin(shasToBatchReader).
|
||||
WithStdout(catFileBatchWriter).
|
||||
WithStderr(stderr).
|
||||
Run(ctx); err != nil {
|
||||
_ = shasToBatchReader.CloseWithError(fmt.Errorf("git rev-list [%s]: %w - %s", tmpBasePath, err, errbuf.String()))
|
||||
}
|
||||
func CatFileBatch(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error {
|
||||
return cmd.AddArguments("cat-file", "--batch").WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// BlobsLessThan1024FromCatFileBatchCheck reads a pipeline from cat-file --batch-check and returns the blobs <1024 in size
|
||||
func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, shasToBatchWriter *io.PipeWriter, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer catFileCheckReader.Close()
|
||||
scanner := bufio.NewScanner(catFileCheckReader)
|
||||
defer func() {
|
||||
_ = shasToBatchWriter.CloseWithError(scanner.Err())
|
||||
}()
|
||||
func BlobsLessThan1024FromCatFileBatchCheck(in io.ReadCloser, out io.WriteCloser) error {
|
||||
defer out.Close()
|
||||
scanner := bufio.NewScanner(in)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
@ -95,12 +48,12 @@ func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, s
|
||||
}
|
||||
toWrite := []byte(fields[0] + "\n")
|
||||
for len(toWrite) > 0 {
|
||||
n, err := shasToBatchWriter.Write(toWrite)
|
||||
n, err := out.Write(toWrite)
|
||||
if err != nil {
|
||||
_ = catFileCheckReader.CloseWithError(err)
|
||||
break
|
||||
return err
|
||||
}
|
||||
toWrite = toWrite[n:]
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@ -26,7 +25,3 @@ type lfsResultSlice []*LFSResult
|
||||
func (a lfsResultSlice) Len() int { return len(a) }
|
||||
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
||||
|
||||
func lfsError(msg string, err error) error {
|
||||
return fmt.Errorf("LFS error occurred, %s: err: %w", msg, err)
|
||||
}
|
||||
|
||||
@ -6,11 +6,10 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
@ -24,7 +23,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
resultsMap := map[string]*LFSResult{}
|
||||
results := make([]*LFSResult, 0)
|
||||
|
||||
basePath := repo.Path
|
||||
gogitRepo := repo.GoGitRepo()
|
||||
|
||||
commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
|
||||
@ -32,7 +30,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, lfsError("failed to get GoGit CommitsIter", err)
|
||||
return nil, fmt.Errorf("LFS error occurred, failed to get GoGit CommitsIter: err: %w", err)
|
||||
}
|
||||
|
||||
err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
|
||||
@ -66,7 +64,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
return nil
|
||||
})
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, lfsError("failure in CommitIter.ForEach", err)
|
||||
return nil, fmt.Errorf("LFS error occurred, failure in CommitIter.ForEach: %w", err)
|
||||
}
|
||||
|
||||
for _, result := range resultsMap {
|
||||
@ -82,65 +80,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
}
|
||||
|
||||
sort.Sort(lfsResultSlice(results))
|
||||
|
||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||
shasToNameReader, shasToNameWriter := io.Pipe()
|
||||
nameRevStdinReader, nameRevStdinWriter := io.Pipe()
|
||||
errChan := make(chan error, 1)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scanner := bufio.NewScanner(nameRevStdinReader)
|
||||
i := 0
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
result := results[i]
|
||||
result.FullCommitName = line
|
||||
result.BranchName = strings.Split(line, "~")[0]
|
||||
i++
|
||||
}
|
||||
}()
|
||||
go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer shasToNameWriter.Close()
|
||||
for _, result := range results {
|
||||
i := 0
|
||||
if i < len(result.SHA) {
|
||||
n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
break
|
||||
}
|
||||
i += n
|
||||
}
|
||||
n := 0
|
||||
for n < 1 {
|
||||
n, err = shasToNameWriter.Write([]byte{'\n'})
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err, has := <-errChan:
|
||||
if has {
|
||||
return nil, lfsError("unable to obtain name for LFS files", err)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return results, nil
|
||||
err = fillResultNameRev(repo.Ctx, repo.Path, results)
|
||||
return results, err
|
||||
}
|
||||
|
||||
@ -8,46 +8,34 @@ package pipeline
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
// FindLFSFile finds commits that contain a provided pointer file hash
|
||||
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
|
||||
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) (results []*LFSResult, _ error) {
|
||||
cmd := gitcmd.NewCommand("rev-list", "--all")
|
||||
revListReader, revListReaderClose := cmd.MakeStdoutPipe()
|
||||
defer revListReaderClose()
|
||||
err := cmd.WithDir(repo.Path).
|
||||
WithPipelineFunc(func(context gitcmd.Context) (err error) {
|
||||
results, err = findLFSFileFunc(repo, objectID, revListReader)
|
||||
return err
|
||||
}).RunWithStderr(repo.Ctx)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader io.Reader) ([]*LFSResult, error) {
|
||||
resultsMap := map[string]*LFSResult{}
|
||||
results := make([]*LFSResult, 0)
|
||||
|
||||
basePath := repo.Path
|
||||
|
||||
// Use rev-list to provide us with all commits in order
|
||||
revListReader, revListWriter := io.Pipe()
|
||||
defer func() {
|
||||
_ = revListWriter.Close()
|
||||
_ = revListReader.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
stderr := strings.Builder{}
|
||||
err := gitcmd.NewCommand("rev-list", "--all").
|
||||
WithDir(repo.Path).
|
||||
WithStdout(revListWriter).
|
||||
WithStderr(&stderr).
|
||||
Run(repo.Ctx)
|
||||
if err != nil {
|
||||
_ = revListWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
|
||||
} else {
|
||||
_ = revListWriter.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Next feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.
|
||||
// so let's create a batch stdin and stdout
|
||||
batchStdinWriter, batchReader, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -55,7 +43,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
|
||||
// We'll use a scanner for the revList because it's simpler than a bufio.Reader
|
||||
scan := bufio.NewScanner(revListReader)
|
||||
trees := [][]byte{}
|
||||
trees := []string{}
|
||||
paths := []string{}
|
||||
|
||||
fnameBuf := make([]byte, 4096)
|
||||
@ -64,14 +52,10 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
|
||||
for scan.Scan() {
|
||||
// Get the next commit ID
|
||||
commitID := scan.Bytes()
|
||||
commitID := scan.Text()
|
||||
|
||||
// push the commit to the cat-file --batch process
|
||||
_, err := batchStdinWriter.Write(commitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = batchStdinWriter.Write([]byte{'\n'})
|
||||
info, batchReader, err := batch.QueryContent(commitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -81,26 +65,20 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
|
||||
commitReadingLoop:
|
||||
for {
|
||||
_, typ, size, err := git.ReadBatchLine(batchReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch typ {
|
||||
switch info.Type {
|
||||
case "tag":
|
||||
// This shouldn't happen but if it does well just get the commit and try again
|
||||
id, err := git.ReadTagObjectID(batchReader, size)
|
||||
id, err := git.ReadTagObjectID(batchReader, info.Size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = batchStdinWriter.Write([]byte(id + "\n"))
|
||||
if err != nil {
|
||||
if info, batchReader, err = batch.QueryContent(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
case "commit":
|
||||
// Read in the commit to get its tree and in case this is one of the last used commits
|
||||
curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(string(commitID)), io.LimitReader(batchReader, size))
|
||||
curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(commitID), io.LimitReader(batchReader, info.Size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -108,13 +86,13 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := batchStdinWriter.Write([]byte(curCommit.Tree.ID.String() + "\n")); err != nil {
|
||||
if info, _, err = batch.QueryContent(curCommit.Tree.ID.String()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curPath = ""
|
||||
case "tree":
|
||||
var n int64
|
||||
for n < size {
|
||||
for n < info.Size {
|
||||
mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -130,9 +108,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
}
|
||||
resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
|
||||
} else if string(mode) == git.EntryModeTree.String() {
|
||||
hexObjectID := make([]byte, objectID.Type().FullLength())
|
||||
git.BinToHex(objectID.Type(), binObjectID, hexObjectID)
|
||||
trees = append(trees, hexObjectID)
|
||||
trees = append(trees, hex.EncodeToString(binObjectID))
|
||||
paths = append(paths, curPath+string(fname)+"/")
|
||||
}
|
||||
}
|
||||
@ -140,11 +116,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
return nil, err
|
||||
}
|
||||
if len(trees) > 0 {
|
||||
_, err := batchStdinWriter.Write(trees[len(trees)-1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = batchStdinWriter.Write([]byte("\n"))
|
||||
info, _, err = batch.QueryContent(trees[len(trees)-1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -155,7 +127,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
break commitReadingLoop
|
||||
}
|
||||
default:
|
||||
if err := git.DiscardFull(batchReader, size+1); err != nil {
|
||||
if err := git.DiscardFull(batchReader, info.Size+1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@ -179,56 +151,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
|
||||
}
|
||||
|
||||
sort.Sort(lfsResultSlice(results))
|
||||
|
||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||
shasToNameReader, shasToNameWriter := io.Pipe()
|
||||
nameRevStdinReader, nameRevStdinWriter := io.Pipe()
|
||||
errChan := make(chan error, 1)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
scanner := bufio.NewScanner(nameRevStdinReader)
|
||||
i := 0
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
result := results[i]
|
||||
result.FullCommitName = line
|
||||
result.BranchName = strings.Split(line, "~")[0]
|
||||
i++
|
||||
}
|
||||
}()
|
||||
go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer shasToNameWriter.Close()
|
||||
for _, result := range results {
|
||||
_, err := shasToNameWriter.Write([]byte(result.SHA))
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
break
|
||||
}
|
||||
_, err = shasToNameWriter.Write([]byte{'\n'})
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err, has := <-errChan:
|
||||
if has {
|
||||
return nil, lfsError("unable to obtain name for LFS files", err)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return results, nil
|
||||
err = fillResultNameRev(repo.Ctx, repo.Path, results)
|
||||
return results, err
|
||||
}
|
||||
|
||||
38
modules/git/pipeline/lfs_test.go
Normal file
38
modules/git/pipeline/lfs_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFindLFSFile(t *testing.T) {
|
||||
repoPath := "../../../tests/gitea-repositories-meta/user2/lfs.git"
|
||||
gitRepo, err := git.OpenRepository(t.Context(), repoPath)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
objectID := git.MustIDFromString("2b6c6c4eaefa24b22f2092c3d54b263ff26feb58")
|
||||
|
||||
stats, err := FindLFSFile(gitRepo, objectID)
|
||||
require.NoError(t, err)
|
||||
|
||||
tm, err := time.Parse(time.RFC3339, "2022-12-21T17:56:42-05:00")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, stats, 1)
|
||||
assert.Equal(t, "CONTRIBUTING.md", stats[0].Name)
|
||||
assert.Equal(t, "73cf03db6ece34e12bf91e8853dc58f678f2f82d", stats[0].SHA)
|
||||
assert.Equal(t, "Initial commit", stats[0].Summary)
|
||||
assert.Equal(t, tm, stats[0].When)
|
||||
assert.Empty(t, stats[0].ParentHashes)
|
||||
assert.Equal(t, "master", stats[0].BranchName)
|
||||
assert.Equal(t, "master", stats[0].FullCommitName)
|
||||
}
|
||||
14
modules/git/pipeline/main_test.go
Normal file
14
modules/git/pipeline/main_test.go
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
git.RunGitTests(m)
|
||||
}
|
||||
@ -4,30 +4,54 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// NameRevStdin runs name-rev --stdin
|
||||
func NameRevStdin(ctx context.Context, shasToNameReader *io.PipeReader, nameRevStdinWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) {
|
||||
defer wg.Done()
|
||||
defer shasToNameReader.Close()
|
||||
defer nameRevStdinWriter.Close()
|
||||
func fillResultNameRev(ctx context.Context, basePath string, results []*LFSResult) error {
|
||||
// Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
|
||||
wg := errgroup.Group{}
|
||||
cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithDir(basePath)
|
||||
stdin, stdinClose := cmd.MakeStdinPipe()
|
||||
stdout, stdoutClose := cmd.MakeStdoutPipe()
|
||||
defer stdinClose()
|
||||
defer stdoutClose()
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
var errbuf strings.Builder
|
||||
if err := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").
|
||||
WithDir(tmpBasePath).
|
||||
WithStdin(shasToNameReader).
|
||||
WithStdout(nameRevStdinWriter).
|
||||
WithStderr(stderr).
|
||||
Run(ctx); err != nil {
|
||||
_ = shasToNameReader.CloseWithError(fmt.Errorf("git name-rev [%s]: %w - %s", tmpBasePath, err, errbuf.String()))
|
||||
}
|
||||
wg.Go(func() error {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
i := 0
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
result := results[i]
|
||||
result.FullCommitName = line
|
||||
result.BranchName = strings.Split(line, "~")[0]
|
||||
i++
|
||||
}
|
||||
return scanner.Err()
|
||||
})
|
||||
wg.Go(func() error {
|
||||
defer stdinClose()
|
||||
for _, result := range results {
|
||||
_, err := stdin.Write([]byte(result.SHA))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stdin.Write([]byte{'\n'})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
err := cmd.RunWithStderr(ctx)
|
||||
return errors.Join(err, wg.Wait())
|
||||
}
|
||||
|
||||
@ -5,63 +5,26 @@ package pipeline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// RevListAllObjects runs rev-list --objects --all and writes to a pipewriter
|
||||
func RevListAllObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, basePath string, errChan chan<- error) {
|
||||
defer wg.Done()
|
||||
defer revListWriter.Close()
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
var errbuf strings.Builder
|
||||
cmd := gitcmd.NewCommand("rev-list", "--objects", "--all")
|
||||
if err := cmd.WithDir(basePath).
|
||||
WithStdout(revListWriter).
|
||||
WithStderr(stderr).
|
||||
Run(ctx); err != nil {
|
||||
log.Error("git rev-list --objects --all [%s]: %v - %s", basePath, err, errbuf.String())
|
||||
err = fmt.Errorf("git rev-list --objects --all [%s]: %w - %s", basePath, err, errbuf.String())
|
||||
_ = revListWriter.CloseWithError(err)
|
||||
errChan <- err
|
||||
}
|
||||
}
|
||||
|
||||
// RevListObjects run rev-list --objects from headSHA to baseSHA
|
||||
func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath, headSHA, baseSHA string, errChan chan<- error) {
|
||||
defer wg.Done()
|
||||
defer revListWriter.Close()
|
||||
stderr := new(bytes.Buffer)
|
||||
var errbuf strings.Builder
|
||||
cmd := gitcmd.NewCommand("rev-list", "--objects").AddDynamicArguments(headSHA)
|
||||
func RevListObjects(ctx context.Context, cmd *gitcmd.Command, tmpBasePath, headSHA, baseSHA string) error {
|
||||
cmd.AddArguments("rev-list", "--objects").AddDynamicArguments(headSHA)
|
||||
if baseSHA != "" {
|
||||
cmd = cmd.AddArguments("--not").AddDynamicArguments(baseSHA)
|
||||
}
|
||||
if err := cmd.WithDir(tmpBasePath).
|
||||
WithStdout(revListWriter).
|
||||
WithStderr(stderr).
|
||||
Run(ctx); err != nil {
|
||||
log.Error("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String())
|
||||
errChan <- fmt.Errorf("git rev-list [%s]: %w - %s", tmpBasePath, err, errbuf.String())
|
||||
}
|
||||
return cmd.WithDir(tmpBasePath).RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// BlobsFromRevListObjects reads a RevListAllObjects and only selects blobs
|
||||
func BlobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io.PipeWriter, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer revListReader.Close()
|
||||
scanner := bufio.NewScanner(revListReader)
|
||||
defer func() {
|
||||
_ = shasToCheckWriter.CloseWithError(scanner.Err())
|
||||
}()
|
||||
func BlobsFromRevListObjects(in io.ReadCloser, out io.WriteCloser) error {
|
||||
defer out.Close()
|
||||
scanner := bufio.NewScanner(in)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
@ -73,12 +36,12 @@ func BlobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io
|
||||
}
|
||||
toWrite := []byte(fields[0] + "\n")
|
||||
for len(toWrite) > 0 {
|
||||
n, err := shasToCheckWriter.Write(toWrite)
|
||||
n, err := out.Write(toWrite)
|
||||
if err != nil {
|
||||
_ = revListReader.CloseWithError(err)
|
||||
break
|
||||
return err
|
||||
}
|
||||
toWrite = toWrite[n:]
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
@ -220,3 +220,14 @@ func (ref RefName) RefWebLinkPath() string {
|
||||
}
|
||||
return string(refType) + "/" + util.PathEscapeSegments(ref.ShortName())
|
||||
}
|
||||
|
||||
func ParseRefSuffix(ref string) (string, string) {
|
||||
// Partially support https://git-scm.com/docs/gitrevisions
|
||||
if idx := strings.Index(ref, "@{"); idx != -1 {
|
||||
return ref[:idx], ref[idx:]
|
||||
}
|
||||
if idx := strings.Index(ref, "^"); idx != -1 {
|
||||
return ref[:idx], ref[idx:]
|
||||
}
|
||||
return ref, ""
|
||||
}
|
||||
|
||||
@ -74,9 +74,9 @@ func (err *ErrInvalidCloneAddr) Unwrap() error {
|
||||
func IsRemoteNotExistError(err error) bool {
|
||||
// see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216
|
||||
// Should not add space in the end, sometimes git will add a `:`
|
||||
prefix1 := "exit status 128 - fatal: No such remote" // git < 2.30
|
||||
prefix2 := "exit status 2 - error: No such remote" // git >= 2.30
|
||||
return strings.HasPrefix(err.Error(), prefix1) || strings.HasPrefix(err.Error(), prefix2)
|
||||
prefix1 := "fatal: No such remote" // git < 2.30, exit status 128
|
||||
prefix2 := "error: No such remote" // git >= 2.30. exit status 2
|
||||
return gitcmd.StderrHasPrefix(err, prefix1) || gitcmd.StderrHasPrefix(err, prefix2)
|
||||
}
|
||||
|
||||
// ParseRemoteAddr checks if given remote address is valid,
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@ -83,22 +82,19 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma
|
||||
|
||||
// IsEmpty Check if repository is empty.
|
||||
func (repo *Repository) IsEmpty() (bool, error) {
|
||||
var errbuf, output strings.Builder
|
||||
if err := gitcmd.NewCommand().
|
||||
stdout, _, err := gitcmd.NewCommand().
|
||||
AddOptionFormat("--git-dir=%s", repo.Path).
|
||||
AddArguments("rev-list", "-n", "1", "--all").
|
||||
WithDir(repo.Path).
|
||||
WithStdout(&output).
|
||||
WithStderr(&errbuf).
|
||||
Run(repo.Ctx); err != nil {
|
||||
if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" {
|
||||
RunStdString(repo.Ctx)
|
||||
if err != nil {
|
||||
if (gitcmd.IsErrorExitCode(err, 1) && err.Stderr() == "") || gitcmd.IsErrorExitCode(err, 129) {
|
||||
// git 2.11 exits with 129 if the repo is empty
|
||||
return true, nil
|
||||
}
|
||||
return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String())
|
||||
return true, fmt.Errorf("check empty: %w", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(output.String()) == "", nil
|
||||
return strings.TrimSpace(stdout) == "", nil
|
||||
}
|
||||
|
||||
// CloneRepoOptions options when clone a repository
|
||||
@ -171,21 +167,16 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
if err := cmd.
|
||||
return cmd.
|
||||
WithTimeout(opts.Timeout).
|
||||
WithEnv(envs).
|
||||
WithStdout(io.Discard).
|
||||
WithStderr(stderr).
|
||||
Run(ctx); err != nil {
|
||||
return gitcmd.ConcatenateError(err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
RunWithStderr(ctx)
|
||||
}
|
||||
|
||||
// PushOptions options when push to remote
|
||||
type PushOptions struct {
|
||||
Remote string
|
||||
LocalRefName string
|
||||
Branch string
|
||||
Force bool
|
||||
ForceWithLease string
|
||||
@ -207,7 +198,13 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error {
|
||||
}
|
||||
remoteBranchArgs := []string{opts.Remote}
|
||||
if len(opts.Branch) > 0 {
|
||||
remoteBranchArgs = append(remoteBranchArgs, opts.Branch)
|
||||
var refspec string
|
||||
if opts.LocalRefName != "" {
|
||||
refspec = fmt.Sprintf("%s:%s", opts.LocalRefName, opts.Branch)
|
||||
} else {
|
||||
refspec = opts.Branch
|
||||
}
|
||||
remoteBranchArgs = append(remoteBranchArgs, refspec)
|
||||
}
|
||||
cmd.AddDashesAndList(remoteBranchArgs...)
|
||||
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
// ArchiveType archive types
|
||||
type ArchiveType int
|
||||
|
||||
const (
|
||||
ArchiveUnknown ArchiveType = iota
|
||||
ArchiveZip // 1
|
||||
ArchiveTarGz // 2
|
||||
ArchiveBundle // 3
|
||||
)
|
||||
|
||||
// String converts an ArchiveType to string: the extension of the archive file without prefix dot
|
||||
func (a ArchiveType) String() string {
|
||||
switch a {
|
||||
case ArchiveZip:
|
||||
return "zip"
|
||||
case ArchiveTarGz:
|
||||
return "tar.gz"
|
||||
case ArchiveBundle:
|
||||
return "bundle"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func SplitArchiveNameType(s string) (string, ArchiveType) {
|
||||
switch {
|
||||
case strings.HasSuffix(s, ".zip"):
|
||||
return strings.TrimSuffix(s, ".zip"), ArchiveZip
|
||||
case strings.HasSuffix(s, ".tar.gz"):
|
||||
return strings.TrimSuffix(s, ".tar.gz"), ArchiveTarGz
|
||||
case strings.HasSuffix(s, ".bundle"):
|
||||
return strings.TrimSuffix(s, ".bundle"), ArchiveBundle
|
||||
}
|
||||
return s, ArchiveUnknown
|
||||
}
|
||||
|
||||
// CreateArchive create archive content to the target path
|
||||
func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, target io.Writer, usePrefix bool, commitID string) error {
|
||||
if format.String() == "unknown" {
|
||||
return fmt.Errorf("unknown format: %v", format)
|
||||
}
|
||||
|
||||
cmd := gitcmd.NewCommand("archive")
|
||||
if usePrefix {
|
||||
cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/")
|
||||
}
|
||||
cmd.AddOptionFormat("--format=%s", format.String())
|
||||
cmd.AddDynamicArguments(commitID)
|
||||
|
||||
var stderr strings.Builder
|
||||
err := cmd.WithDir(repo.Path).
|
||||
WithStdout(target).
|
||||
WithStderr(&stderr).
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
return gitcmd.ConcatenateError(err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -7,9 +7,9 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -23,11 +23,9 @@ type Repository struct {
|
||||
|
||||
tagCache *ObjectCache[*Tag]
|
||||
|
||||
batchInUse bool
|
||||
batch *Batch
|
||||
|
||||
checkInUse bool
|
||||
check *Batch
|
||||
mu sync.Mutex
|
||||
catFileBatchCloser CatFileBatchCloser
|
||||
catFileBatchInUse bool
|
||||
|
||||
Ctx context.Context
|
||||
LastCommitCache *LastCommitCache
|
||||
@ -56,69 +54,47 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CatFileBatch obtains a CatFileBatch for this repository
|
||||
func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) {
|
||||
if repo.batch == nil {
|
||||
var err error
|
||||
repo.batch, err = NewBatch(ctx, repo.Path)
|
||||
// CatFileBatch obtains a "batch object provider" for this repository.
|
||||
// It reuses an existing one if available, otherwise creates a new one.
|
||||
func (repo *Repository) CatFileBatch(ctx context.Context) (_ CatFileBatch, closeFunc func(), err error) {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
|
||||
if repo.catFileBatchCloser == nil {
|
||||
repo.catFileBatchCloser, err = NewBatch(ctx, repo.Path)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
repo.catFileBatchCloser = nil // otherwise it is "interface(nil)" and will cause wrong logic
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !repo.batchInUse {
|
||||
repo.batchInUse = true
|
||||
return repo.batch.Writer, repo.batch.Reader, func() {
|
||||
repo.batchInUse = false
|
||||
if !repo.catFileBatchInUse {
|
||||
repo.catFileBatchInUse = true
|
||||
return CatFileBatch(repo.catFileBatchCloser), func() {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
repo.catFileBatchInUse = false
|
||||
}, nil
|
||||
}
|
||||
|
||||
log.Debug("Opening temporary cat file batch for: %s", repo.Path)
|
||||
tempBatch, err := NewBatch(ctx, repo.Path)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
return tempBatch.Writer, tempBatch.Reader, tempBatch.Close, nil
|
||||
}
|
||||
|
||||
// CatFileBatchCheck obtains a CatFileBatchCheck for this repository
|
||||
func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) {
|
||||
if repo.check == nil {
|
||||
var err error
|
||||
repo.check, err = NewBatchCheck(ctx, repo.Path)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !repo.checkInUse {
|
||||
repo.checkInUse = true
|
||||
return repo.check.Writer, repo.check.Reader, func() {
|
||||
repo.checkInUse = false
|
||||
}, nil
|
||||
}
|
||||
|
||||
log.Debug("Opening temporary cat file batch-check for: %s", repo.Path)
|
||||
tempBatchCheck, err := NewBatchCheck(ctx, repo.Path)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return tempBatchCheck.Writer, tempBatchCheck.Reader, tempBatchCheck.Close, nil
|
||||
return tempBatch, tempBatch.Close, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) Close() error {
|
||||
if repo == nil {
|
||||
return nil
|
||||
}
|
||||
if repo.batch != nil {
|
||||
repo.batch.Close()
|
||||
repo.batch = nil
|
||||
repo.batchInUse = false
|
||||
}
|
||||
if repo.check != nil {
|
||||
repo.check.Close()
|
||||
repo.check = nil
|
||||
repo.checkInUse = false
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
if repo.catFileBatchCloser != nil {
|
||||
repo.catFileBatchCloser.Close()
|
||||
repo.catFileBatchCloser = nil
|
||||
repo.catFileBatchInUse = false
|
||||
}
|
||||
repo.LastCommitCache = nil
|
||||
repo.tagCache = nil
|
||||
|
||||
26
modules/git/repo_base_nogogit_test.go
Normal file
26
modules/git/repo_base_nogogit_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepoCatFileBatch(t *testing.T) {
|
||||
t.Run("MissingRepoAndClose", func(t *testing.T) {
|
||||
repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare"))
|
||||
require.NoError(t, err)
|
||||
repo.Path = "/no-such" // when the repo is missing (it usually occurs during testing because the fixtures are synced frequently)
|
||||
_, _, err = repo.CatFileBatch(t.Context())
|
||||
require.Error(t, err)
|
||||
require.NoError(t, repo.Close()) // shouldn't panic
|
||||
})
|
||||
|
||||
// TODO: test more methods and concurrency queries
|
||||
}
|
||||
@ -8,7 +8,6 @@ package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
@ -18,24 +17,24 @@ import (
|
||||
)
|
||||
|
||||
// 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(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx)
|
||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
if err != nil {
|
||||
log.Debug("Error writing to CatFileBatchCheck %v", err)
|
||||
log.Debug("Error opening CatFileBatch %v", err)
|
||||
return false
|
||||
}
|
||||
defer cancel()
|
||||
_, err = wr.Write([]byte(name + "\n"))
|
||||
info, err := batch.QueryInfo(name)
|
||||
if err != nil {
|
||||
log.Debug("Error writing to CatFileBatchCheck %v", err)
|
||||
log.Debug("Error checking object info %v", err)
|
||||
return false
|
||||
}
|
||||
sha, _, _, err := ReadBatchLine(rd)
|
||||
return err == nil && bytes.HasPrefix(sha, []byte(strings.TrimSpace(name)))
|
||||
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.
|
||||
@ -44,18 +43,13 @@ func (repo *Repository) IsReferenceExist(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx)
|
||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
if err != nil {
|
||||
log.Debug("Error writing to CatFileBatchCheck %v", err)
|
||||
log.Error("Error opening CatFileBatch %v", err)
|
||||
return false
|
||||
}
|
||||
defer cancel()
|
||||
_, err = wr.Write([]byte(name + "\n"))
|
||||
if err != nil {
|
||||
log.Debug("Error writing to CatFileBatchCheck %v", err)
|
||||
return false
|
||||
}
|
||||
_, _, _, err = ReadBatchLine(rd)
|
||||
_, err = batch.QueryInfo(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@ -100,94 +94,81 @@ func callShowRef(ctx context.Context, repoPath, trimPrefix string, extraArgs git
|
||||
}
|
||||
|
||||
func WalkShowRef(ctx context.Context, repoPath string, extraArgs gitcmd.TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
stderrBuilder := &strings.Builder{}
|
||||
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
|
||||
args = append(args, extraArgs...)
|
||||
err := gitcmd.NewCommand(args...).
|
||||
WithDir(repoPath).
|
||||
WithStdout(stdoutWriter).
|
||||
WithStderr(stderrBuilder).
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
if stderrBuilder.Len() == 0 {
|
||||
_ = stdoutWriter.Close()
|
||||
return
|
||||
}
|
||||
_ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, stderrBuilder.String()))
|
||||
} else {
|
||||
_ = stdoutWriter.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
i := 0
|
||||
bufReader := bufio.NewReader(stdoutReader)
|
||||
for i < skip {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return i, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
|
||||
args = append(args, extraArgs...)
|
||||
cmd := gitcmd.NewCommand(args...)
|
||||
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
cmd.WithDir(repoPath).
|
||||
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
|
||||
}
|
||||
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 i, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, 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 i, nil
|
||||
}
|
||||
if err != nil {
|
||||
return i, 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 i, err
|
||||
}
|
||||
i++
|
||||
}
|
||||
// count all refs
|
||||
for limit != 0 {
|
||||
_, isPrefix, err := bufReader.ReadLine()
|
||||
if err == io.EOF {
|
||||
return i, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !isPrefix {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return i, nil
|
||||
return i, err
|
||||
}
|
||||
|
||||
// GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash
|
||||
|
||||
@ -226,66 +226,55 @@ type CommitsByFileAndRangeOptions struct {
|
||||
|
||||
// CommitsByFileAndRange return the commits according revision file and the page
|
||||
func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
go func() {
|
||||
stderr := strings.Builder{}
|
||||
gitCmd := gitcmd.NewCommand("rev-list").
|
||||
AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
|
||||
AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
|
||||
gitCmd.AddDynamicArguments(opts.Revision)
|
||||
gitCmd := gitcmd.NewCommand("rev-list").
|
||||
AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
|
||||
AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
|
||||
gitCmd.AddDynamicArguments(opts.Revision)
|
||||
|
||||
if opts.Not != "" {
|
||||
gitCmd.AddOptionValues("--not", opts.Not)
|
||||
}
|
||||
if opts.Since != "" {
|
||||
gitCmd.AddOptionFormat("--since=%s", opts.Since)
|
||||
}
|
||||
if opts.Until != "" {
|
||||
gitCmd.AddOptionFormat("--until=%s", opts.Until)
|
||||
}
|
||||
|
||||
gitCmd.AddDashesAndList(opts.File)
|
||||
err := gitCmd.WithDir(repo.Path).
|
||||
WithStdout(stdoutWriter).
|
||||
WithStderr(&stderr).
|
||||
Run(repo.Ctx)
|
||||
if err != nil {
|
||||
_ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
|
||||
} else {
|
||||
_ = stdoutWriter.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
objectFormat, err := repo.GetObjectFormat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if opts.Not != "" {
|
||||
gitCmd.AddOptionValues("--not", opts.Not)
|
||||
}
|
||||
if opts.Since != "" {
|
||||
gitCmd.AddOptionFormat("--since=%s", opts.Since)
|
||||
}
|
||||
if opts.Until != "" {
|
||||
gitCmd.AddOptionFormat("--until=%s", opts.Until)
|
||||
}
|
||||
gitCmd.AddDashesAndList(opts.File)
|
||||
|
||||
length := objectFormat.FullLength()
|
||||
commits := []*Commit{}
|
||||
shaline := make([]byte, length+1)
|
||||
for {
|
||||
n, err := io.ReadFull(stdoutReader, shaline)
|
||||
if err != nil || n < length {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
var commits []*Commit
|
||||
stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe()
|
||||
defer stdoutReaderClose()
|
||||
err := gitCmd.WithDir(repo.Path).
|
||||
WithPipelineFunc(func(context gitcmd.Context) error {
|
||||
objectFormat, err := repo.GetObjectFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return commits, err
|
||||
}
|
||||
objectID, err := NewIDFromString(string(shaline[0:length]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commit, err := repo.getCommit(objectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = append(commits, commit)
|
||||
}
|
||||
|
||||
length := objectFormat.FullLength()
|
||||
shaline := make([]byte, length+1)
|
||||
for {
|
||||
n, err := io.ReadFull(stdoutReader, shaline)
|
||||
if err != nil || n < length {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
objectID, err := NewIDFromString(string(shaline[0:length]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commit, err := repo.getCommit(objectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commits = append(commits, commit)
|
||||
}
|
||||
}).
|
||||
RunWithStderr(repo.Ctx)
|
||||
return commits, err
|
||||
}
|
||||
|
||||
// FilesCountBetween return the number of files changed between two commits
|
||||
|
||||
@ -67,16 +67,6 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
|
||||
return NewIDFromString(actualCommitID)
|
||||
}
|
||||
|
||||
// IsCommitExist returns true if given commit exists in current repository.
|
||||
func (repo *Repository) IsCommitExist(name string) bool {
|
||||
hash, err := repo.ConvertToGitID(name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = repo.gogitRepo.CommitObject(plumbing.Hash(hash.RawValue()))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (repo *Repository) getCommit(id ObjectID) (*Commit, error) {
|
||||
var tagObject *object.Tag
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
@ -37,50 +36,31 @@ func (repo *Repository) ResolveReference(name string) (string, error) {
|
||||
|
||||
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
||||
func (repo *Repository) GetRefCommitID(name string) (string, error) {
|
||||
wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx)
|
||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cancel()
|
||||
_, err = wr.Write([]byte(name + "\n"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
shaBs, _, _, err := ReadBatchLine(rd)
|
||||
info, err := batch.QueryInfo(name)
|
||||
if IsErrNotExist(err) {
|
||||
return "", ErrNotExist{name, ""}
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(shaBs), nil
|
||||
}
|
||||
|
||||
// IsCommitExist returns true if given commit exists in current repository.
|
||||
func (repo *Repository) IsCommitExist(name string) bool {
|
||||
if err := ensureValidGitRepository(repo.Ctx, repo.Path); err != nil {
|
||||
log.Error("IsCommitExist: %v", err)
|
||||
return false
|
||||
}
|
||||
_, _, err := gitcmd.NewCommand("cat-file", "-e").
|
||||
AddDynamicArguments(name).
|
||||
WithDir(repo.Path).
|
||||
RunStdString(repo.Ctx)
|
||||
return err == nil
|
||||
return info.ID, nil
|
||||
}
|
||||
|
||||
func (repo *Repository) getCommit(id ObjectID) (*Commit, error) {
|
||||
wr, rd, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
_, _ = wr.Write([]byte(id.String() + "\n"))
|
||||
|
||||
return repo.getCommitFromBatchReader(wr, rd, id)
|
||||
return repo.getCommitWithBatch(batch, id)
|
||||
}
|
||||
|
||||
func (repo *Repository) getCommitFromBatchReader(wr WriteCloserError, rd *bufio.Reader, id ObjectID) (*Commit, error) {
|
||||
_, typ, size, err := ReadBatchLine(rd)
|
||||
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()}
|
||||
@ -88,13 +68,13 @@ func (repo *Repository) getCommitFromBatchReader(wr WriteCloserError, rd *bufio.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch typ {
|
||||
switch info.Type {
|
||||
case "missing":
|
||||
return nil, ErrNotExist{ID: id.String()}
|
||||
case "tag":
|
||||
// then we need to parse the tag
|
||||
// and load the commit
|
||||
data, err := io.ReadAll(io.LimitReader(rd, size))
|
||||
data, err := io.ReadAll(io.LimitReader(rd, info.Size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -106,19 +86,9 @@ func (repo *Repository) getCommitFromBatchReader(wr WriteCloserError, rd *bufio.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := wr.Write([]byte(tag.Object.String() + "\n")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commit, err := repo.getCommitFromBatchReader(wr, rd, tag.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return commit, nil
|
||||
return repo.getCommitWithBatch(batch, tag.Object)
|
||||
case "commit":
|
||||
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, size))
|
||||
commit, err := CommitFromReader(repo, id, io.LimitReader(rd, info.Size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -129,8 +99,8 @@ func (repo *Repository) getCommitFromBatchReader(wr WriteCloserError, rd *bufio.
|
||||
|
||||
return commit, nil
|
||||
default:
|
||||
log.Debug("Unknown typ: %s", typ)
|
||||
if err := DiscardFull(rd, size+1); err != nil {
|
||||
log.Debug("Unknown cat-file object type: %s", info.Type)
|
||||
if err := DiscardFull(rd, info.Size+1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, ErrNotExist{
|
||||
@ -152,16 +122,12 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
|
||||
}
|
||||
}
|
||||
|
||||
wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx)
|
||||
batch, cancel, err := repo.CatFileBatch(repo.Ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
_, err = wr.Write([]byte(commitID + "\n"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sha, _, _, err := ReadBatchLine(rd)
|
||||
info, err := batch.QueryInfo(commitID)
|
||||
if err != nil {
|
||||
if IsErrNotExist(err) {
|
||||
return nil, ErrNotExist{commitID, ""}
|
||||
@ -169,5 +135,5 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return MustIDFromString(string(sha)), nil
|
||||
return MustIDFromString(info.ID), nil
|
||||
}
|
||||
|
||||
@ -18,32 +18,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
)
|
||||
|
||||
// GetMergeBase checks and returns merge base of two branches and the reference used as base.
|
||||
func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, string, error) {
|
||||
if tmpRemote == "" {
|
||||
tmpRemote = "origin"
|
||||
}
|
||||
|
||||
if tmpRemote != "origin" {
|
||||
tmpBaseName := RemotePrefix + tmpRemote + "/tmp_" + base
|
||||
// Fetch commit into a temporary branch in order to be able to handle commits and tags
|
||||
_, _, err := gitcmd.NewCommand("fetch", "--no-tags").
|
||||
AddDynamicArguments(tmpRemote).
|
||||
AddDashesAndList(base + ":" + tmpBaseName).
|
||||
WithDir(repo.Path).
|
||||
RunStdString(repo.Ctx)
|
||||
if err == nil {
|
||||
base = tmpBaseName
|
||||
}
|
||||
}
|
||||
|
||||
stdout, _, err := gitcmd.NewCommand("merge-base").
|
||||
AddDashesAndList(base, head).
|
||||
WithDir(repo.Path).
|
||||
RunStdString(repo.Ctx)
|
||||
return strings.TrimSpace(stdout), base, err
|
||||
}
|
||||
|
||||
type lineCountWriter struct {
|
||||
numLines int
|
||||
}
|
||||
@ -60,7 +34,6 @@ func (l *lineCountWriter) Write(p []byte) (n int, err error) {
|
||||
func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparison bool) (int, error) {
|
||||
// Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly
|
||||
w := &lineCountWriter{}
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
separator := "..."
|
||||
if directComparison {
|
||||
@ -72,25 +45,22 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis
|
||||
AddDynamicArguments(base + separator + head).
|
||||
AddArguments("--").
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx); err != nil {
|
||||
if strings.Contains(stderr.String(), "no merge base") {
|
||||
WithStdoutCopy(w).
|
||||
RunWithStderr(repo.Ctx); err != nil {
|
||||
if strings.Contains(err.Stderr(), "no merge base") {
|
||||
// git >= 2.28 now returns an error if base and head have become unrelated.
|
||||
// previously it would return the results of git diff -z --name-only base head so let's try that...
|
||||
w = &lineCountWriter{}
|
||||
stderr.Reset()
|
||||
if err = gitcmd.NewCommand("diff", "-z", "--name-only").
|
||||
AddDynamicArguments(base, head).
|
||||
AddArguments("--").
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx); err == nil {
|
||||
WithStdoutCopy(w).
|
||||
RunWithStderr(repo.Ctx); err == nil {
|
||||
return w.numLines, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("%w: Stderr: %s", err, stderr)
|
||||
return 0, err
|
||||
}
|
||||
return w.numLines, nil
|
||||
}
|
||||
@ -99,11 +69,9 @@ var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`)
|
||||
|
||||
// GetDiff generates and returns patch data between given revisions, optimized for human readability
|
||||
func (repo *Repository) GetDiff(compareArg string, w io.Writer) error {
|
||||
stderr := new(bytes.Buffer)
|
||||
return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg).
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStderr(stderr).
|
||||
WithStdoutCopy(w).
|
||||
Run(repo.Ctx)
|
||||
}
|
||||
|
||||
@ -112,17 +80,15 @@ func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error {
|
||||
return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram").
|
||||
AddDynamicArguments(compareArg).
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStdoutCopy(w).
|
||||
Run(repo.Ctx)
|
||||
}
|
||||
|
||||
// GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply`
|
||||
func (repo *Repository) GetPatch(compareArg string, w io.Writer) error {
|
||||
stderr := new(bytes.Buffer)
|
||||
return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg).
|
||||
WithDir(repo.Path).
|
||||
WithStdout(w).
|
||||
WithStderr(stderr).
|
||||
WithStdoutCopy(w).
|
||||
Run(repo.Ctx)
|
||||
}
|
||||
|
||||
|
||||
@ -101,21 +101,17 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
|
||||
return err
|
||||
}
|
||||
cmd := gitcmd.NewCommand("update-index", "--remove", "-z", "--index-info")
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
buffer := new(bytes.Buffer)
|
||||
input := new(bytes.Buffer)
|
||||
for _, file := range filenames {
|
||||
if file != "" {
|
||||
// using format: mode SP type SP sha1 TAB path
|
||||
buffer.WriteString("0 blob " + objectFormat.EmptyObjectID().String() + "\t" + file + "\000")
|
||||
input.WriteString("0 blob " + objectFormat.EmptyObjectID().String() + "\t" + file + "\000")
|
||||
}
|
||||
}
|
||||
return cmd.
|
||||
WithDir(repo.Path).
|
||||
WithStdin(bytes.NewReader(buffer.Bytes())).
|
||||
WithStdout(stdout).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx)
|
||||
WithStdinBytes(input.Bytes()).
|
||||
RunWithStderr(repo.Ctx)
|
||||
}
|
||||
|
||||
type IndexObjectInfo struct {
|
||||
@ -127,19 +123,15 @@ type IndexObjectInfo struct {
|
||||
// AddObjectsToIndex adds the provided object hashes to the index at the provided filenames
|
||||
func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error {
|
||||
cmd := gitcmd.NewCommand("update-index", "--add", "--replace", "-z", "--index-info")
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
buffer := new(bytes.Buffer)
|
||||
input := new(bytes.Buffer)
|
||||
for _, object := range objects {
|
||||
// using format: mode SP type SP sha1 TAB path
|
||||
buffer.WriteString(object.Mode + " blob " + object.Object.String() + "\t" + object.Filename + "\000")
|
||||
input.WriteString(object.Mode + " blob " + object.Object.String() + "\t" + object.Filename + "\000")
|
||||
}
|
||||
return cmd.
|
||||
WithDir(repo.Path).
|
||||
WithStdin(bytes.NewReader(buffer.Bytes())).
|
||||
WithStdout(stdout).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx)
|
||||
WithStdinBytes(input.Bytes()).
|
||||
RunWithStderr(repo.Ctx)
|
||||
}
|
||||
|
||||
// AddObjectToIndex adds the provided object hash to the index at the provided filename
|
||||
|
||||
@ -5,8 +5,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
@ -33,18 +31,12 @@ func (o ObjectType) Bytes() []byte {
|
||||
return []byte(o)
|
||||
}
|
||||
|
||||
type EmptyReader struct{}
|
||||
|
||||
func (EmptyReader) Read(p []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (repo *Repository) GetObjectFormat() (ObjectFormat, error) {
|
||||
if repo != nil && repo.objectFormat != nil {
|
||||
return repo.objectFormat, nil
|
||||
}
|
||||
|
||||
str, err := repo.hashObject(EmptyReader{}, false)
|
||||
str, err := repo.hashObjectBytes(nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -58,32 +50,28 @@ func (repo *Repository) GetObjectFormat() (ObjectFormat, error) {
|
||||
return repo.objectFormat, nil
|
||||
}
|
||||
|
||||
// HashObject takes a reader and returns hash for that reader
|
||||
func (repo *Repository) HashObject(reader io.Reader) (ObjectID, error) {
|
||||
idStr, err := repo.hashObject(reader, true)
|
||||
// HashObjectBytes returns hash for the content
|
||||
func (repo *Repository) HashObjectBytes(buf []byte) (ObjectID, error) {
|
||||
idStr, err := repo.hashObjectBytes(buf, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewIDFromString(idStr)
|
||||
}
|
||||
|
||||
func (repo *Repository) hashObject(reader io.Reader, save bool) (string, error) {
|
||||
func (repo *Repository) hashObjectBytes(buf []byte, save bool) (string, error) {
|
||||
var cmd *gitcmd.Command
|
||||
if save {
|
||||
cmd = gitcmd.NewCommand("hash-object", "-w", "--stdin")
|
||||
} else {
|
||||
cmd = gitcmd.NewCommand("hash-object", "--stdin")
|
||||
}
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
err := cmd.
|
||||
stdout, _, err := cmd.
|
||||
WithDir(repo.Path).
|
||||
WithStdin(reader).
|
||||
WithStdout(stdout).
|
||||
WithStderr(stderr).
|
||||
Run(repo.Ctx)
|
||||
WithStdinBytes(buf).
|
||||
RunStdString(repo.Ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
return strings.TrimSpace(stdout), nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user