0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-13 19:45:47 +02:00

Merge branch 'main' into fix-24635

This commit is contained in:
Excellencedev 2025-12-18 02:39:42 +01:00 committed by GitHub
commit e20d12e8a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1359 additions and 986 deletions

View File

@ -111,9 +111,6 @@ linters:
- require-error - require-error
usetesting: usetesting:
os-temp-dir: true os-temp-dir: true
modernize:
disable:
- stringsbuilder
perfsprint: perfsprint:
concat-loop: false concat-loop: false
govet: govet:

View File

@ -32,7 +32,7 @@ XGO_VERSION := go-1.25.x
AIR_PACKAGE ?= github.com/air-verse/air@v1 AIR_PACKAGE ?= github.com/air-verse/air@v1
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.0 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
@ -339,12 +339,12 @@ lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backen
.PHONY: lint-js .PHONY: lint-js
lint-js: node_modules ## lint js files lint-js: node_modules ## lint js files
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES) $(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES)
$(NODE_VARS) pnpm exec vue-tsc $(NODE_VARS) pnpm exec vue-tsc
.PHONY: lint-js-fix .PHONY: lint-js-fix
lint-js-fix: node_modules ## lint js files and fix issues lint-js-fix: node_modules ## lint js files and fix issues
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES) --fix $(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES) --fix
$(NODE_VARS) pnpm exec vue-tsc $(NODE_VARS) pnpm exec vue-tsc
.PHONY: lint-css .PHONY: lint-css

View File

@ -215,7 +215,7 @@ export default defineConfig([
'@typescript-eslint/no-unnecessary-condition': [0], '@typescript-eslint/no-unnecessary-condition': [0],
'@typescript-eslint/no-unnecessary-qualifier': [0], '@typescript-eslint/no-unnecessary-qualifier': [0],
'@typescript-eslint/no-unnecessary-template-expression': [0], '@typescript-eslint/no-unnecessary-template-expression': [0],
'@typescript-eslint/no-unnecessary-type-arguments': [0], '@typescript-eslint/no-unnecessary-type-arguments': [2],
'@typescript-eslint/no-unnecessary-type-assertion': [2], '@typescript-eslint/no-unnecessary-type-assertion': [2],
'@typescript-eslint/no-unnecessary-type-constraint': [2], '@typescript-eslint/no-unnecessary-type-constraint': [2],
'@typescript-eslint/no-unnecessary-type-conversion': [2], '@typescript-eslint/no-unnecessary-type-conversion': [2],
@ -228,11 +228,12 @@ export default defineConfig([
'@typescript-eslint/no-unsafe-member-access': [0], '@typescript-eslint/no-unsafe-member-access': [0],
'@typescript-eslint/no-unsafe-return': [0], '@typescript-eslint/no-unsafe-return': [0],
'@typescript-eslint/no-unsafe-unary-minus': [2], '@typescript-eslint/no-unsafe-unary-minus': [2],
'@typescript-eslint/no-unused-expressions': [0], '@typescript-eslint/no-unused-expressions': [2],
'@typescript-eslint/no-unused-private-class-members': [2], '@typescript-eslint/no-unused-private-class-members': [2],
'@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}], '@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-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-constructor': [0],
'@typescript-eslint/no-useless-default-assignment': [0], // https://github.com/typescript-eslint/typescript-eslint/issues/11847
'@typescript-eslint/no-useless-empty-export': [0], '@typescript-eslint/no-useless-empty-export': [0],
'@typescript-eslint/no-wrapper-object-types': [2], '@typescript-eslint/no-wrapper-object-types': [2],
'@typescript-eslint/non-nullable-type-assertion-style': [0], '@typescript-eslint/non-nullable-type-assertion-style': [0],
@ -584,7 +585,7 @@ export default defineConfig([
'no-unreachable': [2], 'no-unreachable': [2],
'no-unsafe-finally': [2], 'no-unsafe-finally': [2],
'no-unsafe-negation': [2], 'no-unsafe-negation': [2],
'no-unused-expressions': [2], 'no-unused-expressions': [0], // handled by @typescript-eslint/no-unused-expressions
'no-unused-labels': [2], 'no-unused-labels': [2],
'no-unused-private-class-members': [0], // handled by @typescript-eslint/no-unused-private-class-members 'no-unused-private-class-members': [0], // handled by @typescript-eslint/no-unused-private-class-members
'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars 'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"slices" "slices"
"strings"
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -169,7 +170,8 @@ func (p *Permission) ReadableUnitTypes() []unit.Type {
} }
func (p *Permission) LogString() string { func (p *Permission) LogString() string {
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [" var format strings.Builder
format.WriteString("<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [")
args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)} args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)}
for i, u := range p.units { for i, u := range p.units {
@ -181,19 +183,19 @@ func (p *Permission) LogString() string {
config = err.Error() config = err.Error()
} }
} }
format += "\n\tunits[%d]: ID=%d RepoID=%d Type=%s Config=%s" format.WriteString("\n\tunits[%d]: ID=%d RepoID=%d Type=%s Config=%s")
args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config) args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config)
} }
for key, value := range p.unitsMode { for key, value := range p.unitsMode {
format += "\n\tunitsMode[%-v]: %-v" format.WriteString("\n\tunitsMode[%-v]: %-v")
args = append(args, key.LogString(), value.LogString()) args = append(args, key.LogString(), value.LogString())
} }
format += "\n\tanonymousAccessMode: %-v" format.WriteString("\n\tanonymousAccessMode: %-v")
args = append(args, p.anonymousAccessMode) args = append(args, p.anonymousAccessMode)
format += "\n\teveryoneAccessMode: %-v" format.WriteString("\n\teveryoneAccessMode: %-v")
args = append(args, p.everyoneAccessMode) args = append(args, p.everyoneAccessMode)
format += "\n\t]>" format.WriteString("\n\t]>")
return fmt.Sprintf(format, args...) return fmt.Sprintf(format.String(), args...)
} }
func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) { func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {

View File

@ -1461,3 +1461,15 @@ func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) {
} }
return id, nil return id, nil
} }
// GetUserOrOrgByName returns the user or org by name
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
var u User
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist{Name: name}
}
return &u, nil
}

View File

