Merge pull request '[gitea] week 2024-17 cherry pick (gitea/main -> forgejo)' (#3350) from algernon/wcp/2024-17 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3350
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-04-22 18:48:13 +00:00
commit b63b224c36
60 changed files with 638 additions and 222 deletions

View file

@ -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"]

View file

@ -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

View file

@ -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:

View file

@ -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
}

View file

@ -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,
}

View file

@ -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]

View file

@ -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 {

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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.
}

View file

@ -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

View file

@ -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)
})
}

View file

@ -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.

View file

@ -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

7
options/license/pkgconf Normal file
View file

@ -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.

View file

@ -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
}

View file

@ -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)

View file

@ -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

View file

@ -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,
})
}

View file

@ -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"`
}

21
routers/common/compare.go Normal file
View file

@ -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
}

View file

@ -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)

View file

@ -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 (

View file

@ -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)

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,27 @@
{{template "base/head" .}}
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}">
<div class="page-content devtest ui container">
<div>
<h1>Label</h1>
<div class="flex-text-block tw-my-2">
<span class="ui label">simple label</span>
<span class="ui red label">red label</span>
<span class="ui green label">green label</span>
</div>
<div class="flex-text-block tw-my-2">
<span class="ui basic label">basic label</span>
<span class="ui basic red label">basic red label</span>
<span class="ui basic green label">basic green label</span>
</div>
<div class="flex-text-block tw-my-2">
<span class="ui label">long content must be in a non-flex "gt-ellipsis" element, otherwise it won't get ellipsis. very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong label</span>
</div>
<div class="flex-text-block tw-my-2">
<span class="ui label"><span class="gt-ellipsis">very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong label</span></span>
</div>
<div class="tw-my-2">
<span class="ui label tw-max-w-full"><span class="gt-ellipsis">very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong label</span></span>
</div>
</div>
</div>
{{template "base/footer" .}}

View file

@ -1,4 +1,4 @@
<div class="flex-list">
<div class="flex-list run-list">
{{if not .Runs}}
<div class="empty-placeholder">
{{svg "octicon-no-entry" 48}}
@ -28,14 +28,14 @@
</div>
<div class="flex-item-trailing">
{{if .RefLink}}
<a class="ui label tw-px-1 tw-mx-0" href="{{.RefLink}}">{{.PrettyRef}}</a>
<a class="ui label run-list-ref gt-ellipsis" href="{{.RefLink}}">{{.PrettyRef}}</a>
{{else}}
<span class="ui label tw-px-1 tw-mx-0">{{.PrettyRef}}</span>
<span class="ui label run-list-ref gt-ellipsis">{{.PrettyRef}}</span>
{{end}}
</div>
<div class="run-list-item-right">
<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{TimeSinceUnix .Updated ctx.Locale}}</div>
<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{.Duration}}</div>
<div class="run-list-item-right">
<div class="run-list-meta">{{svg "octicon-calendar" 16}}{{TimeSinceUnix .Updated ctx.Locale}}</div>
<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{.Duration}}</div>
</div>
</div>
</div>
{{end}}

View file

@ -28,7 +28,7 @@
</div>
</div>
</h4>
<div class="ui attached table unstackable segment">
<div class="ui bottom attached table unstackable segment">
<div class="file-view code-view unicode-escaped">
{{if .IsFileTooLarge}}
<table>

View file

@ -6,14 +6,23 @@
<div class="singular-commit" id="{{$tag}}">
<span class="badge badge-commit">{{svg "octicon-git-commit"}}</span>
{{if .User}}
<a class="avatar" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User}}</a>
<a class="avatar" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}}</a>
{{else}}
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name}}
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}}
{{end}}
{{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}}
<span class="shabox tw-flex tw-items-center tw-float-right">
<span class="tw-flex-1 gt-ellipsis tw-font-mono{{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span>
{{if IsMultilineCommitMessage .Message}}
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
{{end}}
{{if IsMultilineCommitMessage .Message}}
<pre class="commit-body tw-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre>
{{end}}
<span class="shabox tw-flex tw-items-center">
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
{{$class := "ui sha label"}}
{{if .Signature}}
@ -37,14 +46,6 @@
{{end}}
</a>
</span>
<span class="tw-font-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span>
{{if IsMultilineCommitMessage .Message}}
<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
{{end}}
{{if IsMultilineCommitMessage .Message}}
<pre class="commit-body tw-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre>
{{end}}
</div>
{{end}}
</div>

