Merge pull request '[v1.22/gitea] week 2024-17 cherry pick v7.0' (#3354) from earl-warren/forgejo:wip-v7.0-gitea-cherry-pick into v7.0/forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3354
Reviewed-by: Gergely Nagy <algernon@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-04-22 09:55:54 +00:00
commit 4433cd9793
35 changed files with 284 additions and 159 deletions

View file

@ -66,6 +66,7 @@ package "code.gitea.io/gitea/models/migrations/base"
func MainTest func MainTest
package "code.gitea.io/gitea/models/organization" package "code.gitea.io/gitea/models/organization"
func GetTeamNamesByID
func UpdateTeamUnits func UpdateTeamUnits
func (SearchMembersOptions).ToConds func (SearchMembersOptions).ToConds
func UsersInTeamsCount func UsersInTeamsCount
@ -131,6 +132,7 @@ package "code.gitea.io/gitea/models/user"
func GetUserAllSettings func GetUserAllSettings
func DeleteUserSetting func DeleteUserSetting
func GetUserEmailsByNames func GetUserEmailsByNames
func GetUserNamesByIDs
package "code.gitea.io/gitea/modules/activitypub" package "code.gitea.io/gitea/modules/activitypub"
func CurrentTime 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 LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
ifeq ($(HAS_GO), yes) 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/)) 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 endif
@ -457,7 +456,7 @@ lint-go-windows:
.PHONY: lint-go-vet .PHONY: lint-go-vet
lint-go-vet: lint-go-vet:
@echo "Running go vet..." @echo "Running go vet..."
@$(GO) vet $(GO_PACKAGES) @$(GO) vet ./...
.PHONY: lint-editorconfig .PHONY: lint-editorconfig
lint-editorconfig: lint-editorconfig:
@ -823,7 +822,7 @@ generate-backend: $(TAGS_PREREQ) generate-go
.PHONY: generate-go .PHONY: generate-go
generate-go: $(TAGS_PREREQ) generate-go: $(TAGS_PREREQ)
@echo "Running go generate..." @echo "Running go generate..."
@CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' $(GO_PACKAGES) @CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' ./...
.PHONY: merge-locales .PHONY: merge-locales
merge-locales: merge-locales:

View file

@ -36,6 +36,7 @@ var microcmdUserChangePassword = &cli.Command{
&cli.BoolFlag{ &cli.BoolFlag{
Name: "must-change-password", Name: "must-change-password",
Usage: "User must change password", Usage: "User must change password",
Value: true,
}, },
}, },
} }
@ -57,23 +58,18 @@ func runChangePassword(c *cli.Context) error {
return err 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{ opts := &user_service.UpdateAuthOptions{
Password: optional.Some(c.String("password")), 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 { if err := user_service.UpdateAuth(ctx, user, opts); err != nil {
switch { switch {
case errors.Is(err, password.ErrMinLength): 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): 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): 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: default:
return err return err
} }

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
pwd "code.gitea.io/gitea/modules/auth/password" pwd "code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
@ -49,6 +50,7 @@ var microcmdUserCreate = &cli.Command{
Name: "must-change-password", Name: "must-change-password",
Usage: "Set this option to false to prevent forcing the user to change their password after initial login", Usage: "Set this option to false to prevent forcing the user to change their password after initial login",
Value: true, Value: true,
DisableDefaultText: true,
}, },
&cli.IntFlag{ &cli.IntFlag{
Name: "random-password-length", Name: "random-password-length",
@ -72,10 +74,10 @@ func runCreateUser(c *cli.Context) error {
} }
if c.IsSet("name") && c.IsSet("username") { 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") { 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") { 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") return errors.New("must set either password or random-password flag")
} }
changePassword := c.Bool("must-change-password") isAdmin := c.Bool("admin")
mustChangePassword := true // always default to true
// If this is the first user being created. if c.IsSet("must-change-password") {
// Take it as the admin and don't force a password update. // if the flag is set, use the value provided by the user
if n := user_model.CountUsers(ctx, nil); n == 0 { mustChangePassword = c.Bool("must-change-password")
changePassword = false } 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]() restricted := optional.None[bool]()
@ -132,8 +143,8 @@ func runCreateUser(c *cli.Context) error {
Name: username, Name: username,
Email: c.String("email"), Email: c.String("email"),
Passwd: password, Passwd: password,
IsAdmin: c.Bool("admin"), IsAdmin: isAdmin,
MustChangePassword: changePassword, MustChangePassword: mustChangePassword,
Visibility: visibility, Visibility: visibility,
} }