@ -75,9 +75,9 @@ func (f Format) Parser(r io.Reader) *Parser {
// hexEscaped produces hex-escpaed characters from a string. For example, "\n\0" // hexEscaped produces hex-escpaed characters from a string. For example, "\n\0"
// would turn into "%0a%00". // would turn into "%0a%00".
func (f Format) hexEscaped(delim []byte) string { func (f Format) hexEscaped(delim []byte) string {
escaped := "" var escaped strings.Builder
for i := range delim { for i := range delim {
escaped += "%" + hex.EncodeToString([]byte{delim[i]}) escaped.WriteString("%" + hex.EncodeToString([]byte{delim[i]}))
} }
return escaped return escaped.String()
} }

View File

@ -14,6 +14,7 @@ type Uploader interface {
CreateMilestones(ctx context.Context, milestones ...*Milestone) error CreateMilestones(ctx context.Context, milestones ...*Milestone) error
CreateReleases(ctx context.Context, releases ...*Release) error CreateReleases(ctx context.Context, releases ...*Release) error
SyncTags(ctx context.Context) error SyncTags(ctx context.Context) error
SyncBranches(ctx context.Context) error
CreateLabels(ctx context.Context, labels ...*Label) error CreateLabels(ctx context.Context, labels ...*Label) error
CreateIssues(ctx context.Context, issues ...*Issue) error CreateIssues(ctx context.Context, issues ...*Issue) error
CreateComments(ctx context.Context, comments ...*Comment) error CreateComments(ctx context.Context, comments ...*Comment) error

View File

@ -4,6 +4,7 @@
package setting package setting
import ( import (
"strings"
"sync" "sync"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -23,11 +24,11 @@ type OpenWithEditorApp struct {
type OpenWithEditorAppsType []OpenWithEditorApp type OpenWithEditorAppsType []OpenWithEditorApp
func (t OpenWithEditorAppsType) ToTextareaString() string { func (t OpenWithEditorAppsType) ToTextareaString() string {
ret := "" var ret strings.Builder
for _, app := range t { for _, app := range t {
ret += app.DisplayName + " = " + app.OpenURL + "\n" ret.WriteString(app.DisplayName + " = " + app.OpenURL + "\n")
} }
return ret return ret.String()
} }
func DefaultOpenWithEditorApps() OpenWithEditorAppsType { func DefaultOpenWithEditorApps() OpenWithEditorAppsType {

View File

@ -249,17 +249,18 @@ func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:rev
func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML { func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML {
isPullRequest := issue != nil && issue.IsPull isPullRequest := issue != nil && issue.IsPull
baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues")) baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues"))
htmlCode := `<span class="labels-list">` var htmlCode strings.Builder
htmlCode.WriteString(`<span class="labels-list">`)
for _, label := range labels { for _, label := range labels {
// Protect against nil value in labels - shouldn't happen but would cause a panic if so // Protect against nil value in labels - shouldn't happen but would cause a panic if so
if label == nil { if label == nil {
continue continue
} }
link := fmt.Sprintf("%s?labels=%d", baseLink, label.ID) link := fmt.Sprintf("%s?labels=%d", baseLink, label.ID)
htmlCode += string(ut.RenderLabelWithLink(label, template.URL(link))) htmlCode.WriteString(string(ut.RenderLabelWithLink(label, template.URL(link))))
} }
htmlCode += "</span>" htmlCode.WriteString("</span>")
return template.HTML(htmlCode) return template.HTML(htmlCode.String())
} }
func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize int) template.HTML { func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize int) template.HTML {

View File

@ -1863,6 +1863,7 @@ pulls.desc = Enable pull requests and code reviews.
pulls.new = New Pull Request pulls.new = New Pull Request
pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner. pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
pulls.new.must_collaborator = You must be a collaborator to create pull request. pulls.new.must_collaborator = You must be a collaborator to create pull request.
pulls.new.already_existed = A pull request between these branches already exists
pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes. pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.
pulls.view = View Pull Request pulls.view = View Pull Request
pulls.compare_changes = New Pull Request pulls.compare_changes = New Pull Request

View File

@ -1,6 +1,6 @@
{ {
"type": "module", "type": "module",
"packageManager": "pnpm@10.24.0", "packageManager": "pnpm@10.26.0",
"engines": { "engines": {
"node": ">= 22.6.0", "node": ">= 22.6.0",
"pnpm": ">= 10.0.0" "pnpm": ">= 10.0.0"
@ -12,7 +12,7 @@
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3", "@github/markdown-toolbar-element": "2.2.3",
"@github/paste-markdown": "1.5.3", "@github/paste-markdown": "1.5.3",
"@github/relative-time-element": "4.5.1", "@github/relative-time-element": "5.0.0",
"@github/text-expander-element": "2.9.2", "@github/text-expander-element": "2.9.2",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.21.1", "@primer/octicons": "19.21.1",
@ -21,7 +21,7 @@
"@techknowlogick/license-checker-webpack-plugin": "0.3.0", "@techknowlogick/license-checker-webpack-plugin": "0.3.0",
"add-asset-webpack-plugin": "3.1.1", "add-asset-webpack-plugin": "3.1.1",
"ansi_up": "6.0.6", "ansi_up": "6.0.6",
"asciinema-player": "3.12.1", "asciinema-player": "3.13.5",
"chart.js": "4.5.1", "chart.js": "4.5.1",
"chartjs-adapter-dayjs-4": "1.0.4", "chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.2.0", "chartjs-plugin-zoom": "2.2.0",
@ -36,32 +36,31 @@
"htmx.org": "2.0.8", "htmx.org": "2.0.8",
"idiomorph": "0.7.4", "idiomorph": "0.7.4",
"jquery": "3.7.1", "jquery": "3.7.1",
"katex": "0.16.25", "katex": "0.16.27",
"mermaid": "11.12.2", "mermaid": "11.12.2",
"mini-css-extract-plugin": "2.9.4", "mini-css-extract-plugin": "2.9.4",
"monaco-editor": "0.55.1", "monaco-editor": "0.55.1",
"monaco-editor-webpack-plugin": "7.1.1", "monaco-editor-webpack-plugin": "7.1.1",
"online-3d-viewer": "0.16.0", "online-3d-viewer": "0.17.0",
"pdfobject": "2.3.1", "pdfobject": "2.3.1",
"perfect-debounce": "2.0.0", "perfect-debounce": "2.0.0",
"postcss": "8.5.6", "postcss": "8.5.6",
"postcss-loader": "8.2.0", "postcss-loader": "8.2.0",
"sortablejs": "1.15.6", "sortablejs": "1.15.6",
"swagger-ui-dist": "5.30.3", "swagger-ui-dist": "5.31.0",
"tailwindcss": "3.4.17", "tailwindcss": "3.4.17",
"throttle-debounce": "5.0.2", "throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tippy.js": "6.3.7", "tippy.js": "6.3.7",
"toastify-js": "1.12.0", "toastify-js": "1.12.0",
"tributejs": "5.1.3", "tributejs": "5.1.3",
"typescript": "5.9.3",
"uint8-to-base64": "0.2.1", "uint8-to-base64": "0.2.1",
"vanilla-colorful": "0.7.2", "vanilla-colorful": "0.7.2",
"vue": "3.5.25", "vue": "3.5.25",
"vue-bar-graph": "2.2.0", "vue-bar-graph": "2.2.0",
"vue-chartjs": "5.3.3", "vue-chartjs": "5.3.3",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"webpack": "5.103.0", "webpack": "5.104.0",
"webpack-cli": "6.0.1", "webpack-cli": "6.0.1",
"wrap-ansi": "9.0.2" "wrap-ansi": "9.0.2"
}, },
@ -80,10 +79,10 @@
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.4", "@types/toastify-js": "1.12.4",
"@typescript-eslint/parser": "8.48.1", "@typescript-eslint/parser": "8.50.0",
"@vitejs/plugin-vue": "6.0.2", "@vitejs/plugin-vue": "6.0.3",
"@vitest/eslint-plugin": "1.5.1", "@vitest/eslint-plugin": "1.5.2",
"eslint": "9.39.1", "eslint": "9.39.2",
"eslint-import-resolver-typescript": "4.4.4", "eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-array-func": "5.1.0", "eslint-plugin-array-func": "5.1.0",
"eslint-plugin-github": "6.0.0", "eslint-plugin-github": "6.0.0",
@ -97,7 +96,8 @@
"eslint-plugin-wc": "3.0.2", "eslint-plugin-wc": "3.0.2",
"globals": "16.5.0", "globals": "16.5.0",
"happy-dom": "20.0.11", "happy-dom": "20.0.11",
"markdownlint-cli": "0.46.0", "jiti": "2.6.1",
"markdownlint-cli": "0.47.0",
"material-icon-theme": "5.29.0", "material-icon-theme": "5.29.0",
"nolyfill": "1.0.44", "nolyfill": "1.0.44",
"postcss-html": "1.8.0", "postcss-html": "1.8.0",
@ -108,11 +108,12 @@
"stylelint-declaration-strict-value": "1.10.11", "stylelint-declaration-strict-value": "1.10.11",
"stylelint-value-no-unknown-custom-properties": "6.0.1", "stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "4.0.0", "svgo": "4.0.0",
"typescript-eslint": "8.48.1", "typescript": "5.9.3",
"updates": "17.0.4", "typescript-eslint": "8.50.0",
"updates": "17.0.7",
"vite-string-plugin": "1.4.9", "vite-string-plugin": "1.4.9",
"vitest": "4.0.15", "vitest": "4.0.16",
"vue-tsc": "3.1.5" "vue-tsc": "3.1.8"
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"

1345
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -433,15 +433,16 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model.
} }
func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) { func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) {
ret := "---\n" var ret strings.Builder
ret.WriteString("---\n")
for _, v := range versions { for _, v := range versions {
dep, err := makePackageVersionDependency(ctx, v, c) dep, err := makePackageVersionDependency(ctx, v, c)
if err != nil { if err != nil {
return "", err return "", err
} }
ret += dep + "\n" ret.WriteString(dep + "\n")
} }
return ret, nil return ret.String(), nil
} }
func makeGemFullFileName(gemName, version, platform string) string { func makeGemFullFileName(gemName, version, platform string) string {

View File

@ -30,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@ -1082,7 +1083,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} else if len(headInfos) == 2 { } else if len(headInfos) == 2 {
// There is a head repository (the head repository could also be the same base repo) // There is a head repository (the head repository could also be the same base repo)
headRefToGuess = headInfos[1] headRefToGuess = headInfos[1]
headUser, err = user_model.GetUserByName(ctx, headInfos[0]) headUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound("GetUserByName") ctx.APIErrorNotFound("GetUserByName")
@ -1098,28 +1099,23 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
isSameRepo := ctx.Repo.Owner.ID == headUser.ID isSameRepo := ctx.Repo.Owner.ID == headUser.ID
// Check if current user has fork of repository or in the same repository. var headRepo *repo_model.Repository
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) if isSameRepo {
if headRepo == nil && !isSameRepo { headRepo = baseRepo
err = baseRepo.GetBaseRepo(ctx) } else {
headRepo, err = common.FindHeadRepo(ctx, baseRepo, headUser.ID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return nil, nil return nil, nil
} }
if headRepo == nil {
// Check if baseRepo's base repository is the same as headUser's repository. ctx.APIErrorNotFound("head repository not found")
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
ctx.APIErrorNotFound("GetBaseRepo")
return nil, nil return nil, nil
} }
// Assign headRepo so it can be used below.
headRepo = baseRepo.BaseRepo
} }
var headGitRepo *git.Repository var headGitRepo *git.Repository
if isSameRepo { if isSameRepo {
headRepo = ctx.Repo.Repository
headGitRepo = ctx.Repo.GitRepo headGitRepo = ctx.Repo.GitRepo
closer = func() {} // no need to close the head repo because it shares the base repo closer = func() {} // no need to close the head repo because it shares the base repo
} else { } else {
@ -1143,7 +1139,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil return nil, nil
} }
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) { if !permBase.CanRead(unit.TypeCode) {
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase) log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode") ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
return nil, nil return nil, nil

View File

@ -4,6 +4,8 @@
package common package common
import ( import (
"context"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@ -20,3 +22,54 @@ type CompareInfo struct {
HeadBranch string HeadBranch string
DirectComparison bool DirectComparison bool
} }
// maxForkTraverseLevel defines the maximum levels to traverse when searching for the head repository.
const maxForkTraverseLevel = 10
// FindHeadRepo tries to find the head repository based on the base repository and head user ID.
func FindHeadRepo(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64) (*repo_model.Repository, error) {
if baseRepo.IsFork {
curRepo := baseRepo
for curRepo.OwnerID != headUserID { // We assume the fork deepth is not too deep.
if err := curRepo.GetBaseRepo(ctx); err != nil {
return nil, err
}
if curRepo.BaseRepo == nil {
return findHeadRepoFromRootBase(ctx, curRepo, headUserID, maxForkTraverseLevel)
}
curRepo = curRepo.BaseRepo
}
return curRepo, nil
}
return findHeadRepoFromRootBase(ctx, baseRepo, headUserID, maxForkTraverseLevel)
}
func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) {
if traverseLevel == 0 {
return nil, nil
}
// test if we are lucky
repo, err := repo_model.GetUserFork(ctx, baseRepo.ID, headUserID)
if err != nil {
return nil, err
}
if repo != nil {
return repo, nil
}
firstLevelForkedRepos, err := repo_model.GetRepositoriesByForkID(ctx, baseRepo.ID)
if err != nil {
return nil, err
}
for _, repo := range firstLevelForkedRepos {
forked, err := findHeadRepoFromRootBase(ctx, repo, headUserID, traverseLevel-1)
if err != nil {
return nil, err
}
if forked != nil {
return forked, nil
}
}
return nil, nil
}

View File

@ -179,11 +179,11 @@ func AuthorizeOAuth(ctx *context.Context) {
errs := binding.Errors{} errs := binding.Errors{}
errs = form.Validate(ctx.Req, errs) errs = form.Validate(ctx.Req, errs)
if len(errs) > 0 { if len(errs) > 0 {
errstring := "" var errstring strings.Builder
for _, e := range errs { for _, e := range errs {
errstring += e.Error() + "\n" errstring.WriteString(e.Error() + "\n")
} }
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring)) ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring.String()))
return return
} }

