diff --git a/.air.toml b/.air.toml index 19e9a150b4..d506c19426 100644 --- a/.air.toml +++ b/.air.toml @@ -2,9 +2,10 @@ root = "." tmp_dir = ".air" [build] +pre_cmd = ["killall -9 gitea 2>/dev/null || true"] # kill off potential zombie processes from previous runs cmd = "make --no-print-directory backend" bin = "gitea" -delay = 1000 +delay = 2000 include_ext = ["go", "tmpl"] include_file = ["main.go"] include_dir = ["cmd", "models", "modules", "options", "routers", "services"] diff --git a/.deadcode-out b/.deadcode-out index 2d4104ff2e..62458dd6b6 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -65,6 +65,7 @@ package "code.gitea.io/gitea/models/migrations/base" func MainTest package "code.gitea.io/gitea/models/organization" + func GetTeamNamesByID func UpdateTeamUnits func (SearchMembersOptions).ToConds func UsersInTeamsCount @@ -130,6 +131,7 @@ package "code.gitea.io/gitea/models/user" func GetUserAllSettings func DeleteUserSetting func GetUserEmailsByNames + func GetUserNamesByIDs package "code.gitea.io/gitea/modules/activitypub" func CurrentTime diff --git a/Makefile b/Makefile index f8f7fffb19..6e46283ac8 100644 --- a/Makefile +++ b/Makefile @@ -121,7 +121,6 @@ LDFLAGS := $(LDFLAGS) -X "main.ReleaseVersion=$(RELEASE_VERSION)" -X "main.MakeV LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 ifeq ($(HAS_GO), yes) - GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) $(shell $(GO) list code.gitea.io/gitea/models/forgejo_migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) endif @@ -155,9 +154,9 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN GO_DIRS := build cmd models modules routers services tests WEB_DIRS := web_src/js web_src/css -ESLINT_FILES := web_src/js tools *.config.js tests/e2e +ESLINT_FILES := web_src/js tools *.js tests/e2e STYLELINT_FILES := web_src/css web_src/js/components/*.vue -SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github +SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github $(wildcard *.go *.js *.md *.yml *.yaml *.toml) GO_SOURCES := $(wildcard *.go) GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go) @@ -453,7 +452,7 @@ lint-go-windows: .PHONY: lint-go-vet lint-go-vet: @echo "Running go vet..." - @$(GO) vet $(GO_PACKAGES) + @$(GO) vet ./... .PHONY: lint-editorconfig lint-editorconfig: @@ -768,7 +767,7 @@ generate-backend: $(TAGS_PREREQ) generate-go .PHONY: generate-go generate-go: $(TAGS_PREREQ) @echo "Running go generate..." - @CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' $(GO_PACKAGES) + @CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' ./... .PHONY: merge-locales merge-locales: diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go index 824d66d112..bd9063a8e4 100644 --- a/cmd/admin_user_change_password.go +++ b/cmd/admin_user_change_password.go @@ -36,6 +36,7 @@ var microcmdUserChangePassword = &cli.Command{ &cli.BoolFlag{ Name: "must-change-password", Usage: "User must change password", + Value: true, }, }, } @@ -57,23 +58,18 @@ func runChangePassword(c *cli.Context) error { return err } - var mustChangePassword optional.Option[bool] - if c.IsSet("must-change-password") { - mustChangePassword = optional.Some(c.Bool("must-change-password")) - } - opts := &user_service.UpdateAuthOptions{ Password: optional.Some(c.String("password")), - MustChangePassword: mustChangePassword, + MustChangePassword: optional.Some(c.Bool("must-change-password")), } if err := user_service.UpdateAuth(ctx, user, opts); err != nil { switch { case errors.Is(err, password.ErrMinLength): - return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength) + return fmt.Errorf("password is not long enough, needs to be at least %d characters", setting.MinPasswordLength) case errors.Is(err, password.ErrComplexity): - return errors.New("Password does not meet complexity requirements") + return errors.New("password does not meet complexity requirements") case errors.Is(err, password.ErrIsPwned): - return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords") + return errors.New("the password is in a list of stolen passwords previously exposed in public data breaches, please try again with a different password, to see more details: https://haveibeenpwned.com/Passwords") default: return err } diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index 10965c7e8f..caafef536c 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -8,6 +8,7 @@ import ( "fmt" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" pwd "code.gitea.io/gitea/modules/auth/password" "code.gitea.io/gitea/modules/optional" @@ -46,9 +47,10 @@ var microcmdUserCreate = &cli.Command{ Usage: "Generate a random password for the user", }, &cli.BoolFlag{ - Name: "must-change-password", - Usage: "Set this option to false to prevent forcing the user to change their password after initial login", - Value: true, + Name: "must-change-password", + Usage: "Set this option to false to prevent forcing the user to change their password after initial login", + Value: true, + DisableDefaultText: true, }, &cli.IntFlag{ Name: "random-password-length", @@ -72,10 +74,10 @@ func runCreateUser(c *cli.Context) error { } if c.IsSet("name") && c.IsSet("username") { - return errors.New("Cannot set both --name and --username flags") + return errors.New("cannot set both --name and --username flags") } if !c.IsSet("name") && !c.IsSet("username") { - return errors.New("One of --name or --username flags must be set") + return errors.New("one of --name or --username flags must be set") } if c.IsSet("password") && c.IsSet("random-password") { @@ -111,12 +113,21 @@ func runCreateUser(c *cli.Context) error { return errors.New("must set either password or random-password flag") } - changePassword := c.Bool("must-change-password") - - // If this is the first user being created. - // Take it as the admin and don't force a password update. - if n := user_model.CountUsers(ctx, nil); n == 0 { - changePassword = false + isAdmin := c.Bool("admin") + mustChangePassword := true // always default to true + if c.IsSet("must-change-password") { + // if the flag is set, use the value provided by the user + mustChangePassword = c.Bool("must-change-password") + } else { + // check whether there are users in the database + hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{}) + if err != nil { + return fmt.Errorf("IsTableNotEmpty: %w", err) + } + if !hasUserRecord && isAdmin { + // if this is the first admin being created, don't force to change password (keep the old behavior) + mustChangePassword = false + } } restricted := optional.None[bool]() @@ -132,8 +143,8 @@ func runCreateUser(c *cli.Context) error { Name: username, Email: c.String("email"), Passwd: password, - IsAdmin: c.Bool("admin"), - MustChangePassword: changePassword, + IsAdmin: isAdmin, + MustChangePassword: mustChangePassword, Visibility: visibility, } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 304eb3af75..23b41e0853 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2394,22 +2394,6 @@ LEVEL = Info ;; Enable issue by repository metrics; default is false ;ENABLED_ISSUE_BY_REPOSITORY = false -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;[task] -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; -;; Task queue type, could be `channel` or `redis`. -;QUEUE_TYPE = channel -;; -;; Task queue length, available only when `QUEUE_TYPE` is `channel`. -;QUEUE_LENGTH = 1000 -;; -;; Task queue connection string, available only when `QUEUE_TYPE` is `redis`. -;; If there is a password of redis, use `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for `redis-clsuter`. -;QUEUE_CONN_STR = "redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s" - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[migrations] diff --git a/models/activities/action_list.go b/models/activities/action_list.go index 6e23b173b5..aafb7f8a26 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -83,6 +83,9 @@ func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]* _, alreadyLoaded := userMap[action.Repo.OwnerID] return action.Repo.OwnerID, !alreadyLoaded }) + if len(missingUserIDs) == 0 { + return nil + } if err := db.GetEngine(ctx). In("id", missingUserIDs). @@ -129,6 +132,9 @@ func (actions ActionList) LoadComments(ctx context.Context) error { commentIDs = append(commentIDs, action.CommentID) } } + if len(commentIDs) == 0 { + return nil + } commentsMap := make(map[int64]*issues_model.Comment, len(commentIDs)) if err := db.GetEngine(ctx).In("id", commentIDs).Find(&commentsMap); err != nil { diff --git a/models/db/engine.go b/models/db/engine.go index 09f64b6c8f..1f0cd1a5dd 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -293,8 +293,8 @@ func MaxBatchInsertSize(bean any) int { } // IsTableNotEmpty returns true if table has at least one record -func IsTableNotEmpty(tableName string) (bool, error) { - return x.Table(tableName).Exist() +func IsTableNotEmpty(beanOrTableName any) (bool, error) { + return x.Table(beanOrTableName).Exist() } // DeleteAllRecords will delete all the records of this table diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 505dbaa0a5..278e8e3a86 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -287,9 +287,10 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) { // SearchVersions gets all versions of packages matching the search options func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { sess := db.GetEngine(ctx). - Where(opts.ToConds()). + Select("package_version.*"). Table("package_version"). - Join("INNER", "package", "package.id = package_version.package_id") + Join("INNER", "package", "package.id = package_version.package_id"). + Where(opts.ToConds()) opts.configureOrderBy(sess) @@ -304,19 +305,18 @@ func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*Package // SearchLatestVersions gets the latest version of every package matching the search options func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { - cond := opts.ToConds(). - And(builder.Expr("pv2.id IS NULL")) - - joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))") - if opts.IsInternal.Has() { - joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()}) - } + in := builder. + Select("MAX(package_version.id)"). + From("package_version"). + InnerJoin("package", "package.id = package_version.package_id"). + Where(opts.ToConds()). + GroupBy("package_version.package_id") sess := db.GetEngine(ctx). + Select("package_version.*"). Table("package_version"). - Join("LEFT", "package_version pv2", joinCond). Join("INNER", "package", "package.id = package_version.package_id"). - Where(cond) + Where(builder.In("package_version.id", in)) opts.configureOrderBy(sess) diff --git a/modules/git/commit.go b/modules/git/commit.go index 00681e3b2f..b5ae2e0e52 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -462,7 +462,7 @@ func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) { _, _ = rd.Discard(1) } for { - modifier, err := rd.ReadSlice('\x00') + modifier, err := rd.ReadString('\x00') if err != nil { if err != io.EOF { log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) diff --git a/modules/session/store.go b/modules/session/store.go index 2f7ab7760b..70988fcdc5 100644 --- a/modules/session/store.go +++ b/modules/session/store.go @@ -6,9 +6,6 @@ package session import ( "net/http" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/web/middleware" - "gitea.com/go-chi/session" ) @@ -21,10 +18,12 @@ type Store interface { // RegenerateSession regenerates the underlying session and returns the new store func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, error) { - // Ensure that a cookie with a trailing slash does not take precedence over - // the cookie written by the middleware. - middleware.DeleteLegacySiteCookie(resp, setting.SessionConfig.CookieName) - + for _, f := range BeforeRegenerateSession { + f(resp, req) + } s, err := session.RegenerateSession(resp, req) return s, err } + +// BeforeRegenerateSession is a list of functions that are called before a session is regenerated. +var BeforeRegenerateSession []func(http.ResponseWriter, *http.Request) diff --git a/modules/structs/repo_compare.go b/modules/structs/repo_compare.go new file mode 100644 index 0000000000..8a12498705 --- /dev/null +++ b/modules/structs/repo_compare.go @@ -0,0 +1,10 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +// Compare represents a comparison between two commits. +type Compare struct { + TotalCommits int `json:"total_commits"` // Total number of commits in the comparison. + Commits []*Commit `json:"commits"` // List of commits in the comparison. +} diff --git a/modules/structs/user.go b/modules/structs/user.go index 82b565e5e7..ad529c966e 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -19,6 +19,8 @@ type User struct { // the user's authentication sign-in name. // default: empty LoginName string `json:"login_name"` + // The ID of the user's Authentication Source + SourceID int64 `json:"source_id"` // the user's full name FullName string `json:"full_name"` // swagger:strfmt email diff --git a/modules/web/middleware/cookie.go b/modules/web/middleware/cookie.go index 0bed726793..ec6b06f993 100644 --- a/modules/web/middleware/cookie.go +++ b/modules/web/middleware/cookie.go @@ -9,6 +9,7 @@ import ( "net/url" "strings" + "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" ) @@ -48,12 +49,12 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) { // Previous versions would use a cookie path with a trailing /. // These are more specific than cookies without a trailing /, so // we need to delete these if they exist. - DeleteLegacySiteCookie(resp, name) + deleteLegacySiteCookie(resp, name) } -// DeleteLegacySiteCookie deletes the cookie with the given name at the cookie +// deleteLegacySiteCookie deletes the cookie with the given name at the cookie // path with a trailing /, which would unintentionally override the cookie. -func DeleteLegacySiteCookie(resp http.ResponseWriter, name string) { +func deleteLegacySiteCookie(resp http.ResponseWriter, name string) { if setting.SessionConfig.CookiePath == "" || strings.HasSuffix(setting.SessionConfig.CookiePath, "/") { // If the cookie path ends with /, no legacy cookies will take // precedence, so do nothing. The exception is that cookies with no @@ -74,3 +75,11 @@ func DeleteLegacySiteCookie(resp http.ResponseWriter, name string) { } resp.Header().Add("Set-Cookie", cookie.String()) } + +func init() { + session.BeforeRegenerateSession = append(session.BeforeRegenerateSession, func(resp http.ResponseWriter, _ *http.Request) { + // Ensure that a cookie with a trailing slash does not take precedence over + // the cookie written by the middleware. + deleteLegacySiteCookie(resp, setting.SessionConfig.CookieName) + }) +} diff --git a/options/license/BSD-2-clause-first-lines b/options/license/BSD-2-clause-first-lines new file mode 100644 index 0000000000..3774caf24a --- /dev/null +++ b/options/license/BSD-2-clause-first-lines @@ -0,0 +1,27 @@ +Copyright (C) 2006,2007,2009 NTT (Nippon Telegraph and Telephone +Corporation). All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above + copyright notice, this list of conditions and the following + disclaimer as the first lines of this file unmodified. + +2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +THIS SOFTWARE IS PROVIDED BY NTT "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL NTT BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/options/license/Sun-PPP-2000 b/options/license/Sun-PPP-2000 new file mode 100644 index 0000000000..914c19544a --- /dev/null +++ b/options/license/Sun-PPP-2000 @@ -0,0 +1,13 @@ +Copyright (c) 2000 by Sun Microsystems, Inc. +All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation is hereby granted, provided that the above copyright +notice appears in all copies. + +SUN MAKES NO REPRESENTATION OR WARRANTIES ABOUT THE SUITABILITY OF +THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR +ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR +DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES diff --git a/options/license/pkgconf b/options/license/pkgconf new file mode 100644 index 0000000000..b8b2ffd996 --- /dev/null +++ b/options/license/pkgconf @@ -0,0 +1,7 @@ +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +This software is provided 'as is' and without any warranty, express or +implied. In no event shall the authors be liable for any damages arising +from the use of this software. diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index cef2a986e0..9ea210ee4e 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -30,7 +30,7 @@ import ( user_service "code.gitea.io/gitea/services/user" ) -func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64, loginName string) { +func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64) { if sourceID == 0 { return } @@ -47,7 +47,6 @@ func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64 u.LoginType = source.Type u.LoginSource = source.ID - u.LoginName = loginName } // CreateUser create a user @@ -83,12 +82,13 @@ func CreateUser(ctx *context.APIContext) { Passwd: form.Password, MustChangePassword: true, LoginType: auth.Plain, + LoginName: form.LoginName, } if form.MustChangePassword != nil { u.MustChangePassword = *form.MustChangePassword } - parseAuthSource(ctx, u, form.SourceID, form.LoginName) + parseAuthSource(ctx, u, form.SourceID) if ctx.Written() { return } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 80f2243ef0..a15210ac51 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -983,6 +983,8 @@ func Routes() *web.Route { m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate) m.Group("/{username}/{reponame}", func() { + m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff) + m.Combo("").Get(reqAnyRepoReader(), repo.Get). Delete(reqToken(), reqOwner(), repo.Delete). Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index c33beee0ae..852b7a2ee0 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -437,7 +437,7 @@ func GetBranchProtection(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToBranchProtection(ctx, bp)) + ctx.JSON(http.StatusOK, convert.ToBranchProtection(ctx, bp, repo)) } // ListBranchProtections list branch protections for a repo @@ -470,7 +470,7 @@ func ListBranchProtections(ctx *context.APIContext) { } apiBps := make([]*api.BranchProtection, len(bps)) for i := range bps { - apiBps[i] = convert.ToBranchProtection(ctx, bps[i]) + apiBps[i] = convert.ToBranchProtection(ctx, bps[i], repo) } ctx.JSON(http.StatusOK, apiBps) @@ -682,7 +682,7 @@ func CreateBranchProtection(ctx *context.APIContext) { return } - ctx.JSON(http.StatusCreated, convert.ToBranchProtection(ctx, bp)) + ctx.JSON(http.StatusCreated, convert.ToBranchProtection(ctx, bp, repo)) } // EditBranchProtection edits a branch protection for a repo @@ -964,7 +964,7 @@ func EditBranchProtection(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToBranchProtection(ctx, bp)) + ctx.JSON(http.StatusOK, convert.ToBranchProtection(ctx, bp, repo)) } // DeleteBranchProtection deletes a branch protection for a repo diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go new file mode 100644 index 0000000000..549b9b7fa9 --- /dev/null +++ b/routers/api/v1/repo/compare.go @@ -0,0 +1,99 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/gitrepo" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// CompareDiff compare two branches or commits +func CompareDiff(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} Get commit comparison information + // --- + // summary: Get commit comparison information + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: basehead + // in: path + // description: compare two branches or commits + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Compare" + // "404": + // "$ref": "#/responses/notFound" + + if ctx.Repo.GitRepo == nil { + gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + if err != nil { + ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + return + } + ctx.Repo.GitRepo = gitRepo + defer gitRepo.Close() + } + + infoPath := ctx.Params("*") + infos := []string{ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository.DefaultBranch} + if infoPath != "" { + infos = strings.SplitN(infoPath, "...", 2) + if len(infos) != 2 { + if infos = strings.SplitN(infoPath, "..", 2); len(infos) != 2 { + infos = []string{ctx.Repo.Repository.DefaultBranch, infoPath} + } + } + } + + _, _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{ + Base: infos[0], + Head: infos[1], + }) + if ctx.Written() { + return + } + defer headGitRepo.Close() + + verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") + files := ctx.FormString("files") == "" || ctx.FormBool("files") + + apiCommits := make([]*api.Commit, 0, len(ci.Commits)) + userCache := make(map[string]*user_model.User) + for i := 0; i < len(ci.Commits); i++ { + apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.Commits[i], userCache, + convert.ToCommitOptions{ + Stat: true, + Verification: verification, + Files: files, + }) + if err != nil { + ctx.ServerError("toCommit", err) + return + } + apiCommits = append(apiCommits, apiCommit) + } + + ctx.JSON(http.StatusOK, &api.Compare{ + TotalCommits: len(ci.Commits), + Commits: apiCommits, + }) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index c7fa98a697..b55ea1d0a9 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -421,3 +421,9 @@ type swaggerBlockedUserList struct { // in:body Body []api.BlockedUser `json:"body"` } + +// swagger:response Compare +type swaggerCompare struct { + // in:body + Body api.Compare `json:"body"` +} diff --git a/routers/common/compare.go b/routers/common/compare.go new file mode 100644 index 0000000000..4d1cc2f0d8 --- /dev/null +++ b/routers/common/compare.go @@ -0,0 +1,21 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" +) + +// CompareInfo represents the collected results from ParseCompareInfo +type CompareInfo struct { + HeadUser *user_model.User + HeadRepo *repo_model.Repository + HeadGitRepo *git.Repository + CompareInfo *git.CompareInfo + BaseBranch string + HeadBranch string + DirectComparison bool +} diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 38f9b85ace..821228b347 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -104,7 +104,7 @@ func Projects(ctx *context.Context) { } for _, project := range projects { - project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render? + project.RenderedContent = templates.RenderMarkdownToHtml(ctx, project.Description) } err = shared_user.LoadHeaderCount(ctx) @@ -372,7 +372,7 @@ func ViewProject(ctx *context.Context) { } } - project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render? + project.RenderedContent = templates.RenderMarkdownToHtml(ctx, project.Description) ctx.Data["LinkedPRs"] = linkedPrsMap ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 93c224b7be..b07209c779 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -35,6 +35,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/gitdiff" @@ -185,21 +186,10 @@ func setCsvCompareContext(ctx *context.Context) { } } -// CompareInfo represents the collected results from ParseCompareInfo -type CompareInfo struct { - HeadUser *user_model.User - HeadRepo *repo_model.Repository - HeadGitRepo *git.Repository - CompareInfo *git.CompareInfo - BaseBranch string - HeadBranch string - DirectComparison bool -} - // ParseCompareInfo parse compare info between two commit for preparing comparing references -func ParseCompareInfo(ctx *context.Context) *CompareInfo { +func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { baseRepo := ctx.Repo.Repository - ci := &CompareInfo{} + ci := &common.CompareInfo{} fileOnly := ctx.FormBool("file-only") @@ -576,7 +566,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { // PrepareCompareDiff renders compare diff page func PrepareCompareDiff( ctx *context.Context, - ci *CompareInfo, + ci *common.CompareInfo, whitespaceBehavior git.TrustedCmdArgs, ) bool { var ( diff --git a/routers/web/web.go b/routers/web/web.go index 71d5efef2c..154e1ce24b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -258,7 +258,7 @@ func Routes() *web.Route { routes.Get("/metrics", append(mid, Metrics)...) } - routes.Get("/robots.txt", append(mid, misc.RobotsTxt)...) + routes.Methods("GET,HEAD", "/robots.txt", append(mid, misc.RobotsTxt)...) routes.Get("/ssh_info", misc.SSHInfo) routes.Get("/api/healthz", healthcheck.Check) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index ef37ff87ee..365212d9c2 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -80,6 +80,11 @@ func newNotifyInput(repo *repo_model.Repository, doer *user_model.User, event we } } +func newNotifyInputForSchedules(repo *repo_model.Repository) *notifyInput { + // the doer here will be ignored as we force using action user when handling schedules + return newNotifyInput(repo, user_model.NewActionsUser(), webhook_module.HookEventSchedule) +} + func (input *notifyInput) WithDoer(doer *user_model.User) *notifyInput { input.Doer = doer return input @@ -562,7 +567,7 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) // We need a notifyInput to call handleSchedules // if repo is a mirror, commit author maybe an external user, // so we use action user as the Doer of the notifyInput - notifyInput := newNotifyInput(repo, user_model.NewActionsUser(), webhook_module.HookEventSchedule) + notifyInput := newNotifyInputForSchedules(repo) return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch) } diff --git a/services/convert/convert.go b/services/convert/convert.go index 5b06b9b1a0..55996d8fe3 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -21,6 +21,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" @@ -105,33 +106,46 @@ func ToBranch(ctx context.Context, repo *repo_model.Repository, branchName strin return branch, nil } +// getWhitelistEntities returns the names of the entities that are in the whitelist +func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T, whitelistIDs []int64) []string { + whitelistUserIDsSet := container.SetOf(whitelistIDs...) + whitelistNames := make([]string, 0) + for _, entity := range entities { + switch v := any(entity).(type) { + case *user_model.User: + if whitelistUserIDsSet.Contains(v.ID) { + whitelistNames = append(whitelistNames, v.Name) + } + case *organization.Team: + if whitelistUserIDsSet.Contains(v.ID) { + whitelistNames = append(whitelistNames, v.Name) + } + } + } + + return whitelistNames +} + // ToBranchProtection convert a ProtectedBranch to api.BranchProtection -func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch) *api.BranchProtection { - pushWhitelistUsernames, err := user_model.GetUserNamesByIDs(ctx, bp.WhitelistUserIDs) +func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection { + readers, err := access_model.GetRepoReaders(ctx, repo) if err != nil { - log.Error("GetUserNamesByIDs (WhitelistUserIDs): %v", err) + log.Error("GetRepoReaders: %v", err) } - mergeWhitelistUsernames, err := user_model.GetUserNamesByIDs(ctx, bp.MergeWhitelistUserIDs) + + pushWhitelistUsernames := getWhitelistEntities(readers, bp.WhitelistUserIDs) + mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs) + approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs) + + teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead) if err != nil { - log.Error("GetUserNamesByIDs (MergeWhitelistUserIDs): %v", err) - } - approvalsWhitelistUsernames, err := user_model.GetUserNamesByIDs(ctx, bp.ApprovalsWhitelistUserIDs) - if err != nil { - log.Error("GetUserNamesByIDs (ApprovalsWhitelistUserIDs): %v", err) - } - pushWhitelistTeams, err := organization.GetTeamNamesByID(ctx, bp.WhitelistTeamIDs) - if err != nil { - log.Error("GetTeamNamesByID (WhitelistTeamIDs): %v", err) - } - mergeWhitelistTeams, err := organization.GetTeamNamesByID(ctx, bp.MergeWhitelistTeamIDs) - if err != nil { - log.Error("GetTeamNamesByID (MergeWhitelistTeamIDs): %v", err) - } - approvalsWhitelistTeams, err := organization.GetTeamNamesByID(ctx, bp.ApprovalsWhitelistTeamIDs) - if err != nil { - log.Error("GetTeamNamesByID (ApprovalsWhitelistTeamIDs): %v", err) + log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err) } + pushWhitelistTeams := getWhitelistEntities(teamReaders, bp.WhitelistTeamIDs) + mergeWhitelistTeams := getWhitelistEntities(teamReaders, bp.MergeWhitelistTeamIDs) + approvalsWhitelistTeams := getWhitelistEntities(teamReaders, bp.ApprovalsWhitelistTeamIDs) + branchName := "" if !git_model.IsRuleNameSpecial(bp.RuleName) { branchName = bp.RuleName diff --git a/services/convert/user.go b/services/convert/user.go index 98db53705b..789bc51097 100644 --- a/services/convert/user.go +++ b/services/convert/user.go @@ -76,6 +76,7 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap if authed { result.IsAdmin = user.IsAdmin result.LoginName = user.LoginName + result.SourceID = user.LoginSource result.LastLogin = user.LastLoginUnix.AsTime() result.Language = user.Language result.IsActive = user.IsActive diff --git a/templates/devtest/label.tmpl b/templates/devtest/label.tmpl new file mode 100644 index 0000000000..c4b52a3e23 --- /dev/null +++ b/templates/devtest/label.tmpl @@ -0,0 +1,27 @@ +{{template "base/head" .}} + +