View file

@ -2387,22 +2387,6 @@ LEVEL = Info
;; Enable issue by repository metrics; default is false ;; Enable issue by repository metrics; default is false
;ENABLED_ISSUE_BY_REPOSITORY = 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] ;[migrations]

View file

@ -296,8 +296,8 @@ func MaxBatchInsertSize(bean any) int {
} }
// IsTableNotEmpty returns true if table has at least one record // IsTableNotEmpty returns true if table has at least one record
func IsTableNotEmpty(tableName string) (bool, error) { func IsTableNotEmpty(beanOrTableName any) (bool, error) {
return x.Table(tableName).Exist() return x.Table(beanOrTableName).Exist()
} }
// DeleteAllRecords will delete all the records of this table // 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 // SearchVersions gets all versions of packages matching the search options
func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
sess := db.GetEngine(ctx). sess := db.GetEngine(ctx).
Where(opts.ToConds()). Select("package_version.*").
Table("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) 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 // SearchLatestVersions gets the latest version of every package matching the search options
func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
cond := opts.ToConds(). in := builder.
And(builder.Expr("pv2.id IS NULL")) Select("MAX(package_version.id)").
From("package_version").
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))") InnerJoin("package", "package.id = package_version.package_id").
if opts.IsInternal.Has() { Where(opts.ToConds()).
joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()}) GroupBy("package_version.package_id")
}
sess := db.GetEngine(ctx). sess := db.GetEngine(ctx).
Select("package_version.*").
Table("package_version"). Table("package_version").
Join("LEFT", "package_version pv2", joinCond).
Join("INNER", "package", "package.id = package_version.package_id"). Join("INNER", "package", "package.id = package_version.package_id").
Where(cond) Where(builder.In("package_version.id", in))
opts.configureOrderBy(sess) opts.configureOrderBy(sess)

View file

@ -18,6 +18,12 @@ type Store interface {
// RegenerateSession regenerates the underlying session and returns the new store // RegenerateSession regenerates the underlying session and returns the new store
func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, error) { func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, error) {
for _, f := range BeforeRegenerateSession {
f(resp, req)
}
s, err := session.RegenerateSession(resp, req) s, err := session.RegenerateSession(resp, req)
return s, err 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

@ -9,6 +9,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
@ -45,10 +46,40 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
SameSite: setting.SessionConfig.SameSite, SameSite: setting.SessionConfig.SameSite,
} }
resp.Header().Add("Set-Cookie", cookie.String()) resp.Header().Add("Set-Cookie", cookie.String())
if maxAge < 0 { // Previous versions would use a cookie path with a trailing /.
// There was a bug in "setting.SessionConfig.CookiePath" code, the old default value of it was empty "". // These are more specific than cookies without a trailing /, so
// So we have to delete the cookie on path="" again, because some old code leaves cookies on path="". // we need to delete these if they exist.
cookie.Path = strings.TrimSuffix(setting.SessionConfig.CookiePath, "/") deleteLegacySiteCookie(resp, name)
}
// 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) {
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
// path could override other cookies, but it's complicated and we don't
// currently handle that.
return
}
cookie := &http.Cookie{
Name: name,
Value: "",
MaxAge: -1,
Path: setting.SessionConfig.CookiePath + "/",
Domain: setting.SessionConfig.Domain,
Secure: setting.SessionConfig.Secure,
HttpOnly: true,
SameSite: setting.SessionConfig.SameSite,
}
resp.Header().Add("Set-Cookie", cookie.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

@ -30,7 +30,7 @@ import (
user_service "code.gitea.io/gitea/services/user" 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 { if sourceID == 0 {
return return
} }
@ -47,7 +47,6 @@ func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64
u.LoginType = source.Type u.LoginType = source.Type
u.LoginSource = source.ID u.LoginSource = source.ID
u.LoginName = loginName
} }
// CreateUser create a user // CreateUser create a user
@ -83,12 +82,13 @@ func CreateUser(ctx *context.APIContext) {
Passwd: form.Password, Passwd: form.Password,
MustChangePassword: true, MustChangePassword: true,
LoginType: auth.Plain, LoginType: auth.Plain,
LoginName: form.LoginName,
} }
if form.MustChangePassword != nil { if form.MustChangePassword != nil {
u.MustChangePassword = *form.MustChangePassword u.MustChangePassword = *form.MustChangePassword
} }
parseAuthSource(ctx, u, form.SourceID, form.LoginName) parseAuthSource(ctx, u, form.SourceID)
if ctx.Written() { if ctx.Written() {
return return
} }