View File

@ -259,7 +259,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
} else if len(headInfos) == 2 { } else if len(headInfos) == 2 {
headInfosSplit := strings.Split(headInfos[0], "/") headInfosSplit := strings.Split(headInfos[0], "/")
if len(headInfosSplit) == 1 { if len(headInfosSplit) == 1 {
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0]) ci.HeadUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
ctx.NotFound(nil) ctx.NotFound(nil)

View File

@ -495,7 +495,7 @@ func preparePullViewSigning(ctx *context.Context, issue *issues_model.Issue) {
pull := issue.PullRequest pull := issue.PullRequest
ctx.Data["WillSign"] = false ctx.Data["WillSign"] = false
if ctx.Doer != nil { if ctx.Doer != nil {
sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitHeadRefName()) sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo, pull.BaseBranch, pull.GetGitHeadRefName())
ctx.Data["WillSign"] = sign ctx.Data["WillSign"] = sign
ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key) ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key)
if err != nil { if err != nil {

View File

@ -1340,6 +1340,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return return
} }
// Check if a pull request already exists with the same head and base branch.
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err)
return
}
if pr != nil {
ctx.JSONError(ctx.Tr("repo.pulls.new.already_existed"))
return
}
content := form.Content content := form.Content
if filename := ctx.Req.Form.Get("template-file"); filename != "" { if filename := ctx.Req.Form.Get("template-file"); filename != "" {
if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil { if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {

View File

@ -169,7 +169,7 @@ Loop:
} }
// SignWikiCommit determines if we should sign the commits to this repository wiki // SignWikiCommit determines if we should sign the commits to this repository wiki
func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) { func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.Wiki) rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
signingKey, sig := gitrepo.GetSigningKey(ctx) signingKey, sig := gitrepo.GetSigningKey(ctx)
if signingKey == nil { if signingKey == nil {
@ -200,11 +200,6 @@ Loop:
return false, nil, nil, &ErrWontSign{twofa} return false, nil, nil, &ErrWontSign{twofa}
} }
case parentSigned: case parentSigned:
gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
commit, err := gitRepo.GetCommit("HEAD") commit, err := gitRepo.GetCommit("HEAD")
if err != nil { if err != nil {
return false, nil, nil, err return false, nil, nil, err
@ -222,7 +217,7 @@ Loop:
} }
// SignCRUDAction determines if we should sign a CRUD commit to this repository // SignCRUDAction determines if we should sign a CRUD commit to this repository
func SignCRUDAction(ctx context.Context, u *user_model.User, tmpBasePath, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) { func SignCRUDAction(ctx context.Context, u *user_model.User, gitRepo *git.Repository, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
signingKey, sig := git.GetSigningKey(ctx) signingKey, sig := git.GetSigningKey(ctx)
if signingKey == nil { if signingKey == nil {
@ -253,11 +248,6 @@ Loop:
return false, nil, nil, &ErrWontSign{twofa} return false, nil, nil, &ErrWontSign{twofa}
} }
case parentSigned: case parentSigned:
gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
isEmpty, err := gitRepo.IsEmpty() isEmpty, err := gitRepo.IsEmpty()
if err != nil { if err != nil {
return false, nil, nil, err return false, nil, nil, err
@ -281,7 +271,7 @@ Loop:
} }
// SignMerge determines if we should sign a PR merge commit to the base repository // SignMerge determines if we should sign a PR merge commit to the base repository
func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, *git.SigningKey, *git.Signature, error) { func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, gitRepo *git.Repository, baseCommit, headCommit string) (bool, *git.SigningKey, *git.Signature, error) {
if err := pr.LoadBaseRepo(ctx); err != nil { if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to get Base Repo for pull request") log.Error("Unable to get Base Repo for pull request")
return false, nil, nil, err return false, nil, nil, err
@ -294,9 +284,6 @@ func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.
} }
rules := signingModeFromStrings(setting.Repository.Signing.Merges) rules := signingModeFromStrings(setting.Repository.Signing.Merges)
var gitRepo *git.Repository
var err error
Loop: Loop:
for _, rule := range rules { for _, rule := range rules {
switch rule { switch rule {
@ -332,13 +319,6 @@ Loop:
return false, nil, nil, &ErrWontSign{approved} return false, nil, nil, &ErrWontSign{approved}
} }
case baseSigned: case baseSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(baseCommit) commit, err := gitRepo.GetCommit(baseCommit)
if err != nil { if err != nil {
return false, nil, nil, err return false, nil, nil, err
@ -348,13 +328,6 @@ Loop:
return false, nil, nil, &ErrWontSign{baseSigned} return false, nil, nil, &ErrWontSign{baseSigned}
} }
case headSigned: case headSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit) commit, err := gitRepo.GetCommit(headCommit)
if err != nil { if err != nil {
return false, nil, nil, err return false, nil, nil, err
@ -364,13 +337,6 @@ Loop:
return false, nil, nil, &ErrWontSign{headSigned} return false, nil, nil, &ErrWontSign{headSigned}
} }
case commitsSigned: case commitsSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit) commit, err := gitRepo.GetCommit(headCommit)
if err != nil { if err != nil {
return false, nil, nil, err return false, nil, nil, err

View File

@ -140,7 +140,13 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r
protectionRequireSigned = protectedBranch.RequireSignedCommits protectionRequireSigned = protectedBranch.RequireSignedCommits
} }
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, doer, targetRepo.RepoPath(), refName.String()) targetGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, targetRepo)
if err != nil {
return nil, err
}
defer closer.Close()
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, doer, targetGitRepo, refName.String())
wontSignReason := "" wontSignReason := ""
if asymkey_service.IsErrWontSign(err) { if asymkey_service.IsErrWontSign(err) {
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)

View File

@ -358,6 +358,11 @@ func (g *RepositoryDumper) SyncTags(ctx context.Context) error {
return nil return nil
} }
// SyncBranches syncs branches in the database
func (g *RepositoryDumper) SyncBranches(ctx context.Context) error {
return nil
}
// CreateIssues creates issues // CreateIssues creates issues
func (g *RepositoryDumper) CreateIssues(_ context.Context, issues ...*base.Issue) error { func (g *RepositoryDumper) CreateIssues(_ context.Context, issues ...*base.Issue) error {
var err error var err error

View File

@ -368,6 +368,11 @@ func (g *GiteaLocalUploader) SyncTags(ctx context.Context) error {
return repo_module.SyncReleasesWithTags(ctx, g.repo, g.gitRepo) return repo_module.SyncReleasesWithTags(ctx, g.repo, g.gitRepo)
} }
func (g *GiteaLocalUploader) SyncBranches(ctx context.Context) error {
_, err := repo_module.SyncRepoBranchesWithRepo(ctx, g.repo, g.gitRepo, g.doer.ID)
return err
}
// CreateIssues creates issues // CreateIssues creates issues
func (g *GiteaLocalUploader) CreateIssues(ctx context.Context, issues ...*base.Issue) error { func (g *GiteaLocalUploader) CreateIssues(ctx context.Context, issues ...*base.Issue) error {
iss := make([]*issues_model.Issue, 0, len(issues)) iss := make([]*issues_model.Issue, 0, len(issues))

View File

@ -5,9 +5,6 @@
package migrations package migrations
import ( import (
"fmt"
"os"
"path/filepath"
"strconv" "strconv"
"testing" "testing"
"time" "time"
@ -17,15 +14,10 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration" base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -228,297 +220,3 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, linkedUser.ID, target.GetUserID()) assert.Equal(t, linkedUser.ID, target.GetUserID())
} }
func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
unittest.PrepareTestEnv(t)
//
// fromRepo master
//
fromRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
baseRef := "master"
// this is very different from the real situation. It should be a bare repository for all the Gitea managed repositories
assert.NoError(t, git.InitRepository(t.Context(), fromRepo.RepoPath(), false, fromRepo.ObjectFormatName))
err := gitrepo.RunCmd(t.Context(), fromRepo, gitcmd.NewCommand("symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+baseRef))
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte("# Testing Repository\n\nOriginally created in: "+fromRepo.RepoPath()), 0o644))
assert.NoError(t, git.AddChanges(t.Context(), fromRepo.RepoPath(), true))
signature := git.Signature{
Email: "test@example.com",
Name: "test",
When: time.Now(),
}
assert.NoError(t, git.CommitChanges(t.Context(), fromRepo.RepoPath(), git.CommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "Initial Commit",
}))
fromGitRepo, err := gitrepo.OpenRepository(t.Context(), fromRepo)
assert.NoError(t, err)
defer fromGitRepo.Close()
baseSHA, err := fromGitRepo.GetBranchCommitID(baseRef)
assert.NoError(t, err)
//
// fromRepo branch1
//
headRef := "branch1"
_, err = gitrepo.RunCmdString(t.Context(), fromRepo, gitcmd.NewCommand("checkout", "-b").AddDynamicArguments(headRef))
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte("SOMETHING"), 0o644))
assert.NoError(t, git.AddChanges(t.Context(), fromRepo.RepoPath(), true))
signature.When = time.Now()
assert.NoError(t, git.CommitChanges(t.Context(), fromRepo.RepoPath(), git.CommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "Pull request",
}))
assert.NoError(t, err)
headSHA, err := fromGitRepo.GetBranchCommitID(headRef)
assert.NoError(t, err)
fromRepoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: fromRepo.OwnerID})
//
// forkRepo branch2
//
forkHeadRef := "branch2"
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8})
assert.NoError(t, git.Clone(t.Context(), fromRepo.RepoPath(), forkRepo.RepoPath(), git.CloneRepoOptions{
Branch: headRef,
}))
_, err = gitrepo.RunCmdString(t.Context(), forkRepo, gitcmd.NewCommand("checkout", "-b").AddDynamicArguments(forkHeadRef))
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(forkRepo.RepoPath(), "README.md"), []byte("# branch2 "+forkRepo.RepoPath()), 0o644))
assert.NoError(t, git.AddChanges(t.Context(), forkRepo.RepoPath(), true))
assert.NoError(t, git.CommitChanges(t.Context(), forkRepo.RepoPath(), git.CommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "branch2 commit",
}))
forkGitRepo, err := gitrepo.OpenRepository(t.Context(), forkRepo)
assert.NoError(t, err)
defer forkGitRepo.Close()
forkHeadSHA, err := forkGitRepo.GetBranchCommitID(forkHeadRef)
assert.NoError(t, err)
toRepoName := "migrated"
ctx := t.Context()
uploader := NewGiteaLocalUploader(ctx, fromRepoOwner, fromRepoOwner.Name, toRepoName)
uploader.gitServiceType = structs.GiteaService
assert.NoError(t, repo_service.Init(t.Context()))
assert.NoError(t, uploader.CreateRepo(ctx, &base.Repository{
Description: "description",
OriginalURL: fromRepo.RepoPath(),
CloneURL: fromRepo.RepoPath(),
IsPrivate: false,
IsMirror: true,
}, base.MigrateOptions{
GitServiceType: structs.GiteaService,
Private: false,
Mirror: true,
}))
for _, testCase := range []struct {
name string
head string
logFilter []string
logFiltered []bool
pr base.PullRequest
}{
{
name: "fork, good Head.SHA",
head: fmt.Sprintf("%s/%s", forkRepo.OwnerName, forkHeadRef),
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: forkRepo.RepoPath(),
Ref: forkHeadRef,
SHA: forkHeadSHA,
RepoName: forkRepo.Name,
OwnerName: forkRepo.OwnerName,
},
},
},
{
name: "fork, invalid Head.Ref",
head: "unknown repository",
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: forkRepo.RepoPath(),
Ref: "INVALID",
SHA: forkHeadSHA,
RepoName: forkRepo.Name,
OwnerName: forkRepo.OwnerName,
},
},
logFilter: []string{"Fetch branch from"},
logFiltered: []bool{true},
},
{
name: "invalid fork CloneURL",
head: "unknown repository",
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: "UNLIKELY",
Ref: forkHeadRef,
SHA: forkHeadSHA,
RepoName: forkRepo.Name,
OwnerName: "WRONG",
},
},
logFilter: []string{"AddRemote"},
logFiltered: []bool{true},
},
{
name: "no fork, good Head.SHA",
head: headRef,
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: headRef,
SHA: headSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
},
},
{
name: "no fork, empty Head.SHA",
head: headRef,
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: headRef,
SHA: "",
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
},
logFilter: []string{"Empty reference", "Cannot remove local head"},
logFiltered: []bool{true, false},
},
{
name: "no fork, invalid Head.SHA",
head: headRef,
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: headRef,
SHA: "brokenSHA",
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
},
logFilter: []string{"Deprecated local head"},
logFiltered: []bool{true},
},
{
name: "no fork, not found Head.SHA",
head: headRef,
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: headRef,
SHA: "2697b352310fcd01cbd1f3dbd43b894080027f68",
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
},
logFilter: []string{"Deprecated local head", "Cannot remove local head"},
logFiltered: []bool{true, false},
},
} {
t.Run(testCase.name, func(t *testing.T) {
stopMark := fmt.Sprintf(">>>>>>>>>>>>>STOP: %s<<<<<<<<<<<<<<<", testCase.name)
logChecker, cleanup := test.NewLogChecker(log.DEFAULT)
logChecker.Filter(testCase.logFilter...).StopMark(stopMark)
defer cleanup()
testCase.pr.EnsuredSafe = true
head, err := uploader.updateGitForPullRequest(ctx, &testCase.pr)
assert.NoError(t, err)
assert.Equal(t, testCase.head, head)
log.Info(stopMark)
logFiltered, logStopped := logChecker.Check(5 * time.Second)
assert.True(t, logStopped)
if len(testCase.logFilter) > 0 {
assert.Equal(t, testCase.logFiltered, logFiltered, "for log message filters: %v", testCase.logFilter)
}
})
}
}