View file

@ -15,7 +15,7 @@
{{range $i, $v := .TreeNames}}
<div class="breadcrumb-divider">/</div>
{{if eq $i $l}}
<input id="file-name" maxlength="500" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
<input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
{{else}}
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>

View file

@ -13,7 +13,7 @@
{{range $i, $v := .TreeNames}}
<div class="breadcrumb-divider">/</div>
{{if eq $i $l}}
<input type="text" id="file-name" maxlength="500" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus>
<input type="text" id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus>
<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
{{else}}
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>

View file

@ -5,26 +5,24 @@
<div class="content tw-text-left">
<form class="ui form form-fetch-action" action="{{printf "%s/issues/new" .Repository.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="ui segment content">
<div class="field">
<span class="text"><strong>{{ctx.Locale.Tr "repository"}}</strong></span>
<div class="ui search normal selection dropdown issue_reference_repository_search">
<div class="default text">{{.Repository.FullName}}</div>
<div class="menu"></div>
</div>
</div>
<div class="field">
<span class="text"><strong>{{ctx.Locale.Tr "repo.milestones.title"}}</strong></span>
<input name="title" value="" autofocus required maxlength="255" autocomplete="off">
</div>
<div class="field">
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.reference_issue.body"}}</strong></span>
<textarea name="content" class="form-control"></textarea>
</div>
<div class="text right">
<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.create"}}</button>
<div class="field">
<label><strong>{{ctx.Locale.Tr "repository"}}</strong></label>
<div class="ui search selection dropdown issue_reference_repository_search">
<div class="default text">{{.Repository.FullName}}</div>
<div class="menu"></div>
</div>
</div>
<div class="field">
<label><strong>{{ctx.Locale.Tr "repo.milestones.title"}}</strong></label>
<input name="title" value="" autofocus required maxlength="255" autocomplete="off">
</div>
<div class="field">
<label><strong>{{ctx.Locale.Tr "repo.issues.reference_issue.body"}}</strong></label>
<textarea name="content" class="form-control"></textarea>
</div>
<div class="text right">
<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.create"}}</button>
</div>
</form>
</div>
</div>

View file