View file

@ -437,7 +437,7 @@ func GetBranchProtection(ctx *context.APIContext) {
return 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 // ListBranchProtections list branch protections for a repo
@ -470,7 +470,7 @@ func ListBranchProtections(ctx *context.APIContext) {
} }
apiBps := make([]*api.BranchProtection, len(bps)) apiBps := make([]*api.BranchProtection, len(bps))
for i := range 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) ctx.JSON(http.StatusOK, apiBps)
@ -682,7 +682,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
return 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 // EditBranchProtection edits a branch protection for a repo
@ -964,7 +964,7 @@ func EditBranchProtection(ctx *context.APIContext) {
return 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 // DeleteBranchProtection deletes a branch protection for a repo

View file

@ -258,7 +258,7 @@ func Routes() *web.Route {
routes.Get("/metrics", append(mid, Metrics)...) 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("/ssh_info", misc.SSHInfo)
routes.Get("/api/healthz", healthcheck.Check) 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 { func (input *notifyInput) WithDoer(doer *user_model.User) *notifyInput {
input.Doer = doer input.Doer = doer
return input return input
@ -562,7 +567,7 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository)
// We need a notifyInput to call handleSchedules // We need a notifyInput to call handleSchedules
// if repo is a mirror, commit author maybe an external user, // if repo is a mirror, commit author maybe an external user,
// so we use action user as the Doer of the notifyInput // 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) return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch)
} }

View file

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
session_module "code.gitea.io/gitea/modules/session"
chiSession "gitea.com/go-chi/session" chiSession "gitea.com/go-chi/session"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
@ -65,7 +66,7 @@ func (st *SessionsStore) Save(r *http.Request, w http.ResponseWriter, session *s
chiStore := chiSession.GetSession(r) chiStore := chiSession.GetSession(r)
if session.IsNew { if session.IsNew {
_, _ = chiSession.RegenerateSession(w, r) _, _ = session_module.RegenerateSession(w, r)
session.IsNew = false session.IsNew = false
} }

View file

@ -21,6 +21,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" 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/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs" 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 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 // ToBranchProtection convert a ProtectedBranch to api.BranchProtection
func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch) *api.BranchProtection { func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection {
pushWhitelistUsernames, err := user_model.GetUserNamesByIDs(ctx, bp.WhitelistUserIDs) readers, err := access_model.GetRepoReaders(ctx, repo)
if err != nil { 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 { if err != nil {
log.Error("GetUserNamesByIDs (MergeWhitelistUserIDs): %v", err) log.Error("Repo.Owner.TeamsWithAccessToRepo: %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)
} }
pushWhitelistTeams := getWhitelistEntities(teamReaders, bp.WhitelistTeamIDs)
mergeWhitelistTeams := getWhitelistEntities(teamReaders, bp.MergeWhitelistTeamIDs)
approvalsWhitelistTeams := getWhitelistEntities(teamReaders, bp.ApprovalsWhitelistTeamIDs)
branchName := "" branchName := ""
if !git_model.IsRuleNameSpecial(bp.RuleName) { if !git_model.IsRuleNameSpecial(bp.RuleName) {
branchName = bp.RuleName branchName = bp.RuleName

View file

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/automerge"
@ -25,12 +26,41 @@ func getCacheKey(repoID int64, brancheName string) string {
return fmt.Sprintf("commit_status:%x", hashBytes) return fmt.Sprintf("commit_status:%x", hashBytes)
} }
func updateCommitStatusCache(ctx context.Context, repoID int64, branchName string, status api.CommitStatusState) error { type commitStatusCacheValue struct {
c := cache.GetCache() State string `json:"state"`
return c.Put(getCacheKey(repoID, branchName), string(status), 3*24*60) TargetURL string `json:"target_url"`
} }
func deleteCommitStatusCache(ctx context.Context, repoID int64, branchName string) error { func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue {
c := cache.GetCache()
statusStr, ok := c.Get(getCacheKey(repoID, branchName)).(string)
if ok && statusStr != "" {
var cv commitStatusCacheValue
err := json.Unmarshal([]byte(statusStr), &cv)
if err == nil && cv.State != "" {
return &cv
}
if err != nil {
log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
}
}
return nil
}
func updateCommitStatusCache(repoID int64, branchName string, state api.CommitStatusState, targetURL string) error {
c := cache.GetCache()
bs, err := json.Marshal(commitStatusCacheValue{
State: state.String(),
TargetURL: targetURL,
})
if err != nil {
log.Warn("updateCommitStatusCache: json.Marshal failed: %v", err)
return nil
}
return c.Put(getCacheKey(repoID, branchName), string(bs), 3*24*60)
}
func deleteCommitStatusCache(repoID int64, branchName string) error {
c := cache.GetCache() c := cache.GetCache()
return c.Delete(getCacheKey(repoID, branchName)) return c.Delete(getCacheKey(repoID, branchName))
} }
@ -74,7 +104,7 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
} }
if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid
if err := deleteCommitStatusCache(ctx, repo.ID, repo.DefaultBranch); err != nil { if err := deleteCommitStatusCache(repo.ID, repo.DefaultBranch); err != nil {
log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
} }
} }
@ -91,12 +121,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
// FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache // FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache
func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) { func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
results := make([]*git_model.CommitStatus, len(repos)) results := make([]*git_model.CommitStatus, len(repos))
c := cache.GetCache()
for i, repo := range repos { for i, repo := range repos {
status, ok := c.Get(getCacheKey(repo.ID, repo.DefaultBranch)).(string) if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
if ok && status != "" { results[i] = &git_model.CommitStatus{
results[i] = &git_model.CommitStatus{State: api.CommitStatusState(status)} State: api.CommitStatusState(cv.State),
TargetURL: cv.TargetURL,
}
} }
} }
@ -123,8 +153,8 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
for i, repo := range repos { for i, repo := range repos {
if results[i] == nil { if results[i] == nil {
results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]) results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
if results[i] != nil { if results[i] != nil && results[i].State != "" {
if err := updateCommitStatusCache(ctx, repo.ID, repo.DefaultBranch, results[i].State); err != nil { if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
} }
} }

View file

@ -1,4 +1,4 @@
<div class="flex-list"> <div class="flex-list run-list">
{{if not .Runs}} {{if not .Runs}}
<div class="empty-placeholder"> <div class="empty-placeholder">
{{svg "octicon-no-entry" 48}} {{svg "octicon-no-entry" 48}}
@ -28,16 +28,16 @@
</div> </div>
<div class="flex-item-trailing"> <div class="flex-item-trailing">
{{if .RefLink}} {{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}} {{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}} {{end}}
</div>
<div class="run-list-item-right"> <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-calendar" 16}}{{TimeSinceUnix .Updated ctx.Locale}}</div>
<div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{.Duration}}</div> <div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{.Duration}}</div>
</div> </div>
</div> </div>
</div>
{{end}} {{end}}
</div> </div>
{{template "base/paginate" .}} {{template "base/paginate" .}}