View File

@ -478,6 +478,15 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba
break break
} }
} }
if len(mapInsertedPRIndexes) > 0 {
// The pull requests migrating process may created head branches in the base repository
// because head repository maybe a fork one which will not be migrated. So that we need
// to sync branches again.
log.Trace("syncing branches after migrating pull requests")
if err = uploader.SyncBranches(ctx); err != nil {
return err
}
}
} }
if opts.Comments && supportAllComments { if opts.Comments && supportAllComments {

View File

@ -232,7 +232,13 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
return true, nil return true, nil
} }
sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitHeadRefName()) gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
if err != nil {
return false, err
}
defer closer.Close()
sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo, pr.BaseBranch, pr.GetGitHeadRefName())
return sign, err return sign, err
} }

View File

@ -84,7 +84,7 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque
if err != nil { if err != nil {
defer cancel() defer cancel()
log.Error("failed to get sha of head branch in %-v: show-ref[%s] --hash refs/heads/tracking: %v", mergeCtx.pr, mergeCtx.tmpBasePath, err) log.Error("failed to get sha of head branch in %-v: show-ref[%s] --hash refs/heads/tracking: %v", mergeCtx.pr, mergeCtx.tmpBasePath, err)
return nil, nil, fmt.Errorf("unable to get sha of head branch in %v %w", pr, err) return nil, nil, fmt.Errorf("unable to get sha of head branch in pr[%d]: %w", pr.ID, err)
} }
if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID { if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID {
defer cancel() defer cancel()
@ -105,8 +105,15 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque
mergeCtx.sig = doer.NewGitSig() mergeCtx.sig = doer.NewGitSig()
mergeCtx.committer = mergeCtx.sig mergeCtx.committer = mergeCtx.sig
gitRepo, err := git.OpenRepository(ctx, mergeCtx.tmpBasePath)
if err != nil {
defer cancel()
return nil, nil, fmt.Errorf("failed to open temp git repo for pr[%d]: %w", mergeCtx.pr.ID, err)
}
defer gitRepo.Close()
// Determine if we should sign // Determine if we should sign
sign, key, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch) sign, key, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, gitRepo, "HEAD", trackingBranch)
if sign { if sign {
mergeCtx.signKey = key mergeCtx.signKey = key
if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {

View File

@ -12,6 +12,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/pull" "code.gitea.io/gitea/services/pull"
@ -35,7 +36,13 @@ func (err ErrCommitIDDoesNotMatch) Error() string {
// CherryPick cherry-picks or reverts a commit to the given repository // CherryPick cherry-picks or reverts a commit to the given repository
func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) { func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
if err := opts.Validate(ctx, repo, doer); err != nil { gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
if err != nil {
return nil, err
}
defer closer.Close()
if err := opts.Validate(ctx, repo, gitRepo, doer); err != nil {
return nil, err return nil, err
} }
message := strings.TrimSpace(opts.Message) message := strings.TrimSpace(opts.Message)

View File

@ -13,6 +13,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -52,7 +53,7 @@ type ApplyDiffPatchOptions struct {
} }
// Validate validates the provided options // Validate validates the provided options
func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error { func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doer *user_model.User) error {
// If no branch name is set, assume master // If no branch name is set, assume master
if opts.OldBranch == "" { if opts.OldBranch == "" {
opts.OldBranch = repo.DefaultBranch opts.OldBranch = repo.DefaultBranch
@ -95,7 +96,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode
} }
} }
if protectedBranch != nil && protectedBranch.RequireSignedCommits { if protectedBranch != nil && protectedBranch.RequireSignedCommits {
_, _, _, err := asymkey_service.SignCRUDAction(ctx, doer, repo.RepoPath(), opts.OldBranch) _, _, _, err := asymkey_service.SignCRUDAction(ctx, doer, gitRepo, opts.OldBranch)
if err != nil { if err != nil {
if !asymkey_service.IsErrWontSign(err) { if !asymkey_service.IsErrWontSign(err) {
return err return err
@ -116,7 +117,13 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
return nil, err return nil, err
} }
if err := opts.Validate(ctx, repo, doer); err != nil { gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
if err != nil {
return nil, err
}
defer closer.Close()
if err := opts.Validate(ctx, repo, gitRepo, doer); err != nil {
return nil, err return nil, err
} }

View File

@ -303,7 +303,7 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit
var key *git.SigningKey var key *git.SigningKey
var signer *git.Signature var signer *git.Signature
if opts.ParentCommitID != "" { if opts.ParentCommitID != "" {
sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, opts.DoerUser, t.basePath, opts.ParentCommitID) sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, opts.DoerUser, t.gitRepo, opts.ParentCommitID)
} else { } else {
sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, opts.DoerUser) sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, opts.DoerUser)
} }

View File

@ -167,7 +167,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
} }
} }
} }
} else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, treePaths); err != nil { } else if err := VerifyBranchProtection(ctx, repo, gitRepo, doer, opts.OldBranch, treePaths); err != nil {
return nil, err return nil, err
} }
@ -659,7 +659,7 @@ func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository,
} }
// VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch // VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch
func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName string, treePaths []string) error { func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doer *user_model.User, branchName string, treePaths []string) error {
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
if err != nil { if err != nil {
return err return err
@ -686,7 +686,7 @@ func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, do
} }
} }
if protectedBranch.RequireSignedCommits { if protectedBranch.RequireSignedCommits {
_, _, _, err := asymkey_service.SignCRUDAction(ctx, doer, repo.RepoPath(), branchName) _, _, _, err := asymkey_service.SignCRUDAction(ctx, doer, gitRepo, branchName)
if err != nil { if err != nil {
if !asymkey_service.IsErrWontSign(err) { if !asymkey_service.IsErrWontSign(err) {
return err return err

View File

@ -72,22 +72,22 @@ func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) {
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
var text string var text strings.Builder
// for each commit, generate attachment text // for each commit, generate attachment text
for i, commit := range p.Commits { for i, commit := range p.Commits {
var authorName string var authorName string
if commit.Author != nil { if commit.Author != nil {
authorName = " - " + commit.Author.Name authorName = " - " + commit.Author.Name
} }
text += fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL, text.WriteString(fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
strings.TrimRight(commit.Message, "\r\n")) + authorName strings.TrimRight(commit.Message, "\r\n")) + authorName)
// add linebreak to each commit but the last // add linebreak to each commit but the last
if i < len(p.Commits)-1 { if i < len(p.Commits)-1 {
text += "\r\n" text.WriteString("\r\n")
} }
} }
return createDingtalkPayload(title, text, linkText, titleLink), nil return createDingtalkPayload(title, text.String(), linkText, titleLink), nil
} }
// Issue implements PayloadConvertor Issue method // Issue implements PayloadConvertor Issue method

View File

@ -159,7 +159,7 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
var text string var text strings.Builder
// for each commit, generate attachment text // for each commit, generate attachment text
for i, commit := range p.Commits { for i, commit := range p.Commits {
// limit the commit message display to just the summary, otherwise it would be hard to read // limit the commit message display to just the summary, otherwise it would be hard to read
@ -169,14 +169,14 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
if utf8.RuneCountInString(message) > 50 { if utf8.RuneCountInString(message) > 50 {
message = fmt.Sprintf("%.47s...", message) message = fmt.Sprintf("%.47s...", message)
} }
text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name) text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name))
// add linebreak to each commit but the last // add linebreak to each commit but the last
if i < len(p.Commits)-1 { if i < len(p.Commits)-1 {
text += "\n" text.WriteString("\n")
} }
} }
return d.createPayload(p.Sender, title, text, titleLink, greenColor), nil return d.createPayload(p.Sender, title, text.String(), titleLink, greenColor), nil
} }
// Issue implements PayloadConvertor Issue method // Issue implements PayloadConvertor Issue method