@ -25,12 +25,14 @@
<div class="column">
{{if gt .Activity.ActivePRCount 0}}
<div class="stats-table">
<a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}{{if ne .Activity.MergedPRPerc 0}}%{{end}}"></a>
<a href="#proposed-pull-requests" class="table-cell tiny background green"></a>
{{if gt .Activity.MergedPRPerc 0}}
<a href="#merged-pull-requests" class="table-cell tiny tw-bg-purple" style="width: {{.Activity.MergedPRPerc}}%"></a>
{{end}}
<a href="#proposed-pull-requests" class="table-cell tiny tw-bg-green"></a>
</div>
{{else}}
<div class="stats-table">
<a class="table-cell tiny background light grey"></a>
<a class="table-cell tiny tw-bg-grey"></a>
</div>
{{end}}
{{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount}}
@ -40,8 +42,10 @@
<div class="column">
{{if gt .Activity.ActiveIssueCount 0}}
<div class="stats-table">
<a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}{{if ne .Activity.ClosedIssuePerc 0}}%{{end}}"></a>
<a href="#new-issues" class="table-cell tiny background green"></a>
{{if gt .Activity.ClosedIssuePerc 0}}
<a href="#closed-issues" class="table-cell tiny tw-bg-red" style="width: {{.Activity.ClosedIssuePerc}}%"></a>
{{end}}
<a href="#new-issues" class="table-cell tiny tw-bg-green"></a>
</div>
{{else}}
<div class="stats-table">
@ -108,7 +112,7 @@
{{end}}
{{if gt .Activity.PublishedReleaseCount 0}}
<h4 class="divider divider-text tw-normal-case" id="published-releases">
<h4 class="divider divider-text" id="published-releases">
{{svg "octicon-tag" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.activity.title.releases_published_by"
(ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
@ -130,7 +134,7 @@
{{end}}
{{if gt .Activity.MergedPRCount 0}}
<h4 class="divider divider-text tw-normal-case" id="merged-pull-requests">
<h4 class="divider divider-text" id="merged-pull-requests">
{{svg "octicon-git-pull-request" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
(ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
@ -149,7 +153,7 @@
{{end}}
{{if gt .Activity.OpenedPRCount 0}}
<h4 class="divider divider-text tw-normal-case" id="proposed-pull-requests">
<h4 class="divider divider-text" id="proposed-pull-requests">
{{svg "octicon-git-branch" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
(ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
@ -168,7 +172,7 @@
{{end}}
{{if gt .Activity.ClosedIssueCount 0}}
<h4 class="divider divider-text tw-normal-case" id="closed-issues">
<h4 class="divider divider-text" id="closed-issues">
{{svg "octicon-issue-closed" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
(ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
@ -187,7 +191,7 @@
{{end}}
{{if gt .Activity.OpenedIssueCount 0}}
<h4 class="divider divider-text tw-normal-case" id="new-issues">
<h4 class="divider divider-text" id="new-issues">
{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.activity.title.issues_created_by"
(ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
@ -206,7 +210,7 @@
{{end}}
{{if gt .Activity.UnresolvedIssueCount 0}}
<h4 class="divider divider-text tw-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
<h4 class="divider divider-text" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
{{svg "octicon-comment-discussion" 16 "tw-mr-2"}}
{{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
</h4>

View file

@ -11,7 +11,7 @@
<a class="ui primary tiny button" href="{{.LFSFilesLink}}/find?oid={{.LFSFile.Oid}}&size={{.LFSFile.Size}}">{{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}</a>
</div>
</h4>
<div class="ui attached table unstackable segment">
<div class="ui bottom attached table unstackable segment">
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}">
{{if .IsMarkup}}

View file

@ -11,7 +11,7 @@
{{end}}
{{if not .ReadmeInList}}
<div id="repo-file-commit-box" class="ui top attached header list-header tw-mb-4 tw-flex tw-justify-between">
<div id="repo-file-commit-box" class="ui segment list-header tw-mb-4 tw-flex tw-justify-between">
<div class="latest-commit">
{{template "repo/latest_commit" .}}
</div>
@ -93,7 +93,7 @@
{{end}}
</div>
</h4>
<div class="ui attached table unstackable segment">
<div class="ui bottom attached table unstackable segment">
{{if not (or .IsMarkup .IsRenderedHTML)}}
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
{{end}}

View file

@ -5210,6 +5210,51 @@
}
}
},
"/repos/{owner}/{repo}/compare/{basehead}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Get",
"commit",
"comparison"
],
"summary": "Get commit comparison information",
"operationId": "information",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "compare two branches or commits",
"name": "basehead",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/Compare"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/contents": {
"get": {
"produces": [
@ -19017,6 +19062,25 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"Compare": {
"type": "object",
"title": "Compare represents a comparison between two commits.",
"properties": {
"commits": {
"type": "array",
"items": {
"$ref": "#/definitions/Commit"
},
"x-go-name": "Commits"
},
"total_commits": {
"type": "integer",
"format": "int64",
"x-go-name": "TotalCommits"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"ContentsResponse": {
"description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content",
"type": "object",
@ -24608,6 +24672,12 @@
"type": "boolean",
"x-go-name": "Restricted"
},
"source_id": {
"description": "The ID of the user's Authentication Source",
"type": "integer",
"format": "int64",
"x-go-name": "SourceID"
},
"starred_repos_count": {
"type": "integer",
"format": "int64",
@ -25088,6 +25158,12 @@
}
}
},
"Compare": {
"description": "",
"schema": {
"$ref": "#/definitions/Compare"
}
},
"ContentsListResponse": {
"description": "ContentsListResponse",
"schema": {

View file

@ -1,15 +1,13 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content dashboard feeds">
{{template "user/dashboard/navbar" .}}
<div class="ui container">
<div class="ui container flex-container">
{{template "base/alert" .}}
<div class="ui mobile reversed stackable grid">
<div class="ui container ten wide column">
{{template "user/heatmap" .}}
{{template "user/dashboard/feeds" .}}
</div>
{{template "user/dashboard/repolist" .}}
<div class="flex-container-main">
{{template "user/heatmap" .}}
{{template "user/dashboard/feeds" .}}
</div>
{{template "user/dashboard/repolist" .}}
</div>
</div>
{{template "base/footer" .}}

View file

@ -52,4 +52,4 @@ data.organizationId = {{.ContextUser.ID}};
window.config.pageData.dashboardRepoList = data;
</script>
<div id="dashboard-repo-list" class="six wide column"></div>
<div id="dashboard-repo-list" class="flex-container-sidebar"></div>

View file

@ -1,6 +1,5 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// SPDX-License-Identifier: MIT
package integration

View file

@ -1,6 +1,5 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// SPDX-License-Identifier: MIT
package integration

View file

@ -1,6 +1,5 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// SPDX-License-Identifier: MIT
package integration

View file

@ -0,0 +1,38 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPICompareBranches(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Login as User2.
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
repoName := "repo20"
req := NewRequestf(t, "GET", "/api/v1/repos/user2/%s/compare/add-csv...remove-files-b", repoName).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var apiResp *api.Compare
DecodeJSON(t, resp, &apiResp)
assert.Equal(t, 2, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 2)
}

View file

@ -1,6 +1,5 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// SPDX-License-Identifier: MIT
package integration

View file

@ -1,3 +1,6 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (

View file

@ -44,9 +44,10 @@
}
.run-list-item-right {
flex: 0 0 min(20%, 130px);
width: 130px;
display: flex;
flex-direction: column;
flex-shrink: 0;
gap: 3px;
color: var(--color-text-light);
}
@ -57,3 +58,26 @@
gap: .25rem;
align-items: center;
}
.run-list .flex-item-trailing {
flex-wrap: nowrap;
width: 280px;
flex: 0 0 280px;
}
.run-list-ref {
display: inline-block !important;
}
@media (max-width: 767.98px) {
.run-list .flex-item-trailing {
flex-direction: column;
align-items: flex-end;
width: auto;
flex-basis: auto;
}
.run-list-item-right,
.run-list-ref {
max-width: 110px;
}
}

View file

@ -453,6 +453,7 @@ a.label,
.ui.selection.dropdown .menu > .item {
border-color: var(--color-secondary);
white-space: nowrap;
}
.ui.selection.visible.dropdown > .text:not(.default) {
@ -474,6 +475,12 @@ a.label,
color: var(--color-text-light-2);
}
.ui.dropdown > .text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */
.ui.dropdown > .text > .img {
margin-left: 0;
@ -1562,6 +1569,7 @@ table th[data-sortt-desc] .svg {
align-items: center;
gap: .25rem;
vertical-align: middle;
min-width: 0;
}
.ui.ui.button {
@ -1582,4 +1590,5 @@ table th[data-sortt-desc] .svg {
display: flex;
align-items: center;
gap: .25rem;
min-width: 0;
}

View file

@ -7,7 +7,6 @@
.dashboard.feeds .context.user.menu .ui.header,
.dashboard.issues .context.user.menu .ui.header {
font-size: 1rem;
text-transform: none;
}
.dashboard.feeds .filter.menu,

View file

@ -18,7 +18,8 @@
width: auto;
}
.page-content.install form.ui.form input {
.page-content.install form.ui.form input:not([type="checkbox"],[type="radio"]),
.page-content.install form.ui.form .ui.selection.dropdown {
width: 60%;
}

View file

@ -66,7 +66,7 @@ input[type="radio"] {
}
.ui.toggle.checkbox input {
width: 3.5rem;
height: 1.5rem;
height: 21px;
opacity: 0;
z-index: 3;
}
@ -81,29 +81,30 @@ input[type="radio"] {
content: "";
z-index: 1;
top: 0;
width: 3.5rem;
height: 1.5rem;
width: 49px;
height: 21px;
border-radius: 500rem;
left: 0;
}
.ui.toggle.checkbox label::after {
background: var(--color-white);
box-shadow: 1px 1px 4px 1px var(--color-shadow);
position: absolute;
content: "";
opacity: 1;
z-index: 2;
width: 1.5rem;
height: 1.5rem;
top: 0;
left: 0;
width: 18px;
height: 18px;
top: 1.5px;
left: 1.5px;
border-radius: 500rem;
transition: background 0.3s ease, left 0.3s ease;
}
.ui.toggle.checkbox input ~ label::after {
left: -0.05rem;
left: 1.5px;
}
.ui.toggle.checkbox input:checked ~ label::after {
left: 2.15rem;
left: 29px;
}
.ui.toggle.checkbox input:focus ~ label::before,
.ui.toggle.checkbox label::before {

View file

@ -2,12 +2,16 @@
margin: 10px 0;
height: 0;
font-weight: var(--font-weight-medium);
text-transform: uppercase;
color: var(--color-text);
font-size: 1rem;
width: 100%;
}
h4.divider {
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
.divider:not(.divider-text) {
border-top: 1px solid var(--color-secondary);
}

View file

@ -6,10 +6,16 @@
margin-top: var(--page-spacing);
}
/* small options menu on the left, used in settings/admin pages */
.flex-container-nav {
width: 240px;
}
/* wide sidebar on the right, used in frontpage */
.flex-container-sidebar {
width: 35%;
}
.flex-container-main {
flex: 1;
min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */
@ -19,7 +25,9 @@
.flex-container {
flex-direction: column;
}
.flex-container-nav {
.flex-container-nav,
.flex-container-sidebar {
order: -1;
width: auto;
}
}

View file

@ -9,7 +9,6 @@
font-family: var(--fonts-regular);
font-weight: var(--font-weight-medium);
line-height: 1.28571429;
text-transform: none;
}
.ui.header:first-child {

View file

@ -5,12 +5,12 @@
display: inline-flex;
align-items: center;
gap: .25rem;
min-width: 0;
vertical-align: middle;
line-height: 1;
background: var(--color-label-bg);
color: var(--color-label-text);
padding: 0.3em 0.5em;
text-transform: none;
font-size: 0.85714286rem;
font-weight: var(--font-weight-medium);
border: 0 solid transparent;

View file

@ -435,7 +435,6 @@ td .commit-summary {
padding: 0 !important;
}
.non-diff-file-content .attached.segment,
.non-diff-file-content .pdfobject {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
@ -836,55 +835,53 @@ td .commit-summary {
margin-right: 0.25em;
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit {
line-height: 34px; /* this must be same as .badge height, to avoid overflow */
clear: both; /* reset the "float right shabox", in the future, use flexbox instead */
.singular-commit {
display: flex;
align-items: center;
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit > img.avatar,
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit > .avatar img {
position: relative;
top: -2px;
.singular-commit .badge {
height: 30px !important;
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label {
.singular-commit .shabox .sha.label {
margin: 0;
border: 1px solid var(--color-light-border);
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isWarning {
.singular-commit .shabox .sha.label.isSigned.isWarning {
border: 1px solid var(--color-red-badge);
background: var(--color-red-badge-bg);
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isWarning:hover {
.singular-commit .shabox .sha.label.isSigned.isWarning:hover {
background: var(--color-red-badge-hover-bg) !important;
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerified {
.singular-commit .shabox .sha.label.isSigned.isVerified {
border: 1px solid var(--color-green-badge);
background: var(--color-green-badge-bg);
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerified:hover {
.singular-commit .shabox .sha.label.isSigned.isVerified:hover {
background: var(--color-green-badge-hover-bg) !important;
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted {
.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted {
border: 1px solid var(--color-yellow-badge);
background: var(--color-yellow-badge-bg);
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover {
.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover {
background: var(--color-yellow-badge-hover-bg) !important;
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched {
.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched {
border: 1px solid var(--color-orange-badge);
background: var(--color-orange-badge-bg);
}
.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover {
.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover {
background: var(--color-orange-badge-hover-bg) !important;
}
@ -1079,6 +1076,12 @@ td .commit-summary {
margin-left: 15px;
}
.repository.view.issue .comment-list .event .detail .text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.repository.view.issue .comment-list .event .segments {
box-shadow: none;
}
@ -2339,6 +2342,8 @@ td .commit-summary {
.stats-table {
display: table;
width: 100%;
margin: 6px 0;
border-spacing: 2px;
}
.stats-table .table-cell {
@ -2346,7 +2351,17 @@ td .commit-summary {
}
.stats-table .table-cell.tiny {
height: 0.5em;
height: 8px;
}
.stats-table .table-cell:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.stats-table .table-cell:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.labels-list {
@ -2429,6 +2444,7 @@ tbody.commit-list {
.author-wrapper {
max-width: 180px;
align-self: center;
white-space: nowrap;
}
/* in the commit list, messages can wrap so we can use inline */

View file

@ -382,7 +382,7 @@ export function initRepositoryActionView() {
<button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
{{ locale.cancel }}
</button>
<button class="ui basic small compact button tw-mr-0 link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
<button class="ui basic small compact button tw-mr-0 tw-whitespace-nowrap link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
{{ locale.rerun_all }}
</button>
</div>
@ -391,8 +391,8 @@ export function initRepositoryActionView() {
<a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a>
{{ run.commit.localePushedBy }}
<a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a>
<span class="ui label" v-if="run.commit.shortSHA">
<a :href="run.commit.branch.link">{{ run.commit.branch.name }}</a>
<span class="ui label tw-max-w-full" v-if="run.commit.shortSHA">
<a class="gt-ellipsis" :href="run.commit.branch.link">{{ run.commit.branch.name }}</a>
</span>
</div>
<div class="action-summary">
@ -435,8 +435,8 @@ export function initRepositoryActionView() {
<div class="action-view-right">
<div class="job-info-header">
<div class="job-info-header-left">
<h3 class="job-info-header-title">
<div class="job-info-header-left gt-ellipsis">
<h3 class="job-info-header-title gt-ellipsis">
{{ currentJob.title }}
</h3>
<p class="job-info-header-detail">
@ -512,6 +512,7 @@ export function initRepositoryActionView() {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.action-info-summary-title {
@ -522,6 +523,7 @@ export function initRepositoryActionView() {
font-size: 20px;
margin: 0 0 0 8px;
flex: 1;
overflow-wrap: anywhere;
}
.action-summary {
@ -737,6 +739,10 @@ export function initRepositoryActionView() {
font-size: 12px;
}
.job-info-header-left {
flex: 1;
}
.job-step-container {
max-height: 100%;
border-radius: 0 0 var(--border-radius) var(--border-radius);

View file

@ -112,6 +112,10 @@ export async function createMonaco(textarea, filename, editorOpts) {
...other,
});
monaco.editor.addKeybindingRules([
{keybinding: monaco.KeyCode.Enter, command: null}, // disable enter from accepting code completion
]);
const model = editor.getModel();
model.onDidChangeContent(() => {
textarea.value = editor.getValue({preserveBOM: true});