View file

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

View file

@ -15,7 +15,7 @@
{{range $i, $v := .TreeNames}} {{range $i, $v := .TreeNames}}
<div class="breadcrumb-divider">/</div> <div class="breadcrumb-divider">/</div>
{{if eq $i $l}} {{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> <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
{{else}} {{else}}
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>

View file

@ -13,7 +13,7 @@
{{range $i, $v := .TreeNames}} {{range $i, $v := .TreeNames}}
<div class="breadcrumb-divider">/</div> <div class="breadcrumb-divider">/</div>
{{if eq $i $l}} {{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> <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
{{else}} {{else}}
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> <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"> <div class="content tw-text-left">
<form class="ui form form-fetch-action" action="{{printf "%s/issues/new" .Repository.Link}}" method="post"> <form class="ui form form-fetch-action" action="{{printf "%s/issues/new" .Repository.Link}}" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<div class="ui segment content">
<div class="field"> <div class="field">
<span class="text"><strong>{{ctx.Locale.Tr "repository"}}</strong></span> <label><strong>{{ctx.Locale.Tr "repository"}}</strong></label>
<div class="ui search normal selection dropdown issue_reference_repository_search"> <div class="ui search selection dropdown issue_reference_repository_search">
<div class="default text">{{.Repository.FullName}}</div> <div class="default text">{{.Repository.FullName}}</div>
<div class="menu"></div> <div class="menu"></div>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<span class="text"><strong>{{ctx.Locale.Tr "repo.milestones.title"}}</strong></span> <label><strong>{{ctx.Locale.Tr "repo.milestones.title"}}</strong></label>
<input name="title" value="" autofocus required maxlength="255" autocomplete="off"> <input name="title" value="" autofocus required maxlength="255" autocomplete="off">
</div> </div>
<div class="field"> <div class="field">
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.reference_issue.body"}}</strong></span> <label><strong>{{ctx.Locale.Tr "repo.issues.reference_issue.body"}}</strong></label>
<textarea name="content" class="form-control"></textarea> <textarea name="content" class="form-control"></textarea>
</div> </div>
<div class="text right"> <div class="text right">
<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.create"}}</button> <button class="ui primary button">{{ctx.Locale.Tr "repo.issues.create"}}</button>
</div> </div>
</div>
</form> </form>
</div> </div>
</div> </div>

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> <a class="ui primary tiny button" href="{{.LFSFilesLink}}/find?oid={{.LFSFile.Oid}}&size={{.LFSFile.Size}}">{{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}</a>
</div> </div>
</h4> </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" $}} {{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}}"> <div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}">
{{if .IsMarkup}} {{if .IsMarkup}}

View file

@ -11,7 +11,7 @@
{{end}} {{end}}
{{if not .ReadmeInList}} {{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"> <div class="latest-commit">
{{template "repo/latest_commit" .}} {{template "repo/latest_commit" .}}
</div> </div>
@ -93,7 +93,7 @@
{{end}} {{end}}
</div> </div>
</h4> </h4>
<div class="ui attached table unstackable segment"> <div class="ui bottom attached table unstackable segment">
{{if not (or .IsMarkup .IsRenderedHTML)}} {{if not (or .IsMarkup .IsRenderedHTML)}}
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
{{end}} {{end}}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -44,9 +44,10 @@
} }
.run-list-item-right { .run-list-item-right {
flex: 0 0 min(20%, 130px); width: 130px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-shrink: 0;
gap: 3px; gap: 3px;
color: var(--color-text-light); color: var(--color-text-light);
} }
@ -57,3 +58,26 @@
gap: .25rem; gap: .25rem;
align-items: center; 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

@ -496,6 +496,7 @@ ol.ui.list li,
.ui.selection.dropdown .menu > .item { .ui.selection.dropdown .menu > .item {
border-color: var(--color-secondary); border-color: var(--color-secondary);
white-space: nowrap;
} }
.ui.selection.visible.dropdown > .text:not(.default) { .ui.selection.visible.dropdown > .text:not(.default) {
@ -517,6 +518,12 @@ ol.ui.list li,
color: var(--color-text-light-2); 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 */ /* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */
.ui.dropdown > .text > .img { .ui.dropdown > .text > .img {
margin-left: 0; margin-left: 0;

View file

@ -18,7 +18,8 @@
width: auto; 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%; width: 60%;
} }

View file

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

View file

@ -435,7 +435,6 @@ td .commit-summary {
padding: 0 !important; padding: 0 !important;
} }
.non-diff-file-content .attached.segment,
.non-diff-file-content .pdfobject { .non-diff-file-content .pdfobject {
border-radius: 0 0 var(--border-radius) var(--border-radius); border-radius: 0 0 var(--border-radius) var(--border-radius);
} }
@ -1083,6 +1082,12 @@ td .commit-summary {
margin-left: 15px; 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 { .repository.view.issue .comment-list .event .segments {
box-shadow: none; box-shadow: none;
} }
@ -2518,6 +2523,7 @@ tbody.commit-list {
.author-wrapper { .author-wrapper {
max-width: 180px; max-width: 180px;
align-self: center; align-self: center;
white-space: nowrap;
} }
/* in the commit list, messages can wrap so we can use inline */ /* 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"> <button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
{{ locale.cancel }} {{ locale.cancel }}
</button> </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 }} {{ locale.rerun_all }}
</button> </button>
</div> </div>
@ -391,8 +391,8 @@ export function initRepositoryActionView() {
<a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a> <a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a>
{{ run.commit.localePushedBy }} {{ run.commit.localePushedBy }}
<a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a> <a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a>
<span class="ui label" v-if="run.commit.shortSHA"> <span class="ui label tw-max-w-full" v-if="run.commit.shortSHA">
<a :href="run.commit.branch.link">{{ run.commit.branch.name }}</a> <a class="gt-ellipsis" :href="run.commit.branch.link">{{ run.commit.branch.name }}</a>
</span> </span>
</div> </div>
<div class="action-summary"> <div class="action-summary">
@ -435,8 +435,8 @@ export function initRepositoryActionView() {
<div class="action-view-right"> <div class="action-view-right">
<div class="job-info-header"> <div class="job-info-header">
<div class="job-info-header-left"> <div class="job-info-header-left gt-ellipsis">
<h3 class="job-info-header-title"> <h3 class="job-info-header-title gt-ellipsis">
{{ currentJob.title }} {{ currentJob.title }}
</h3> </h3>
<p class="job-info-header-detail"> <p class="job-info-header-detail">
@ -512,6 +512,7 @@ export function initRepositoryActionView() {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px;
} }
.action-info-summary-title { .action-info-summary-title {
@ -522,6 +523,7 @@ export function initRepositoryActionView() {
font-size: 20px; font-size: 20px;
margin: 0 0 0 8px; margin: 0 0 0 8px;
flex: 1; flex: 1;
overflow-wrap: anywhere;
} }
.action-summary { .action-summary {
@ -737,6 +739,10 @@ export function initRepositoryActionView() {
font-size: 12px; font-size: 12px;
} }
.job-info-header-left {
flex: 1;
}
.job-step-container { .job-step-container {
max-height: 100%; max-height: 100%;
border-radius: 0 0 var(--border-radius) var(--border-radius); border-radius: 0 0 var(--border-radius) var(--border-radius);

View file

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