View File

@ -76,22 +76,23 @@ func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) {
commitDesc string commitDesc string
) )
text := fmt.Sprintf("[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc) var text strings.Builder
text.WriteString(fmt.Sprintf("[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc))
// for each commit, generate attachment text // for each commit, generate attachment text
for i, commit := range p.Commits { for i, commit := range p.Commits {
var authorName string var authorName string
if commit.Author != nil { if commit.Author != nil {
authorName = " - " + commit.Author.Name authorName = " - " + commit.Author.Name
} }
text += fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL, text.WriteString(fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
strings.TrimRight(commit.Message, "\r\n")) + authorName strings.TrimRight(commit.Message, "\r\n")) + authorName)
// add linebreak to each commit but the last // add linebreak to each commit but the last
if i < len(p.Commits)-1 { if i < len(p.Commits)-1 {
text += "\r\n" text.WriteString("\r\n")
} }
} }
return newFeishuTextPayload(text), nil return newFeishuTextPayload(text.String()), nil
} }
// Issue implements PayloadConvertor Issue method // Issue implements PayloadConvertor Issue method

View File

@ -173,18 +173,19 @@ func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) {
repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s] %s pushed %s to %s:<br>", repoLink, p.Pusher.UserName, commitDesc, branchLink) var text strings.Builder
text.WriteString(fmt.Sprintf("[%s] %s pushed %s to %s:<br>", repoLink, p.Pusher.UserName, commitDesc, branchLink))
// for each commit, generate a new line text // for each commit, generate a new line text
for i, commit := range p.Commits { for i, commit := range p.Commits {
text += fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) text.WriteString(fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name))
// add linebreak to each commit but the last // add linebreak to each commit but the last
if i < len(p.Commits)-1 { if i < len(p.Commits)-1 {
text += "<br>" text.WriteString("<br>")
} }
} }
return m.newPayload(text, p.Commits...) return m.newPayload(text.String(), p.Commits...)
} }
// PullRequest implements payloadConvertor PullRequest method // PullRequest implements payloadConvertor PullRequest method

