diff --git a/models/repo/repo.go b/models/repo/repo.go
index 7b7f5adb41..25207cc28b 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -422,52 +422,37 @@ func (repo *Repository) UnitEnabled(ctx context.Context, tp unit.Type) bool {
return false
}
-// MustGetUnit always returns a RepoUnit object
+// MustGetUnit always returns a RepoUnit object even if the unit doesn't exist (not enabled)
func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit {
ru, err := repo.GetUnit(ctx, tp)
if err == nil {
return ru
}
-
+ if !errors.Is(err, util.ErrNotExist) {
+ setting.PanicInDevOrTesting("Failed to get unit %v for repository %d: %v", tp, repo.ID, err)
+ }
+ ru = &RepoUnit{RepoID: repo.ID, Type: tp}
switch tp {
case unit.TypeExternalWiki:
- return &RepoUnit{
- Type: tp,
- Config: new(ExternalWikiConfig),
- }
+ ru.Config = new(ExternalWikiConfig)
case unit.TypeExternalTracker:
- return &RepoUnit{
- Type: tp,
- Config: new(ExternalTrackerConfig),
- }
+ ru.Config = new(ExternalTrackerConfig)
case unit.TypePullRequests:
- return &RepoUnit{
- Type: tp,
- Config: new(PullRequestsConfig),
- }
+ ru.Config = new(PullRequestsConfig)
case unit.TypeIssues:
- return &RepoUnit{
- Type: tp,
- Config: new(IssuesConfig),
- }
+ ru.Config = new(IssuesConfig)
case unit.TypeActions:
- return &RepoUnit{
- Type: tp,
- Config: new(ActionsConfig),
- }
+ ru.Config = new(ActionsConfig)
case unit.TypeProjects:
- cfg := new(ProjectsConfig)
- cfg.ProjectsMode = ProjectsModeNone
- return &RepoUnit{
- Type: tp,
- Config: cfg,
+ ru.Config = new(ProjectsConfig)
+ default: // other units don't have config
+ }
+ if ru.Config != nil {
+ if err = ru.Config.FromDB(nil); err != nil {
+ setting.PanicInDevOrTesting("Failed to load default config for unit %v of repository %d: %v", tp, repo.ID, err)
}
}
-
- return &RepoUnit{
- Type: tp,
- Config: new(UnitConfig),
- }
+ return ru
}
// GetUnit returns a RepoUnit object
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index d03d5e1e6a..491e96770c 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -134,10 +134,25 @@ type PullRequestsConfig struct {
DefaultTargetBranch string
}
+func DefaultPullRequestsConfig() *PullRequestsConfig {
+ cfg := &PullRequestsConfig{
+ AllowMerge: true,
+ AllowRebase: true,
+ AllowRebaseMerge: true,
+ AllowSquash: true,
+ AllowFastForwardOnly: true,
+ AllowRebaseUpdate: true,
+ DefaultAllowMaintainerEdit: true,
+ }
+ cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
+ cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge)
+ return cfg
+}
+
// FromDB fills up a PullRequestsConfig from serialized format.
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
- // AllowRebaseUpdate = true as default for existing PullRequestConfig in DB
- cfg.AllowRebaseUpdate = true
+ // set default values for existing PullRequestConfig in DB
+ *cfg = *DefaultPullRequestsConfig()
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
@@ -156,17 +171,8 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
}
-// GetDefaultMergeStyle returns the default merge style for this pull request
-func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle {
- if len(cfg.DefaultMergeStyle) != 0 {
- return cfg.DefaultMergeStyle
- }
-
- if setting.Repository.PullRequest.DefaultMergeStyle != "" {
- return MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
- }
-
- return MergeStyleMerge
+func DefaultPullRequestsUnit(repoID int64) RepoUnit {
+ return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()}
}
type ActionsConfig struct {
@@ -241,6 +247,8 @@ type ProjectsConfig struct {
// FromDB fills up a ProjectsConfig from serialized format.
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
+ // TODO: remove GetProjectsMode, only use ProjectsMode
+ cfg.ProjectsMode = ProjectsModeAll
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}
diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go
index 08cf964bc8..e15a64b01e 100644
--- a/models/repo/user_repo.go
+++ b/models/repo/user_repo.go
@@ -147,19 +147,21 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
}
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
-// If isShowFullName is set to true, also include full name prefix search
-func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {
+// It searches with the "user.name" and "user.full_name" fields case-insensitively.
+func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string) ([]*user_model.User, error) {
users := make([]*user_model.User, 0, 30)
- var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"}
- if search != "" && isShowFullName {
- prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%"))
- }
cond := builder.In("`user`.id",
builder.Select("poster_id").From("issue").Where(
builder.Eq{"repo_id": repo.ID}.
And(builder.Eq{"is_pull": isPull}),
- ).GroupBy("poster_id")).And(prefixCond)
+ ).GroupBy("poster_id"))
+
+ if search != "" {
+ var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"}
+ prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%"))
+ cond = cond.And(prefixCond)
+ }
return users, db.GetEngine(ctx).
Where(cond).
diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go
index a53cf39dc4..cd8a0f1a1f 100644
--- a/models/repo/user_repo_test.go
+++ b/models/repo/user_repo_test.go
@@ -44,12 +44,12 @@ func TestGetIssuePostersWithSearch(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
- users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER", false /* full name */)
+ users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER")
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, "user2", users[0].Name)
- users, err = repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "TW%O", true /* full name */)
+ users, err = repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "TW%O")
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, "user2", users[0].Name)
diff --git a/models/user/user.go b/models/user/user.go
index d8f41b869e..a74662bb12 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -8,6 +8,7 @@ import (
"context"
"encoding/hex"
"fmt"
+ "html/template"
"mime"
"net/mail"
"net/url"
@@ -28,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
@@ -417,16 +419,6 @@ func (u *User) IsTokenAccessAllowed() bool {
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
}
-// DisplayName returns full name if it's not empty,
-// returns username otherwise.
-func (u *User) DisplayName() string {
- trimmed := strings.TrimSpace(u.FullName)
- if len(trimmed) > 0 {
- return trimmed
- }
- return u.Name
-}
-
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
func (u *User) EmailTo() string {
sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())
@@ -445,27 +437,45 @@ func (u *User) EmailTo() string {
return fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("utf-8", add.Name), add.Address)
}
-// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,
-// returns username otherwise.
+// TODO: DefaultShowFullName causes messy logic, there are already too many methods to display a user's "display name", need to refactor them
+// * user.Name / user.FullName: directly used in templates
+// * user.DisplayName(): always show FullName if it's not empty, otherwise show Name
+// * user.GetDisplayName(): show FullName if it's not empty and DefaultShowFullName is set, otherwise show Name
+// * user.ShortName(): used a lot in templates, but it should be removed and let frontend use "ellipsis" styles
+// * activity action.ShortActUserName/GetActDisplayName/GetActDisplayNameTitle, etc: duplicate and messy
+
+// DisplayName returns full name if it's not empty, returns username otherwise.
+func (u *User) DisplayName() string {
+ fullName := strings.TrimSpace(u.FullName)
+ if fullName != "" {
+ return fullName
+ }
+ return u.Name
+}
+
+// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set, otherwise, username.
func (u *User) GetDisplayName() string {
if setting.UI.DefaultShowFullName {
- trimmed := strings.TrimSpace(u.FullName)
- if len(trimmed) > 0 {
- return trimmed
+ fullName := strings.TrimSpace(u.FullName)
+ if fullName != "" {
+ return fullName
}
}
return u.Name
}
-// GetCompleteName returns the full name and username in the form of
-// "Full Name (username)" if full name is not empty, otherwise it returns
-// "username".
-func (u *User) GetCompleteName() string {
- trimmedFullName := strings.TrimSpace(u.FullName)
- if len(trimmedFullName) > 0 {
- return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name)
+// ShortName ellipses username to length (still used by many templates), it calls GetDisplayName and respects DEFAULT_SHOW_FULL_NAME
+func (u *User) ShortName(length int) string {
+ return util.EllipsisDisplayString(u.GetDisplayName(), length)
+}
+
+func (u *User) GetShortDisplayNameLinkHTML() template.HTML {
+ fullName := strings.TrimSpace(u.FullName)
+ displayName, displayTooltip := u.Name, fullName
+ if setting.UI.DefaultShowFullName && fullName != "" {
+ displayName, displayTooltip = fullName, u.Name
}
- return u.Name
+ return htmlutil.HTMLFormat(`%s`, u.HomeLink(), displayTooltip, displayName)
}
func gitSafeName(name string) string {
@@ -488,14 +498,6 @@ func (u *User) GitName() string {
return fmt.Sprintf("user-%d", u.ID)
}
-// ShortName ellipses username to length
-func (u *User) ShortName(length int) string {
- if setting.UI.DefaultShowFullName && len(u.FullName) > 0 {
- return util.EllipsisDisplayString(u.FullName, length)
- }
- return util.EllipsisDisplayString(u.Name, length)
-}
-
// IsMailable checks if a user is eligible to receive emails.
// System users like Ghost and Gitea Actions are excluded.
func (u *User) IsMailable() bool {
diff --git a/modules/git/grep.go b/modules/git/grep.go
index 051a7a1d40..47f66b88b2 100644
--- a/modules/git/grep.go
+++ b/modules/git/grep.go
@@ -11,6 +11,7 @@ import (
"slices"
"strconv"
"strings"
+ "time"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/util"
@@ -39,6 +40,10 @@ type GrepOptions struct {
PathspecList []string
}
+// grepSearchTimeout is the timeout for git grep search, it should be long enough to get results
+// but not too long to cause performance issues
+const grepSearchTimeout = 30 * time.Second
+
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
/*
The output is like this ( "^@" means \x00):
@@ -76,6 +81,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe()
defer stdoutReaderClose()
err := cmd.WithDir(repo.Path).
+ WithTimeout(grepSearchTimeout).
WithPipelineFunc(func(ctx gitcmd.Context) error {
isInBlock := false
rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024))
diff --git a/modules/optional/option.go b/modules/optional/option.go
index cbecf86987..a278723bef 100644
--- a/modules/optional/option.go
+++ b/modules/optional/option.go
@@ -67,3 +67,17 @@ func ParseBool(s string) Option[bool] {
}
return Some(v)
}
+
+func AssignPtrValue[T comparable](changed *bool, target, src *T) {
+ if src != nil && *src != *target {
+ *target = *src
+ *changed = true
+ }
+}
+
+func AssignPtrString[TO, FROM ~string](changed *bool, target *TO, src *FROM) {
+ if src != nil && string(*src) != string(*target) {
+ *target = TO(*src)
+ *changed = true
+ }
+}
diff --git a/modules/packages/cran/metadata.go b/modules/packages/cran/metadata.go
index 0b0bfb07c6..0856565e10 100644
--- a/modules/packages/cran/metadata.go
+++ b/modules/packages/cran/metadata.go
@@ -34,7 +34,7 @@ var (
var (
fieldPattern = regexp.MustCompile(`\A\S+:`)
namePattern = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9\.]*[a-zA-Z0-9]\z`)
- versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+){1,3}\z`)
+ versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+)+\z`)
authorReplacePattern = regexp.MustCompile(`[\[\(].+?[\]\)]`)
)
diff --git a/modules/packages/cran/metadata_test.go b/modules/packages/cran/metadata_test.go
index ff68c34c51..1d652a4a05 100644
--- a/modules/packages/cran/metadata_test.go
+++ b/modules/packages/cran/metadata_test.go
@@ -128,13 +128,22 @@ func TestParseDescription(t *testing.T) {
})
t.Run("InvalidVersion", func(t *testing.T) {
- for _, version := range []string{"1", "1 0", "1.2.3.4.5", "1-2-3-4-5", "1.", "1.0.", "1-", "1-0-"} {
+ for _, version := range []string{"1", "1 0", "1.", "1.0.", "1-", "1-0-"} {
p, err := ParseDescription(createDescription(packageName, version))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidVersion)
}
})
+ t.Run("ValidVersionManyComponents", func(t *testing.T) {
+ for _, version := range []string{"0.3.4.0.2", "1.2.3.4.5", "1-2-3-4-5"} {
+ p, err := ParseDescription(createDescription(packageName, version))
+ assert.NoError(t, err)
+ assert.NotNil(t, p)
+ assert.Equal(t, version, p.Version)
+ }
+ })
+
t.Run("Valid", func(t *testing.T) {
p, err := ParseDescription(createDescription(packageName, packageVersion))
assert.NoError(t, err)
diff --git a/modules/setting/ui.go b/modules/setting/ui.go
index 77a5b45d0a..722341a71e 100644
--- a/modules/setting/ui.go
+++ b/modules/setting/ui.go
@@ -25,7 +25,6 @@ var UI = struct {
ReactionMaxUserNum int
MaxDisplayFileSize int64
ShowUserEmail bool
- DefaultShowFullName bool
DefaultTheme string
Themes []string
FileIconTheme string
@@ -43,6 +42,15 @@ var UI = struct {
AmbiguousUnicodeDetection bool
+ // TODO: DefaultShowFullName is introduced by https://github.com/go-gitea/gitea/pull/6710
+ // But there are still many edge cases:
+ // * Many places still use "username", not respecting this setting
+ // * Many places use "Full Name" if it is not empty, cause inconsistent UI for users who have set their full name but some others don't
+ // * Even if DefaultShowFullName=false, many places still need to show the full name
+ // For most cases, either "username" or "username (Full Name)" should be used and are good enough.
+ // Only in very few cases (e.g.: unimportant lists, narrow layout), "username" or "Full Name" can be used.
+ DefaultShowFullName bool
+
Notification struct {
MinTimeout time.Duration
TimeoutStep time.Duration
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 11c52bd5a7..82087568df 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -96,9 +96,6 @@ func NewFuncMap() template.FuncMap {
"AssetVersion": func() string {
return setting.AssetVersion
},
- "DefaultShowFullName": func() bool {
- return setting.UI.DefaultShowFullName
- },
"ShowFooterTemplateLoadTime": func() bool {
return setting.Other.ShowFooterTemplateLoadTime
},
diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go
index ee9994ab0b..524c64d0b6 100644
--- a/modules/templates/util_avatar.go
+++ b/modules/templates/util_avatar.go
@@ -16,6 +16,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
gitea_html "code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
)
type AvatarUtils struct {
@@ -29,13 +30,9 @@ func NewAvatarUtils(ctx context.Context) *AvatarUtils {
// AvatarHTML creates the HTML for an avatar
func AvatarHTML(src string, size int, class, name string) template.HTML {
sizeStr := strconv.Itoa(size)
-
- if name == "" {
- name = "avatar"
- }
-
+ name = util.IfZero(name, "avatar")
// use empty alt, otherwise if the image fails to load, the width will follow the "alt" text's width
- return template.HTML(``)
+ return template.HTML(`
`)
}
// Avatar renders user avatars. args: user, size (int), class (string)
diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json
index ad00325b02..4669252a6e 100644
--- a/options/locale/locale_ga-IE.json
+++ b/options/locale/locale_ga-IE.json
@@ -84,6 +84,7 @@
"save": "Sábháil",
"add": "Cuir",
"add_all": "Cuir Gach",
+ "dismiss": "Díbhe",
"remove": "Bain",
"remove_all": "Bain Gach",
"remove_label_str": "Bain mír “%s”",
@@ -224,7 +225,7 @@
"startpage.lightweight": "Éadrom",
"startpage.lightweight_desc": "Tá íosta riachtanais íseal ag Gitea agus is féidir leo rith ar Raspberry Pi saor. Sábháil fuinneamh do mheaisín!",
"startpage.license": "Foinse Oscailte",
- "startpage.license_desc": "Téigh go bhfaighidh %[2]s! Bí linn trí cur leis chun an tionscadal seo a fheabhsú fós. Ná bíodh cúthail ort a bheith i do rannpháirtí!",
+ "startpage.license_desc": "Téigh agus faigh %[2]s! Bí linn trí cur leis chun an tionscadal seo a dhéanamh níos fearr fós. Ná bíodh leisce ort cur leis!",
"install.install": "Suiteáil",
"install.installing_desc": "Suiteáil anois, fan go fóill…",
"install.title": "Cumraíocht Tosaigh",
@@ -284,12 +285,6 @@
"install.register_confirm": "Deimhniú Ríomhphoist a cheangal le Clárú",
"install.mail_notify": "Cumasaigh Fógraí Ríomhphoist",
"install.server_service_title": "Socruithe Freastalaí agus Seirbhíse Tríú Páirtí",
- "install.offline_mode": "Cumasaigh Mód Áitiúil",
- "install.offline_mode_popup": "Díchumasaigh líonraí seachadta ábhair tríú páirtí agus freastal ar na hacmhainní go léir go háitiúil.",
- "install.disable_gravatar": "Díchumasaigh Gravatar",
- "install.disable_gravatar_popup": "Díchumasaigh foinsí abhatár Gravatar agus tríú páirtí. Úsáidfear abhatár réamhshocraithe mura n-uaslódálann úsáideoir abhatár go háitiúil.",
- "install.federated_avatar_lookup": "Cumasaigh Abhatáir Chónaidhme",
- "install.federated_avatar_lookup_popup": "Cumasaigh cuardach avatar cónaidhme ag baint úsáide as Libravatar.",
"install.disable_registration": "Díchumasaigh Féin-Chlárú",
"install.disable_registration_popup": "Díchumasaigh féinchlárú úsáideora. Ní bheidh ach riarthóirí in ann cuntais úsáideora nua a chruthú.",
"install.allow_only_external_registration_popup": "Ceadaigh Clárú Trí Sheirbhísí Seachtracha amháin",
@@ -871,7 +866,7 @@
"settings.permissions_list": "Ceadanna:",
"settings.manage_oauth2_applications": "Bainistigh Feidhmchláir OAuth2",
"settings.edit_oauth2_application": "Cuir Feidhmchlár OAuth2 in eagar",
- "settings.oauth2_applications_desc": "Cumasaíonn feidhmchláir OAuth2 d’fheidhmchlár tríú páirtí úsáideoirí a fhíordheimhniú go slán ag an ásc Gitea seo.",
+ "settings.oauth2_applications_desc": "Cuireann feidhmchláir OAuth2 ar chumas d’fheidhmchlár tríú páirtí úsáideoirí a fhíordheimhniú go slán ag an gcás Gitea seo.",
"settings.remove_oauth2_application": "Bain Feidhmchlár OAuth2",
"settings.remove_oauth2_application_desc": "Ag baint feidhmchlár OAuth2, cúlghairfear rochtain ar gach comhartha rochtana sínithe. Lean ar aghaidh?",
"settings.remove_oauth2_application_success": "Scriosadh an feidhmchlár.",
@@ -890,7 +885,7 @@
"settings.oauth2_regenerate_secret_hint": "Chaill tú do rún?",
"settings.oauth2_client_secret_hint": "Ní thaispeánfar an rún arís tar éis duit an leathanach seo a fhágáil nó a athnuachan. Déan cinnte le do thoil gur shábháil tú é.",
"settings.oauth2_application_edit": "Cuir in eagar",
- "settings.oauth2_application_create_description": "Tugann feidhmchláir OAuth2 rochtain d'iarratas tríú páirtí ar chuntais úsáideora ar an gcás seo.",
+ "settings.oauth2_application_create_description": "Tugann feidhmchláir OAuth2 rochtain do d’fheidhmchlár tríú páirtí ar chuntais úsáideora ar an gcás seo.",
"settings.oauth2_application_remove_description": "Cuirfear feidhmchlár OAuth2 a bhaint cosc air rochtain a fháil ar chuntais úsáideora údaraithe ar an gcás seo. Lean ar aghaidh?",
"settings.oauth2_application_locked": "Réamhchláraíonn Gitea roinnt feidhmchlár OAuth2 ar thosú má tá sé cumasaithe i gcumraíocht. Chun iompar gan choinne a chosc, ní féidir iad seo a chur in eagar ná a bhaint. Féach do thoil do dhoiciméadú OAuth2 le haghaidh tuilleadh faisnéise.",
"settings.authorized_oauth2_applications": "Feidhmchláir Údaraithe OAuth2",
@@ -1524,6 +1519,7 @@
"repo.issues.commented_at": "trácht %s ",
"repo.issues.delete_comment_confirm": "An bhfuil tú cinnte gur mhaith leat an trácht seo a scriosadh?",
"repo.issues.context.copy_link": "Cóipeáil Nasc",
+ "repo.issues.context.copy_source": "Cóipeáil Foinse",
"repo.issues.context.quote_reply": "Luaigh Freagra",
"repo.issues.context.reference_issue": "Tagairt in Eagrán Nua",
"repo.issues.context.edit": "Cuir in eagar",
@@ -3192,7 +3188,6 @@
"admin.config.custom_conf": "Cosán Comhad Cumraíochta",
"admin.config.custom_file_root_path": "Cosán Fréamh Comhad Saincheaptha",
"admin.config.domain": "Fearann Freastalaí",
- "admin.config.offline_mode": "Mód Áitiúil",
"admin.config.disable_router_log": "Díchumasaigh Loga an Ródaire",
"admin.config.run_user": "Rith Mar Ainm úsáideora",
"admin.config.run_mode": "Mód Rith",
@@ -3278,6 +3273,13 @@
"admin.config.cache_test_failed": "Theip ar an taisce a thaiscéaladh: %v.",
"admin.config.cache_test_slow": "D'éirigh leis an tástáil taisce, ach tá an freagra mall: %s.",
"admin.config.cache_test_succeeded": "D'éirigh leis an tástáil taisce, fuair sé freagra i %s.",
+ "admin.config.common.start_time": "Am tosaithe",
+ "admin.config.common.end_time": "Am deiridh",
+ "admin.config.common.skip_time_check": "Fág an t-am folamh (glan an réimse) chun seiceáil ama a scipeáil",
+ "admin.config.instance_maintenance": "Cothabháil Cásanna",
+ "admin.config.instance_maintenance_mode.admin_web_access_only": "Lig don riarthóir amháin rochtain a fháil ar chomhéadan gréasáin",
+ "admin.config.instance_web_banner.enabled": "Taispeáin meirge",
+ "admin.config.instance_web_banner.message_placeholder": "Teachtaireacht meirge (tacaíonn sé le Markdown)",
"admin.config.session_config": "Cumraíocht Seisiúin",
"admin.config.session_provider": "Soláthraí Seisiúin",
"admin.config.provider_config": "Cumraíocht Soláthraí",
@@ -3288,7 +3290,7 @@
"admin.config.cookie_life_time": "Am Saoil Fianán",
"admin.config.picture_config": "Cumraíocht Pictiúr agus Avatar",
"admin.config.picture_service": "Seirbhís Pictiúr",
- "admin.config.disable_gravatar": "Díchumasaigh Gravatar",
+ "admin.config.enable_gravatar": "Cumasaigh Gravatar",
"admin.config.enable_federated_avatar": "Cumasaigh Avatars Cónaidhme",
"admin.config.open_with_editor_app_help": "Na heagarthóirí \"Oscailte le\" don roghchlár Clón. Má fhágtar folamh é, úsáidfear an réamhshocrú. Leathnaigh chun an réamhshocrú a fheiceáil.",
"admin.config.git_guide_remote_name": "Ainm iargúlta stórais le haghaidh orduithe git sa treoir",
@@ -3672,6 +3674,8 @@
"actions.runners.reset_registration_token_confirm": "Ar mhaith leat an comhartha reatha a neamhbhailiú agus ceann nua a ghiniúint?",
"actions.runners.reset_registration_token_success": "D'éirigh le hathshocrú comhartha clárúcháin an dara háit",
"actions.runs.all_workflows": "Gach Sreafaí Oibre",
+ "actions.runs.workflow_run_count_1": "%d rith sreabha oibre",
+ "actions.runs.workflow_run_count_n": "%d rith sreabha oibre",
"actions.runs.commit": "Tiomantas",
"actions.runs.scheduled": "Sceidealaithe",
"actions.runs.pushed_by": "bhrú ag",
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 50626cebbf..767e5533fd 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -895,34 +895,35 @@ func Routes() *web.Router {
addActionsRoutes := func(
m *web.Router,
- reqChecker func(ctx *context.APIContext),
+ reqReaderCheck func(ctx *context.APIContext),
+ reqOwnerCheck func(ctx *context.APIContext),
act actions.API,
) {
m.Group("/actions", func() {
m.Group("/secrets", func() {
- m.Get("", reqToken(), reqChecker, act.ListActionsSecrets)
+ m.Get("", reqToken(), reqOwnerCheck, act.ListActionsSecrets)
m.Combo("/{secretname}").
- Put(reqToken(), reqChecker, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
- Delete(reqToken(), reqChecker, act.DeleteSecret)
+ Put(reqToken(), reqOwnerCheck, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
+ Delete(reqToken(), reqOwnerCheck, act.DeleteSecret)
})
m.Group("/variables", func() {
- m.Get("", reqToken(), reqChecker, act.ListVariables)
+ m.Get("", reqToken(), reqOwnerCheck, act.ListVariables)
m.Combo("/{variablename}").
- Get(reqToken(), reqChecker, act.GetVariable).
- Delete(reqToken(), reqChecker, act.DeleteVariable).
- Post(reqToken(), reqChecker, bind(api.CreateVariableOption{}), act.CreateVariable).
- Put(reqToken(), reqChecker, bind(api.UpdateVariableOption{}), act.UpdateVariable)
+ Get(reqToken(), reqOwnerCheck, act.GetVariable).
+ Delete(reqToken(), reqOwnerCheck, act.DeleteVariable).
+ Post(reqToken(), reqOwnerCheck, bind(api.CreateVariableOption{}), act.CreateVariable).
+ Put(reqToken(), reqOwnerCheck, bind(api.UpdateVariableOption{}), act.UpdateVariable)
})
m.Group("/runners", func() {
- m.Get("", reqToken(), reqChecker, act.ListRunners)
- m.Post("/registration-token", reqToken(), reqChecker, act.CreateRegistrationToken)
- m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner)
- m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner)
+ m.Get("", reqToken(), reqOwnerCheck, act.ListRunners)
+ m.Post("/registration-token", reqToken(), reqOwnerCheck, act.CreateRegistrationToken)
+ m.Get("/{runner_id}", reqToken(), reqOwnerCheck, act.GetRunner)
+ m.Delete("/{runner_id}", reqToken(), reqOwnerCheck, act.DeleteRunner)
})
- m.Get("/runs", reqToken(), reqChecker, act.ListWorkflowRuns)
- m.Get("/jobs", reqToken(), reqChecker, act.ListWorkflowJobs)
+ m.Get("/runs", reqToken(), reqReaderCheck, act.ListWorkflowRuns)
+ m.Get("/jobs", reqToken(), reqReaderCheck, act.ListWorkflowJobs)
})
}
@@ -1164,7 +1165,8 @@ func Routes() *web.Router {
m.Post("/reject", repo.RejectTransfer)
}, reqToken())
- addActionsRoutes(m, reqOwner(), repo.NewAction()) // it adds the routes for secrets/variables and runner management
+ // Adds the routes for secrets/variables and runner management
+ addActionsRoutes(m, reqRepoReader(unit.TypeActions), reqOwner(), repo.NewAction())
m.Group("/actions/workflows", func() {
m.Get("", repo.ActionsListRepositoryWorkflows)
@@ -1259,7 +1261,9 @@ func Routes() *web.Router {
m.Group("/{run}", func() {
m.Get("", repo.GetWorkflowRun)
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
+ m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
m.Get("/jobs", repo.ListWorkflowRunJobs)
+ m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
m.Get("/artifacts", repo.GetArtifactsOfRun)
})
})
@@ -1617,6 +1621,7 @@ func Routes() *web.Router {
})
addActionsRoutes(
m,
+ reqOrgMembership(),
reqOrgOwnership(),
org.NewAction(),
)
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index 4c3a0dceff..13da5aa815 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -12,6 +12,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "slices"
"strconv"
"strings"
"time"
@@ -1103,6 +1104,33 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
+func getCurrentRepoActionRunByID(ctx *context.APIContext) *actions_model.ActionRun {
+ runID := ctx.PathParamInt64("run")
+ run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.APIErrorNotFound(err)
+ return nil
+ } else if err != nil {
+ ctx.APIErrorInternal(err)
+ return nil
+ }
+ return run
+}
+
+func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.ActionRun, actions_model.ActionJobList) {
+ run := getCurrentRepoActionRunByID(ctx)
+ if ctx.Written() {
+ return nil, nil
+ }
+
+ jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
+ if err != nil {
+ ctx.APIErrorInternal(err)
+ return nil, nil
+ }
+ return run, jobs
+}
+
// GetWorkflowRun Gets a specific workflow run.
func GetWorkflowRun(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
@@ -1134,19 +1162,12 @@ func GetWorkflowRun(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- runID := ctx.PathParamInt64("run")
- job, has, err := db.GetByID[actions_model.ActionRun](ctx, runID)
- if err != nil {
- ctx.APIErrorInternal(err)
+ run := getCurrentRepoActionRunByID(ctx)
+ if ctx.Written() {
return
}
- if !has || job.RepoID != ctx.Repo.Repository.ID {
- ctx.APIErrorNotFound(util.ErrNotExist)
- return
- }
-
- convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job)
+ convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1154,6 +1175,133 @@ func GetWorkflowRun(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convertedRun)
}
+// RerunWorkflowRun Reruns an entire workflow run.
+func RerunWorkflowRun(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun repository rerunWorkflowRun
+ // ---
+ // summary: Reruns an entire workflow run
+ // 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 repository
+ // type: string
+ // required: true
+ // - name: run
+ // in: path
+ // description: id of the run
+ // type: integer
+ // required: true
+ // responses:
+ // "201":
+ // "$ref": "#/responses/WorkflowRun"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ run, jobs := getCurrentRepoActionRunJobsByID(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, nil); err != nil {
+ handleWorkflowRerunError(ctx, err)
+ return
+ }
+
+ convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
+ if err != nil {
+ ctx.APIErrorInternal(err)
+ return
+ }
+ ctx.JSON(http.StatusCreated, convertedRun)
+}
+
+// RerunWorkflowJob Reruns a specific workflow job in a run.
+func RerunWorkflowJob(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob
+ // ---
+ // summary: Reruns a specific workflow job in a run
+ // 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 repository
+ // type: string
+ // required: true
+ // - name: run
+ // in: path
+ // description: id of the run
+ // type: integer
+ // required: true
+ // - name: job_id
+ // in: path
+ // description: id of the job
+ // type: integer
+ // required: true
+ // responses:
+ // "201":
+ // "$ref": "#/responses/WorkflowJob"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ run, jobs := getCurrentRepoActionRunJobsByID(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ jobID := ctx.PathParamInt64("job_id")
+ jobIdx := slices.IndexFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.ID == jobID })
+ if jobIdx == -1 {
+ ctx.APIErrorNotFound(util.NewNotExistErrorf("workflow job with id %d", jobID))
+ return
+ }
+
+ targetJob := jobs[jobIdx]
+ if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil {
+ handleWorkflowRerunError(ctx, err)
+ return
+ }
+
+ convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, targetJob)
+ if err != nil {
+ ctx.APIErrorInternal(err)
+ return
+ }
+ ctx.JSON(http.StatusCreated, convertedJob)
+}
+
+func handleWorkflowRerunError(ctx *context.APIContext, err error) {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.APIError(http.StatusBadRequest, err)
+ return
+ }
+ ctx.APIErrorInternal(err)
+}
+
// ListWorkflowRunJobs Lists all jobs for a workflow run.
func ListWorkflowRunJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs
@@ -1198,9 +1346,7 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- repoID := ctx.Repo.Repository.ID
-
- runID := ctx.PathParamInt64("run")
+ repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
// Avoid the list all jobs functionality for this api route to be used with a runID == 0.
if runID <= 0 {
@@ -1300,10 +1446,8 @@ func GetArtifactsOfRun(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- repoID := ctx.Repo.Repository.ID
artifactName := ctx.Req.URL.Query().Get("name")
-
- runID := ctx.PathParamInt64("run")
+ repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
RepoID: repoID,
@@ -1364,15 +1508,11 @@ func DeleteActionRun(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- runID := ctx.PathParamInt64("run")
- run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
- if errors.Is(err, util.ErrNotExist) {
- ctx.APIErrorNotFound(err)
- return
- } else if err != nil {
- ctx.APIErrorInternal(err)
+ run := getCurrentRepoActionRunByID(ctx)
+ if ctx.Written() {
return
}
+
if !run.Status.IsDone() {
ctx.APIError(http.StatusBadRequest, "this workflow run is not done")
return
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index bb6bda587d..cfdcf7b374 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -884,77 +884,44 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
}
}
- if opts.HasPullRequests != nil && !unit_model.TypePullRequests.UnitGlobalDisabled() {
- if *opts.HasPullRequests {
- // We do allow setting individual PR settings through the API, so
- // we get the config settings and then set them
- // if those settings were provided in the opts.
- unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests)
- var config *repo_model.PullRequestsConfig
- if err != nil {
- // Unit type doesn't exist so we make a new config file with default values
- config = &repo_model.PullRequestsConfig{
- IgnoreWhitespaceConflicts: false,
- AllowMerge: true,
- AllowRebase: true,
- AllowRebaseMerge: true,
- AllowSquash: true,
- AllowFastForwardOnly: true,
- AllowManualMerge: true,
- AutodetectManualMerge: false,
- AllowRebaseUpdate: true,
- DefaultDeleteBranchAfterMerge: false,
- DefaultMergeStyle: repo_model.MergeStyleMerge,
- DefaultAllowMaintainerEdit: false,
- }
- } else {
- config = unit.PullRequestsConfig()
- }
-
- if opts.IgnoreWhitespaceConflicts != nil {
- config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
- }
- if opts.AllowMerge != nil {
- config.AllowMerge = *opts.AllowMerge
- }
- if opts.AllowRebase != nil {
- config.AllowRebase = *opts.AllowRebase
- }
- if opts.AllowRebaseMerge != nil {
- config.AllowRebaseMerge = *opts.AllowRebaseMerge
- }
- if opts.AllowSquash != nil {
- config.AllowSquash = *opts.AllowSquash
- }
- if opts.AllowFastForwardOnly != nil {
- config.AllowFastForwardOnly = *opts.AllowFastForwardOnly
- }
- if opts.AllowManualMerge != nil {
- config.AllowManualMerge = *opts.AllowManualMerge
- }
- if opts.AutodetectManualMerge != nil {
- config.AutodetectManualMerge = *opts.AutodetectManualMerge
- }
- if opts.AllowRebaseUpdate != nil {
- config.AllowRebaseUpdate = *opts.AllowRebaseUpdate
- }
- if opts.DefaultDeleteBranchAfterMerge != nil {
- config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge
- }
- if opts.DefaultMergeStyle != nil {
- config.DefaultMergeStyle = repo_model.MergeStyle(*opts.DefaultMergeStyle)
- }
- if opts.DefaultAllowMaintainerEdit != nil {
- config.DefaultAllowMaintainerEdit = *opts.DefaultAllowMaintainerEdit
- }
-
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypePullRequests,
- Config: config,
- })
- } else {
+ if !unit_model.TypePullRequests.UnitGlobalDisabled() {
+ mustDeletePullRequestUnit := opts.HasPullRequests != nil && !*opts.HasPullRequests
+ mustInsertPullRequestUnit := opts.HasPullRequests != nil && *opts.HasPullRequests
+ if mustDeletePullRequestUnit {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
+ } else {
+ // We do allow setting individual PR settings through the API,
+ // so we get the config settings and then set them if those settings were provided in the opts.
+ unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests)
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ return err
+ }
+ if unit == nil {
+ // Unit doesn't exist yet but is being enabled, create with defaults
+ unit = new(repo_model.DefaultPullRequestsUnit(repo.ID))
+ }
+
+ changed := new(false)
+ config := unit.PullRequestsConfig()
+ optional.AssignPtrValue(changed, &config.IgnoreWhitespaceConflicts, opts.IgnoreWhitespaceConflicts)
+ optional.AssignPtrValue(changed, &config.AllowMerge, opts.AllowMerge)
+ optional.AssignPtrValue(changed, &config.AllowRebase, opts.AllowRebase)
+ optional.AssignPtrValue(changed, &config.AllowRebaseMerge, opts.AllowRebaseMerge)
+ optional.AssignPtrValue(changed, &config.AllowSquash, opts.AllowSquash)
+ optional.AssignPtrValue(changed, &config.AllowFastForwardOnly, opts.AllowFastForwardOnly)
+ optional.AssignPtrValue(changed, &config.AllowManualMerge, opts.AllowManualMerge)
+ optional.AssignPtrValue(changed, &config.AutodetectManualMerge, opts.AutodetectManualMerge)
+ optional.AssignPtrValue(changed, &config.AllowRebaseUpdate, opts.AllowRebaseUpdate)
+ optional.AssignPtrValue(changed, &config.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge)
+ optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
+ optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
+ if *changed || mustInsertPullRequestUnit {
+ units = append(units, repo_model.RepoUnit{
+ RepoID: repo.ID,
+ Type: unit_model.TypePullRequests,
+ Config: config,
+ })
+ }
}
}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 4c023d9252..0eaa6cab41 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -36,8 +36,6 @@ import (
notify_service "code.gitea.io/gitea/services/notify"
"github.com/nektos/act/pkg/model"
- "go.yaml.in/yaml/v4"
- "xorm.io/builder"
)
func getRunIndex(ctx *context_module.Context) int64 {
@@ -53,7 +51,7 @@ func getRunIndex(ctx *context_module.Context) int64 {
func View(ctx *context_module.Context) {
ctx.Data["PageIsActions"] = true
runIndex := getRunIndex(ctx)
- jobIndex := ctx.PathParamInt64("job")
+ jobIndex := ctx.PathParamInt("job")
ctx.Data["RunIndex"] = runIndex
ctx.Data["JobIndex"] = jobIndex
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
@@ -211,7 +209,7 @@ func getActionsViewArtifacts(ctx context.Context, repoID, runIndex int64) (artif
func ViewPost(ctx *context_module.Context) {
req := web.GetForm(ctx).(*ViewRequest)
runIndex := getRunIndex(ctx)
- jobIndex := ctx.PathParamInt64("job")
+ jobIndex := ctx.PathParamInt("job")
current, jobs := getRunJobs(ctx, runIndex, jobIndex)
if ctx.Written() {
@@ -405,11 +403,8 @@ func convertToViewModel(ctx context.Context, locale translation.Locale, cursors
// If jobIndexStr is a blank string, it means rerun all jobs
func Rerun(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
- jobIndexStr := ctx.PathParam("job")
- var jobIndex int64
- if jobIndexStr != "" {
- jobIndex, _ = strconv.ParseInt(jobIndexStr, 10, 64)
- }
+ jobIndexHas := ctx.PathParam("job") != ""
+ jobIndex := ctx.PathParamInt("job")
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
@@ -431,130 +426,29 @@ func Rerun(ctx *context_module.Context) {
return
}
- // reset run's start and stop time
- run.PreviousDuration = run.Duration()
- run.Started = 0
- run.Stopped = 0
- run.Status = actions_model.StatusWaiting
-
- vars, err := actions_model.GetVariablesOfRun(ctx, run)
+ jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
- ctx.ServerError("GetVariablesOfRun", fmt.Errorf("get run %d variables: %w", run.ID, err))
+ ctx.ServerError("GetRunJobsByRunID", err)
return
}
- if run.RawConcurrency != "" {
- var rawConcurrency model.RawConcurrency
- if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
- ctx.ServerError("UnmarshalRawConcurrency", fmt.Errorf("unmarshal raw concurrency: %w", err))
- return
- }
-
- err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars, nil)
- if err != nil {
- ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
- return
- }
-
- run.Status, err = actions_service.PrepareToStartRunWithConcurrency(ctx, run)
- if err != nil {
- ctx.ServerError("PrepareToStartRunWithConcurrency", err)
+ var targetJob *actions_model.ActionRunJob // nil means rerun all jobs
+ if jobIndexHas {
+ if jobIndex < 0 || jobIndex >= len(jobs) {
+ ctx.JSONError(ctx.Locale.Tr("error.not_found"))
return
}
+ targetJob = jobs[jobIndex] // only rerun the selected job
}
- if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
- ctx.ServerError("UpdateRun", err)
+
+ if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil {
+ ctx.ServerError("RerunWorkflowRunJobs", err)
return
}
- if err := run.LoadAttributes(ctx); err != nil {
- ctx.ServerError("run.LoadAttributes", err)
- return
- }
- notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
-
- job, jobs := getRunJobs(ctx, runIndex, jobIndex)
- if ctx.Written() {
- return
- }
-
- isRunBlocked := run.Status == actions_model.StatusBlocked
- if jobIndexStr == "" { // rerun all jobs
- for _, j := range jobs {
- // if the job has needs, it should be set to "blocked" status to wait for other jobs
- shouldBlockJob := len(j.Needs) > 0 || isRunBlocked
- if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
- ctx.ServerError("RerunJob", err)
- return
- }
- }
- ctx.JSONOK()
- return
- }
-
- rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
-
- for _, j := range rerunJobs {
- // jobs other than the specified one should be set to "blocked" status
- shouldBlockJob := j.JobID != job.JobID || isRunBlocked
- if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
- ctx.ServerError("RerunJob", err)
- return
- }
- }
-
ctx.JSONOK()
}
-func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
- status := job.Status
- if !status.IsDone() {
- return nil
- }
-
- job.TaskID = 0
- job.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting)
- job.Started = 0
- job.Stopped = 0
-
- job.ConcurrencyGroup = ""
- job.ConcurrencyCancel = false
- job.IsConcurrencyEvaluated = false
- if err := job.LoadRun(ctx); err != nil {
- return err
- }
-
- vars, err := actions_model.GetVariablesOfRun(ctx, job.Run)
- if err != nil {
- return fmt.Errorf("get run %d variables: %w", job.Run.ID, err)
- }
-
- if job.RawConcurrency != "" && !shouldBlock {
- err = actions_service.EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars, nil)
- if err != nil {
- return fmt.Errorf("evaluate job concurrency: %w", err)
- }
-
- job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
- if err != nil {
- return err
- }
- }
-
- if err := db.WithTx(ctx, func(ctx context.Context) error {
- updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"}
- _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...)
- return err
- }); err != nil {
- return err
- }
-
- actions_service.CreateCommitStatusForRunJobs(ctx, job.Run, job)
- notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
-
- return nil
-}
-
func Logs(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
jobIndex := ctx.PathParamInt64("job")
@@ -715,7 +609,7 @@ func Delete(ctx *context_module.Context) {
// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
// Any error will be written to the ctx.
// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
-func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
+func getRunJobs(ctx *context_module.Context, runIndex int64, jobIndex int) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
@@ -740,7 +634,7 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
v.Run = run
}
- if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
+ if jobIndex >= 0 && jobIndex < len(jobs) {
return jobs[jobIndex], jobs
}
return jobs[0], jobs
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index a56df78163..23cedfcb80 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -6,13 +6,14 @@ package repo
import (
"bytes"
"html"
+ "html/template"
"net/http"
"strings"
"code.gitea.io/gitea/models/avatars"
issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
@@ -53,29 +54,25 @@ func GetContentHistoryList(ctx *context.Context) {
// value is historyId
var results []map[string]any
for _, item := range items {
- var actionText string
+ var actionHTML template.HTML
if item.IsDeleted {
- actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
- actionText = "" + actionTextDeleted + ""
+ actionHTML = htmlutil.HTMLFormat(`%s`, ctx.Locale.TrString("repo.issues.content_history.deleted"))
} else if item.IsFirstCreated {
- actionText = ctx.Locale.TrString("repo.issues.content_history.created")
+ actionHTML = ctx.Locale.Tr("repo.issues.content_history.created")
} else {
- actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
+ actionHTML = ctx.Locale.Tr("repo.issues.content_history.edited")
}
- username := item.UserName
- if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
- username = strings.TrimSpace(item.UserFullName)
+ var fullNameHTML template.HTML
+ userName, fullName := item.UserName, strings.TrimSpace(item.UserFullName)
+ if fullName != "" {
+ fullNameHTML = htmlutil.HTMLFormat(` (%s)`, fullName)
}
- src := html.EscapeString(item.UserAvatarLink)
- class := avatars.DefaultAvatarClass + " tw-mr-2"
- name := html.EscapeString(username)
- avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
- timeSinceHTML := string(templates.TimeSince(item.EditedUnix))
-
+ avatarHTML := templates.AvatarHTML(item.UserAvatarLink, 24, avatars.DefaultAvatarClass+" tw-mr-2", userName)
+ timeSinceHTML := templates.TimeSince(item.EditedUnix)
results = append(results, map[string]any{
- "name": avatarHTML + "" + name + " " + actionText + " " + timeSinceHTML,
+ "name": htmlutil.HTMLFormat("%s %s%s %s %s", avatarHTML, userName, fullNameHTML, actionHTML, timeSinceHTML),
"value": item.HistoryID,
})
}
diff --git a/routers/web/repo/issue_poster.go b/routers/web/repo/issue_poster.go
index 07059b9b7b..4f00f40a91 100644
--- a/routers/web/repo/issue_poster.go
+++ b/routers/web/repo/issue_poster.go
@@ -10,7 +10,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
)
@@ -34,7 +33,7 @@ func IssuePullPosters(ctx *context.Context) {
func issuePosters(ctx *context.Context, isPullList bool) {
repo := ctx.Repo.Repository
search := strings.TrimSpace(ctx.FormString("q"))
- posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search, setting.UI.DefaultShowFullName)
+ posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search)
if err != nil {
ctx.JSON(http.StatusInternalServerError, err)
return
@@ -54,9 +53,7 @@ func issuePosters(ctx *context.Context, isPullList bool) {
resp.Results = make([]*userSearchInfo, len(posters))
for i, user := range posters {
resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)}
- if setting.UI.DefaultShowFullName {
- resp.Results[i].FullName = user.FullName
- }
+ resp.Results[i].FullName = user.FullName
}
ctx.JSON(http.StatusOK, resp)
}
diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go
index 1354c2d6f9..2cd8be4533 100644
--- a/routers/web/repo/issue_view.go
+++ b/routers/web/repo/issue_view.go
@@ -905,9 +905,8 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
// Check correct values and select default
if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
!prConfig.IsMergeStyleAllowed(ms) {
- defaultMergeStyle := prConfig.GetDefaultMergeStyle()
- if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
- mergeStyle = defaultMergeStyle
+ if prConfig.IsMergeStyleAllowed(prConfig.DefaultMergeStyle) && !ok {
+ mergeStyle = prConfig.DefaultMergeStyle
} else if prConfig.AllowMerge {
mergeStyle = repo_model.MergeStyleMerge
} else if prConfig.AllowRebase {
diff --git a/services/actions/rerun.go b/services/actions/rerun.go
index 60f6650905..277da39b82 100644
--- a/services/actions/rerun.go
+++ b/services/actions/rerun.go
@@ -4,8 +4,20 @@
package actions
import (
+ "context"
+ "fmt"
+
actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/container"
+ "code.gitea.io/gitea/modules/util"
+ notify_service "code.gitea.io/gitea/services/notify"
+
+ "github.com/nektos/act/pkg/model"
+ "go.yaml.in/yaml/v4"
+ "xorm.io/builder"
)
// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
@@ -36,3 +48,132 @@ func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.A
return rerunJobs
}
+
+// RerunWorkflowRunJobs reruns all done jobs of a workflow run,
+// or reruns a selected job and all of its downstream jobs when targetJob is specified.
+func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob, targetJob *actions_model.ActionRunJob) error {
+ // Rerun is not allowed if the run is not done.
+ if !run.Status.IsDone() {
+ return util.NewInvalidArgumentErrorf("this workflow run is not done")
+ }
+
+ cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
+
+ // Rerun is not allowed when workflow is disabled.
+ cfg := cfgUnit.ActionsConfig()
+ if cfg.IsWorkflowDisabled(run.WorkflowID) {
+ return util.NewInvalidArgumentErrorf("workflow %s is disabled", run.WorkflowID)
+ }
+
+ // Reset run's timestamps and status.
+ run.PreviousDuration = run.Duration()
+ run.Started = 0
+ run.Stopped = 0
+ run.Status = actions_model.StatusWaiting
+
+ vars, err := actions_model.GetVariablesOfRun(ctx, run)
+ if err != nil {
+ return fmt.Errorf("get run %d variables: %w", run.ID, err)
+ }
+
+ if run.RawConcurrency != "" {
+ var rawConcurrency model.RawConcurrency
+ if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
+ return fmt.Errorf("unmarshal raw concurrency: %w", err)
+ }
+
+ if err := EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars, nil); err != nil {
+ return err
+ }
+
+ run.Status, err = PrepareToStartRunWithConcurrency(ctx, run)
+ if err != nil {
+ return err
+ }
+ }
+
+ if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
+ return err
+ }
+
+ if err := run.LoadAttributes(ctx); err != nil {
+ return err
+ }
+
+ for _, job := range jobs {
+ job.Run = run
+ }
+
+ notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
+
+ isRunBlocked := run.Status == actions_model.StatusBlocked
+
+ if targetJob == nil {
+ for _, job := range jobs {
+ // If the job has needs, it should be blocked to wait for its dependencies.
+ shouldBlockJob := len(job.Needs) > 0 || isRunBlocked
+ if err := rerunWorkflowJob(ctx, job, shouldBlockJob); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+
+ rerunJobs := GetAllRerunJobs(targetJob, jobs)
+ for _, job := range rerunJobs {
+ // Jobs other than the selected one should wait for dependencies.
+ shouldBlockJob := job.JobID != targetJob.JobID || isRunBlocked
+ if err := rerunWorkflowJob(ctx, job, shouldBlockJob); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func rerunWorkflowJob(ctx context.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
+ status := job.Status
+ if !status.IsDone() {
+ return nil
+ }
+
+ job.TaskID = 0
+ job.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting)
+ job.Started = 0
+ job.Stopped = 0
+ job.ConcurrencyGroup = ""
+ job.ConcurrencyCancel = false
+ job.IsConcurrencyEvaluated = false
+
+ if err := job.LoadRun(ctx); err != nil {
+ return err
+ }
+
+ vars, err := actions_model.GetVariablesOfRun(ctx, job.Run)
+ if err != nil {
+ return fmt.Errorf("get run %d variables: %w", job.Run.ID, err)
+ }
+
+ if job.RawConcurrency != "" && !shouldBlock {
+ if err := EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars, nil); err != nil {
+ return fmt.Errorf("evaluate job concurrency: %w", err)
+ }
+
+ job.Status, err = PrepareToStartJobWithConcurrency(ctx, job)
+ if err != nil {
+ return err
+ }
+ }
+
+ if err := db.WithTx(ctx, func(ctx context.Context) error {
+ updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"}
+ _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...)
+ return err
+ }); err != nil {
+ return err
+ }
+
+ CreateCommitStatusForRunJobs(ctx, job.Run, job)
+ notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+ return nil
+}
diff --git a/services/convert/repository.go b/services/convert/repository.go
index 658d31d55c..3c9cc83ccb 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -118,7 +118,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
allowManualMerge = config.AllowManualMerge
autodetectManualMerge = config.AutodetectManualMerge
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
- defaultMergeStyle = config.GetDefaultMergeStyle()
+ defaultMergeStyle = config.DefaultMergeStyle
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
defaultTargetBranch = config.DefaultTargetBranch
}
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index 8f831f89ad..a08ed71480 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -158,18 +158,23 @@ func (b64embedder *mailAttachmentBase64Embedder) AttachmentSrcToBase64DataURI(ct
func fromDisplayName(u *user_model.User) string {
if setting.MailService.FromDisplayNameFormatTemplate != nil {
- var ctx bytes.Buffer
- err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&ctx, map[string]any{
+ var buf bytes.Buffer
+ err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&buf, map[string]any{
"DisplayName": u.DisplayName(),
"AppName": setting.AppName,
"Domain": setting.Domain,
})
if err == nil {
- return mime.QEncoding.Encode("utf-8", ctx.String())
+ return mime.QEncoding.Encode("utf-8", buf.String())
}
log.Error("fromDisplayName: %w", err)
}
- return u.GetCompleteName()
+ def := u.Name
+ if fullName := strings.TrimSpace(u.FullName); fullName != "" {
+ // use "Full Name (username)" for email's sender name if Full Name is not empty
+ def = fullName + " (" + u.Name + ")"
+ }
+ return def
}
func generateMetadataHeaders(repo *repo_model.Repository) map[string]string {
diff --git a/services/repository/create.go b/services/repository/create.go
index e027d3b979..a8b57b6707 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -386,15 +386,7 @@ func createRepositoryInDB(ctx context.Context, doer, u *user_model.User, repo *r
},
})
case unit.TypePullRequests:
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: tp,
- Config: &repo_model.PullRequestsConfig{
- AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true,
- DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle),
- AllowRebaseUpdate: true,
- },
- })
+ units = append(units, repo_model.DefaultPullRequestsUnit(repo.ID))
case unit.TypeProjects:
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index a4317b8c4e..e6de93c5f8 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -48,11 +48,14 @@