View File

@ -131,14 +131,14 @@ func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc) title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
var text string var text strings.Builder
// for each commit, generate attachment text // for each commit, generate attachment text
for i, commit := range p.Commits { for i, commit := range p.Commits {
text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL,
strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name) strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name))
// add linebreak to each commit but the last // add linebreak to each commit but the last
if i < len(p.Commits)-1 { if i < len(p.Commits)-1 {
text += "\n\n" text.WriteString("\n\n")
} }
} }
@ -146,7 +146,7 @@ func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
p.Repo, p.Repo,
p.Sender, p.Sender,
title, title,
text, text.String(),
titleLink, titleLink,
greenColor, greenColor,
&MSTeamsFact{"Commit count:", strconv.Itoa(p.TotalCommits)}, &MSTeamsFact{"Commit count:", strconv.Itoa(p.TotalCommits)},

View File

@ -208,13 +208,13 @@ func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
branchLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) branchLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName) text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName)
var attachmentText string var attachmentText strings.Builder
// for each commit, generate attachment text // for each commit, generate attachment text
for i, commit := range p.Commits { for i, commit := range p.Commits {
attachmentText += fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name)) attachmentText.WriteString(fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name)))
// add linebreak to each commit but the last // add linebreak to each commit but the last
if i < len(p.Commits)-1 { if i < len(p.Commits)-1 {
attachmentText += "\n" attachmentText.WriteString("\n")
} }
} }
@ -222,7 +222,7 @@ func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
Color: s.Color, Color: s.Color,
Title: p.Repo.HTMLURL, Title: p.Repo.HTMLURL,
TitleLink: p.Repo.HTMLURL, TitleLink: p.Repo.HTMLURL,
Text: attachmentText, Text: attachmentText.String(),
}}), nil }}), nil
} }

View File

@ -94,14 +94,14 @@ func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) {
} }
title := fmt.Sprintf(`[%s:%s] %s`, htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName), htmlLinkFormatter(titleLink, branchName), html.EscapeString(commitDesc)) title := fmt.Sprintf(`[%s:%s] %s`, htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName), htmlLinkFormatter(titleLink, branchName), html.EscapeString(commitDesc))
var htmlCommits string var htmlCommits strings.Builder
for _, commit := range p.Commits { for _, commit := range p.Commits {
htmlCommits += fmt.Sprintf("\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n"))) htmlCommits.WriteString(fmt.Sprintf("\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n"))))
if commit.Author != nil { if commit.Author != nil {
htmlCommits += " - " + html.EscapeString(commit.Author.Name) htmlCommits.WriteString(" - " + html.EscapeString(commit.Author.Name))
} }
} }
return createTelegramPayloadHTML(title + htmlCommits), nil return createTelegramPayloadHTML(title + htmlCommits.String()), nil
} }
// Issue implements PayloadConvertor Issue method // Issue implements PayloadConvertor Issue method

View File

@ -77,7 +77,7 @@ func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error
title := fmt.Sprintf("# %s:%s <font color=\"warning\"> %s </font>", p.Repo.FullName, branchName, commitDesc) title := fmt.Sprintf("# %s:%s <font color=\"warning\"> %s </font>", p.Repo.FullName, branchName, commitDesc)
var text string var text strings.Builder
// for each commit, generate attachment text // for each commit, generate attachment text
for i, commit := range p.Commits { for i, commit := range p.Commits {
var authorName string var authorName string
@ -86,15 +86,15 @@ func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error
} }
message := strings.ReplaceAll(commit.Message, "\n\n", "\r\n") message := strings.ReplaceAll(commit.Message, "\n\n", "\r\n")
text += fmt.Sprintf(" > [%s](%s) \r\n ><font color=\"info\">%s</font> \n ><font color=\"warning\">%s</font>", commit.ID[:7], commit.URL, text.WriteString(fmt.Sprintf(" > [%s](%s) \r\n ><font color=\"info\">%s</font> \n ><font color=\"warning\">%s</font>", commit.ID[:7], commit.URL,
message, authorName) message, authorName))
// add linebreak to each commit but the last // add linebreak to each commit but the last
if i < len(p.Commits)-1 { if i < len(p.Commits)-1 {
text += "\n" text.WriteString("\n")
} }
} }
return newWechatworkMarkdownPayload(title + "\r\n\r\n" + text), nil return newWechatworkMarkdownPayload(title + "\r\n\r\n" + text.String()), nil
} }
// Issue implements PayloadConvertor Issue method // Issue implements PayloadConvertor Issue method

View File

@ -193,7 +193,13 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
committer := doer.NewGitSig() committer := doer.NewGitSig()
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) originalGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo.WikiStorageRepo())
if err != nil {
return fmt.Errorf("unable to open wiki repository: %w", err)
}
defer closer.Close()
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, originalGitRepo, doer)
if sign { if sign {
commitTreeOpts.Key = signingKey commitTreeOpts.Key = signingKey
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
@ -212,7 +218,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
return err return err
} }
if err := gitrepo.PushFromLocal(gitRepo.Ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{ if err := gitrepo.PushFromLocal(ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch), Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
Env: repo_module.FullPushingEnvironment( Env: repo_module.FullPushingEnvironment(
doer, doer,
@ -315,7 +321,13 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
committer := doer.NewGitSig() committer := doer.NewGitSig()
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) originalGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo.WikiStorageRepo())
if err != nil {
return fmt.Errorf("unable to open wiki repository: %w", err)
}
defer closer.Close()
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, originalGitRepo, doer)
if sign { if sign {
commitTreeOpts.Key = signingKey commitTreeOpts.Key = signingKey
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {

View File

@ -10,18 +10,25 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"testing" "testing"
"time"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/migrations" "code.gitea.io/gitea/services/migrations"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestMigrateLocalPath(t *testing.T) { func TestMigrateLocalPath(t *testing.T) {
@ -112,3 +119,176 @@ func Test_UpdateCommentsMigrationsByType(t *testing.T) {
err := issues_model.UpdateCommentsMigrationsByType(t.Context(), structs.GithubService, "1", 1) err := issues_model.UpdateCommentsMigrationsByType(t.Context(), structs.GithubService, "1", 1)
assert.NoError(t, err) assert.NoError(t, err)
} }
func Test_MigrateFromGiteaToGitea(t *testing.T) {
defer tests.PrepareTestEnv(t)()
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
resp, err := http.Get("https://gitea.com/gitea")
if err != nil || resp.StatusCode != http.StatusOK {
if resp != nil {
resp.Body.Close()
}
t.Skipf("Can't reach https://gitea.com, skipping %s", t.Name())
}
resp.Body.Close()
repoName := fmt.Sprintf("gitea-to-gitea-%d", time.Now().UnixNano())
cloneAddr := "https://gitea.com/gitea/test_repo.git"
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &structs.MigrateRepoOptions{
CloneAddr: cloneAddr,
RepoOwnerID: owner.ID,
RepoName: repoName,
Service: structs.GiteaService.Name(),
Wiki: true,
Milestones: true,
Labels: true,
Issues: true,
PullRequests: true,
Releases: true,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
migratedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: repoName})
assert.Equal(t, owner.ID, migratedRepo.OwnerID)
assert.Equal(t, structs.GiteaService, migratedRepo.OriginalServiceType)
assert.Equal(t, cloneAddr, migratedRepo.OriginalURL)
issueCount := unittest.GetCount(t,
&issues_model.Issue{RepoID: migratedRepo.ID},
unittest.Cond("is_pull = ?", false),
)
assert.Equal(t, 7, issueCount)
pullCount := unittest.GetCount(t,
&issues_model.Issue{RepoID: migratedRepo.ID},
unittest.Cond("is_pull = ?", true),
)
assert.Equal(t, 6, pullCount)
issue4, err := issues_model.GetIssueWithAttrsByIndex(t.Context(), migratedRepo.ID, 4)
require.NoError(t, err)
assert.Equal(t, owner.ID, issue4.PosterID)
assert.Equal(t, "Ghost", issue4.OriginalAuthor)
assert.Equal(t, int64(-1), issue4.OriginalAuthorID)
assert.Equal(t, "what is this repo about?", issue4.Title)
assert.True(t, issue4.IsClosed)
assert.True(t, issue4.IsLocked)
if assert.NotNil(t, issue4.Milestone) {
assert.Equal(t, "V1", issue4.Milestone.Name)
}
labelNames := make([]string, 0, len(issue4.Labels))
for _, label := range issue4.Labels {
labelNames = append(labelNames, label.Name)
}
assert.Contains(t, labelNames, "Question")
reactionTypes := make([]string, 0, len(issue4.Reactions))
for _, reaction := range issue4.Reactions {
reactionTypes = append(reactionTypes, reaction.Type)
}
assert.ElementsMatch(t, []string{"laugh"}, reactionTypes) // gitea's author is ghost which will be ignored when migrating reactions
comments, err := issues_model.FindComments(t.Context(), &issues_model.FindCommentsOptions{
IssueID: issue4.ID,
Type: issues_model.CommentTypeComment,
})
require.NoError(t, err)
require.Len(t, comments, 2)
assert.Equal(t, owner.ID, comments[0].PosterID)
assert.Equal(t, int64(689), comments[0].OriginalAuthorID)
assert.Equal(t, "6543", comments[0].OriginalAuthor)
assert.Contains(t, comments[0].Content, "TESTSET for gitea2gitea")
assert.Equal(t, owner.ID, comments[1].PosterID)
assert.Equal(t, "Ghost", comments[1].OriginalAuthor)
assert.Equal(t, int64(-1), comments[1].OriginalAuthorID)
assert.Equal(t, "Oh!", strings.TrimSpace(comments[1].Content))
pr12, err := issues_model.GetPullRequestByIndex(t.Context(), migratedRepo.ID, 12)
require.NoError(t, err)
assert.Equal(t, owner.ID, pr12.Issue.PosterID)
assert.Equal(t, "6543", pr12.Issue.OriginalAuthor)
assert.Equal(t, int64(689), pr12.Issue.OriginalAuthorID)
assert.Equal(t, "Dont Touch", pr12.Issue.Title)
assert.True(t, pr12.Issue.IsClosed)
assert.True(t, pr12.HasMerged)
assert.Equal(t, "827aa28a907853e5ddfa40c8f9bc52471a2685fd", pr12.MergedCommitID)
assert.NoError(t, pr12.Issue.LoadMilestone(t.Context()))
if assert.NotNil(t, pr12.Issue.Milestone) {
assert.Equal(t, "V2 Finalize", pr12.Issue.Milestone.Name)
}
assert.Contains(t, pr12.Issue.Content, "dont touch")
pr8, err := issues_model.GetPullRequestByIndex(t.Context(), migratedRepo.ID, 8)
require.NoError(t, err)
assert.Equal(t, owner.ID, pr8.Issue.PosterID)
assert.Equal(t, "6543", pr8.Issue.OriginalAuthor)
assert.Equal(t, int64(689), pr8.Issue.OriginalAuthorID)
assert.Equal(t, "add garbage for close pull", pr8.Issue.Title)
assert.True(t, pr8.Issue.IsClosed)
assert.False(t, pr8.HasMerged)
assert.Contains(t, pr8.Issue.Content, "well you'll see")
pr13, err := issues_model.GetPullRequestByIndex(t.Context(), migratedRepo.ID, 13)
require.NoError(t, err)
assert.Equal(t, owner.ID, pr13.Issue.PosterID)
assert.Equal(t, "6543", pr13.Issue.OriginalAuthor)
assert.Equal(t, int64(689), pr13.Issue.OriginalAuthorID)
assert.Equal(t, "extend", pr13.Issue.Title)
assert.False(t, pr13.Issue.IsClosed)
assert.False(t, pr13.HasMerged)
assert.True(t, pr13.Issue.IsLocked)
gitRepo, err := gitrepo.OpenRepository(t.Context(), migratedRepo)
require.NoError(t, err)
defer gitRepo.Close()
branches, _, err := gitRepo.GetBranchNames(0, 0)
require.NoError(t, err)
assert.ElementsMatch(t, []string{"6543-patch-1", "master", "6543-forks/add-xkcd-2199"}, branches) // last branch comes from the pull request
branchNames, err := git_model.FindBranchNames(t.Context(), git_model.FindBranchOptions{
RepoID: migratedRepo.ID,
})
require.NoError(t, err)
assert.ElementsMatch(t, []string{"6543-patch-1", "master", "6543-forks/add-xkcd-2199"}, branchNames)
tags, _, err := gitRepo.GetTagInfos(0, 0)
require.NoError(t, err)
tagNames := make([]string, 0, len(tags))
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
assert.ElementsMatch(t, []string{"V1", "v2-rc1"}, tagNames)
releases, err := db.Find[repo_model.Release](t.Context(), repo_model.FindReleasesOptions{
RepoID: migratedRepo.ID,
IncludeDrafts: true,
IncludeTags: false,
})
require.NoError(t, err)
require.Len(t, releases, 2)
releaseMap := make(map[string]*repo_model.Release, len(releases))
for _, rel := range releases {
releaseMap[rel.TagName] = rel
assert.Equal(t, owner.ID, rel.PublisherID)
assert.Equal(t, "6543", rel.OriginalAuthor)
assert.Equal(t, int64(689), rel.OriginalAuthorID)
assert.False(t, rel.IsDraft)
}
require.Contains(t, releaseMap, "v2-rc1")
v2Release := releaseMap["v2-rc1"]
assert.Equal(t, "Second Release", v2Release.Title)
assert.True(t, v2Release.IsPrerelease)
assert.Contains(t, v2Release.Note, "this repo has:")
require.Contains(t, releaseMap, "V1")
v1Release := releaseMap["V1"]
assert.Equal(t, "First Release", v1Release.Title)
assert.False(t, v1Release.IsPrerelease)
assert.Equal(t, "as title", strings.TrimSpace(v1Release.Note))
}

View File

@ -4,6 +4,7 @@
package integration package integration
import ( import (
"encoding/base64"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -17,7 +18,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/git/gitcmd"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -153,8 +156,16 @@ func TestPullCreate(t *testing.T) {
url := test.RedirectURL(resp) url := test.RedirectURL(resp)
assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url) assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url)
// test create the pull request again and it should fail now
link := "/user2/repo1/compare/master...user1/repo1:master"
req := NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"title": "This is a pull title",
})
session.MakeRequest(t, req, http.StatusBadRequest)
// check .diff can be accessed and matches performed change // check .diff can be accessed and matches performed change
req := NewRequest(t, "GET", url+".diff") req = NewRequest(t, "GET", url+".diff")
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
assert.Regexp(t, `\+Hello, World \(Edited\)`, resp.Body) assert.Regexp(t, `\+Hello, World \(Edited\)`, resp.Body)
assert.Regexp(t, "^diff", resp.Body) assert.Regexp(t, "^diff", resp.Body)
@ -295,6 +306,95 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) {
}) })
} }
func TestCreatePullRequestFromNestedOrgForks(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
const (
baseOrg = "test-fork-org1"
midForkOrg = "test-fork-org2"
leafForkOrg = "test-fork-org3"
repoName = "test-fork-repo"
patchBranch = "teabot-patch-1"
)
createOrg := func(name string) {
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
UserName: name,
Visibility: "public",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
}
createOrg(baseOrg)
createOrg(midForkOrg)
createOrg(leafForkOrg)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/repos", baseOrg), &api.CreateRepoOption{
Name: repoName,
AutoInit: true,
DefaultBranch: "main",
Private: false,
Readme: "Default",
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
var baseRepo api.Repository
DecodeJSON(t, resp, &baseRepo)
assert.Equal(t, "main", baseRepo.DefaultBranch)
forkIntoOrg := func(srcOrg, dstOrg string) api.Repository {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", srcOrg, repoName), &api.CreateForkOption{
Organization: util.ToPointer(dstOrg),
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusAccepted)
var forkRepo api.Repository
DecodeJSON(t, resp, &forkRepo)
assert.NotNil(t, forkRepo.Owner)
if forkRepo.Owner != nil {
assert.Equal(t, dstOrg, forkRepo.Owner.UserName)
}
return forkRepo
}
forkIntoOrg(baseOrg, midForkOrg)
forkIntoOrg(midForkOrg, leafForkOrg)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", leafForkOrg, repoName, "patch-from-org3.txt"), &api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: "main",
NewBranchName: patchBranch,
Message: "create patch from org3",
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte("patch content")),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
prPayload := map[string]string{
"head": fmt.Sprintf("%s:%s", leafForkOrg, patchBranch),
"base": "main",
"title": "test creating pull from test-fork-org3 to test-fork-org1",
}
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/pulls", baseOrg, repoName), prPayload).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
var pr api.PullRequest
DecodeJSON(t, resp, &pr)
assert.Equal(t, prPayload["title"], pr.Title)
if assert.NotNil(t, pr.Head) {
assert.Equal(t, patchBranch, pr.Head.Ref)
if assert.NotNil(t, pr.Head.Repository) {
assert.Equal(t, fmt.Sprintf("%s/%s", leafForkOrg, repoName), pr.Head.Repository.FullName)
}
}
if assert.NotNil(t, pr.Base) {
assert.Equal(t, "main", pr.Base.Ref)
if assert.NotNil(t, pr.Base.Repository) {
assert.Equal(t, fmt.Sprintf("%s/%s", baseOrg, repoName), pr.Base.Repository.FullName)
}
}
})
}
func TestPullCreateParallel(t *testing.T) { func TestPullCreateParallel(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { onGiteaRun(t, func(t *testing.T, u *url.URL) {
sessionFork := loginUser(t, "user1") sessionFork := loginUser(t, "user1")