diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index c909f78597dc8..f9b6b1ec495c1 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -48,6 +48,7 @@ jobs: - "Makefile" - ".golangci.yml" - ".editorconfig" + - "options/locale/locale_en-US.ini" frontend: - "**/*.js" diff --git a/Makefile b/Makefile index 9bbc56451b81d..52357ba00d9d5 100644 --- a/Makefile +++ b/Makefile @@ -147,6 +147,7 @@ GO_DIRS := build cmd models modules routers services tests WEB_DIRS := web_src/js web_src/css SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github +EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini GO_SOURCES := $(wildcard *.go) GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go) @@ -426,7 +427,7 @@ lint-go-vet: .PHONY: lint-editorconfig lint-editorconfig: - $(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .github/workflows + @$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES) .PHONY: lint-actions lint-actions: diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md index a33a38a6f92b4..2c0aaaed4a36b 100644 --- a/docs/content/contributing/guidelines-frontend.en-us.md +++ b/docs/content/contributing/guidelines-frontend.en-us.md @@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided. 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event. 11. Custom event names are recommended to use `ce-` prefix. -12. Gitea's tailwind-style CSS classes use `gt-` prefix (`gt-relative`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). +12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). 13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided. ### Accessibility / ARIA diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md index 43f72b48083a0..ace0d97f497c4 100644 --- a/docs/content/contributing/guidelines-frontend.zh-cn.md +++ b/docs/content/contributing/guidelines-frontend.zh-cn.md @@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。 11. 推荐使用自定义事件名称前缀`ce-`。 -12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。 +12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-df`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。 13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。 ### 可访问性 / ARIA diff --git a/go.mod b/go.mod index 03f6ad1215849..d58890de285e4 100644 --- a/go.mod +++ b/go.mod @@ -113,7 +113,7 @@ require ( golang.org/x/text v0.14.0 golang.org/x/tools v0.17.0 google.golang.org/grpc v1.60.1 - google.golang.org/protobuf v1.32.0 + google.golang.org/protobuf v1.33.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index b3b8ad8ce48f9..87072571e5a6a 100644 --- a/go.sum +++ b/go.sum @@ -1308,8 +1308,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/models/activities/action.go b/models/activities/action.go index fcc97e387264d..36205eedd1f03 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -393,10 +393,14 @@ func (a *Action) GetCreate() time.Time { return a.CreatedUnix.AsTime() } -// GetIssueInfos returns a list of issues associated with -// the action. +// GetIssueInfos returns a list of associated information with the action. func (a *Action) GetIssueInfos() []string { - return strings.SplitN(a.Content, "|", 3) + // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length + ret := strings.SplitN(a.Content, "|", 3) + for len(ret) < 3 { + ret = append(ret, "") + } + return ret } // GetIssueTitle returns the title of first issue associated with the action. diff --git a/models/git/branch.go b/models/git/branch.go index db02fc9b28bbd..fa0781fed1d57 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -158,6 +158,11 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e return &branch, nil } +func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) { + branches := make([]*Branch, 0, len(branchNames)) + return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches) +} + func AddBranches(ctx context.Context, branches []*Branch) error { for _, branch := range branches { if _, err := db.GetEngine(ctx).Insert(branch); err != nil { diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 9d288ec2bdf17..d40866f3e93b3 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -562,6 +562,8 @@ var migrations = []Migration{ NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges), // v288 -> v289 NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable), + // v289 -> v290 + NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_22/v289.go b/models/migrations/v1_22/v289.go new file mode 100644 index 0000000000000..e2dfc48715a28 --- /dev/null +++ b/models/migrations/v1_22/v289.go @@ -0,0 +1,18 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import "xorm.io/xorm" + +func AddDefaultWikiBranch(x *xorm.Engine) error { + type Repository struct { + ID int64 + DefaultWikiBranch string + } + if err := x.Sync(&Repository{}); err != nil { + return err + } + _, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')") + return err +} diff --git a/models/repo/repo.go b/models/repo/repo.go index f6758f1591ac6..1d17e565aef2c 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -136,6 +136,7 @@ type Repository struct { OriginalServiceType api.GitServiceType `xorm:"index"` OriginalURL string `xorm:"VARCHAR(2048)"` DefaultBranch string + DefaultWikiBranch string NumWatches int NumStars int @@ -285,6 +286,9 @@ func (repo *Repository) AfterLoad() { repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns + if repo.DefaultWikiBranch == "" { + repo.DefaultWikiBranch = setting.Repository.DefaultBranch + } } // LoadAttributes loads attributes of the repository. diff --git a/models/user/email_address.go b/models/user/email_address.go index 5d67304691ad1..11700a0129cc8 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -154,37 +154,18 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error { var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") -// ValidateEmail check if email is a allowed address +// ValidateEmail check if email is a valid & allowed address func ValidateEmail(email string) error { - if len(email) == 0 { - return ErrEmailInvalid{email} - } - - if !emailRegexp.MatchString(email) { - return ErrEmailCharIsNotSupported{email} - } - - if email[0] == '-' { - return ErrEmailInvalid{email} - } - - if _, err := mail.ParseAddress(email); err != nil { - return ErrEmailInvalid{email} - } - - // if there is no allow list, then check email against block list - if len(setting.Service.EmailDomainAllowList) == 0 && - validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) { - return ErrEmailInvalid{email} - } - - // if there is an allow list, then check email against allow list - if len(setting.Service.EmailDomainAllowList) > 0 && - !validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) { - return ErrEmailInvalid{email} + if err := validateEmailBasic(email); err != nil { + return err } + return validateEmailDomain(email) +} - return nil +// ValidateEmailForAdmin check if email is a valid address when admins manually add or edit users +func ValidateEmailForAdmin(email string) error { + return validateEmailBasic(email) + // In this case we do not need to check the email domain } func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) { @@ -534,3 +515,41 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate return committer.Commit() } + +// validateEmailBasic checks whether the email complies with the rules +func validateEmailBasic(email string) error { + if len(email) == 0 { + return ErrEmailInvalid{email} + } + + if !emailRegexp.MatchString(email) { + return ErrEmailCharIsNotSupported{email} + } + + if email[0] == '-' { + return ErrEmailInvalid{email} + } + + if _, err := mail.ParseAddress(email); err != nil { + return ErrEmailInvalid{email} + } + + return nil +} + +// validateEmailDomain checks whether the email domain is allowed or blocked +func validateEmailDomain(email string) error { + // if there is no allow list, then check email against block list + if len(setting.Service.EmailDomainAllowList) == 0 && + validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) { + return ErrEmailInvalid{email} + } + + // if there is an allow list, then check email against allow list + if len(setting.Service.EmailDomainAllowList) > 0 && + !validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) { + return ErrEmailInvalid{email} + } + + return nil +} diff --git a/models/user/user.go b/models/user/user.go index 2e1d6af1763a8..0bdda8655fdc7 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -586,6 +586,16 @@ type CreateUserOverwriteOptions struct { // CreateUser creates record of a new user. func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { + return createUser(ctx, u, false, overwriteDefault...) +} + +// AdminCreateUser is used by admins to manually create users +func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { + return createUser(ctx, u, true, overwriteDefault...) +} + +// createUser creates record of a new user. +func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { if err = IsUsableUsername(u.Name); err != nil { return err } @@ -639,8 +649,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve return err } - if err := ValidateEmail(u.Email); err != nil { - return err + if createdByAdmin { + if err := ValidateEmailForAdmin(u.Email); err != nil { + return err + } + } else { + if err := ValidateEmail(u.Email); err != nil { + return err + } } ctx, committer, err := db.TxContext(ctx) diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go index e920177f8944c..0f90ec70cef44 100644 --- a/modules/base/natural_sort.go +++ b/modules/base/natural_sort.go @@ -4,85 +4,12 @@ package base import ( - "math/big" - "unicode/utf8" + "golang.org/x/text/collate" + "golang.org/x/text/language" ) // NaturalSortLess compares two strings so that they could be sorted in natural order func NaturalSortLess(s1, s2 string) bool { - var i1, i2 int - for { - rune1, j1, end1 := getNextRune(s1, i1) - rune2, j2, end2 := getNextRune(s2, i2) - if end1 || end2 { - return end1 != end2 && end1 - } - dec1 := isDecimal(rune1) - dec2 := isDecimal(rune2) - var less, equal bool - if dec1 && dec2 { - i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2) - } else if !dec1 && !dec2 { - equal = rune1 == rune2 - less = rune1 < rune2 - i1 = j1 - i2 = j2 - } else { - return rune1 < rune2 - } - if !equal { - return less - } - } -} - -func getNextRune(str string, pos int) (rune, int, bool) { - if pos < len(str) { - r, w := utf8.DecodeRuneInString(str[pos:]) - // Fallback to ascii - if r == utf8.RuneError { - r = rune(str[pos]) - w = 1 - } - return r, pos + w, false - } - return 0, pos, true -} - -func isDecimal(r rune) bool { - return '0' <= r && r <= '9' -} - -func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) { - d1, d2 := true, true - var dec1, dec2 string - for d1 || d2 { - if d1 { - r, j, end := getNextRune(str1, pos1) - if !end && isDecimal(r) { - dec1 += string(r) - pos1 = j - } else { - d1 = false - } - } - if d2 { - r, j, end := getNextRune(str2, pos2) - if !end && isDecimal(r) { - dec2 += string(r) - pos2 = j - } else { - d2 = false - } - } - } - less, equal = compareBigNumbers(dec1, dec2) - return pos1, pos2, less, equal -} - -func compareBigNumbers(dec1, dec2 string) (less, equal bool) { - d1, _ := big.NewInt(0).SetString(dec1, 10) - d2, _ := big.NewInt(0).SetString(dec2, 10) - cmp := d1.Cmp(d2) - return cmp < 0, cmp == 0 + c := collate.New(language.English, collate.Numeric) + return c.CompareString(s1, s2) < 0 } diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go index 91e864ad2af7e..f27a4eb53a249 100644 --- a/modules/base/natural_sort_test.go +++ b/modules/base/natural_sort_test.go @@ -11,7 +11,7 @@ import ( func TestNaturalSortLess(t *testing.T) { test := func(s1, s2 string, less bool) { - assert.Equal(t, less, NaturalSortLess(s1, s2)) + assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2) } test("v1.20.0", "v1.2.0", false) test("v1.20.0", "v1.29.0", true) @@ -20,4 +20,11 @@ func TestNaturalSortLess(t *testing.T) { test("a-1-a", "a-1-b", true) test("2", "12", true) test("a", "ab", true) + + test("A", "b", true) + test("a", "B", true) + + test("cafe", "café", true) + test("café", "cafe", false) + test("caff", "café", false) } diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index 3ca5eb36c6ede..0cd07dcdc897e 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -8,11 +8,11 @@ package git import ( "context" - "errors" "path/filepath" gitealog "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/osfs" @@ -52,7 +52,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { if err != nil { return nil, err } else if !isDir(repoPath) { - return nil, errors.New("no such file or directory") + return nil, util.NewNotExistErrorf("no such file or directory") } fs := osfs.New(repoPath) diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index 86b6a93567753..7f6512200baeb 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -9,10 +9,10 @@ package git import ( "bufio" "context" - "errors" "path/filepath" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) func init() { @@ -54,7 +54,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { if err != nil { return nil, err } else if !isDir(repoPath) { - return nil, errors.New("no such file or directory") + return nil, util.NewNotExistErrorf("no such file or directory") } // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go index f4af4993d97ea..edf5fc248f99e 100644 --- a/modules/graceful/manager_unix.go +++ b/modules/graceful/manager_unix.go @@ -59,7 +59,15 @@ func (g *Manager) start() { go func() { defer close(startupDone) // Wait till we're done getting all the listeners and then close the unused ones - g.createServerWaitGroup.Wait() + func() { + // FIXME: there is a fundamental design problem of the "manager" and the "wait group". + // If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned + // There is no clear solution besides a complete rewriting of the "manager" + defer func() { + _ = recover() + }() + g.createServerWaitGroup.Wait() + }() // Ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function _ = CloseProvidedListeners() g.notify(readyMsg) diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go index 0248dcb24d22b..ecf30af3f3005 100644 --- a/modules/graceful/manager_windows.go +++ b/modules/graceful/manager_windows.go @@ -150,7 +150,15 @@ func (g *Manager) awaitServer(limit time.Duration) bool { c := make(chan struct{}) go func() { defer close(c) - g.createServerWaitGroup.Wait() + func() { + // FIXME: there is a fundamental design problem of the "manager" and the "wait group". + // If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned + // There is no clear solution besides a complete rewriting of the "manager" + defer func() { + _ = recover() + }() + g.createServerWaitGroup.Wait() + }() }() if limit > 0 { select { diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index e19e22eea0e1e..2ddc2397fa191 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -16,14 +16,18 @@ import ( // Result a search result to display type Result struct { - RepoID int64 - Filename string - CommitID string - UpdatedUnix timeutil.TimeStamp - Language string - Color string - LineNumbers []int - FormattedLines template.HTML + RepoID int64 + Filename string + CommitID string + UpdatedUnix timeutil.TimeStamp + Language string + Color string + Lines []ResultLine +} + +type ResultLine struct { + Num int + FormattedContent template.HTML } type SearchResultLanguages = internal.SearchResultLanguages @@ -70,7 +74,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res var formattedLinesBuffer bytes.Buffer contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n") - lineNumbers := make([]int, len(contentLines)) + lines := make([]ResultLine, 0, len(contentLines)) index := startIndex for i, line := range contentLines { var err error @@ -93,21 +97,29 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res return nil, err } - lineNumbers[i] = startLineNum + i + lines = append(lines, ResultLine{Num: startLineNum + i}) index += len(line) } - highlighted, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String()) + // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting + hl, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String()) + highlightedLines := strings.Split(string(hl), "\n") + + // The lines outputted by highlight.Code might not match the original lines, because "highlight" removes the last `\n` + lines = lines[:min(len(highlightedLines), len(lines))] + highlightedLines = highlightedLines[:len(lines)] + for i := 0; i < len(lines); i++ { + lines[i].FormattedContent = template.HTML(highlightedLines[i]) + } return &Result{ - RepoID: result.RepoID, - Filename: result.Filename, - CommitID: result.CommitID, - UpdatedUnix: result.UpdatedUnix, - Language: result.Language, - Color: result.Color, - LineNumbers: lineNumbers, - FormattedLines: highlighted, + RepoID: result.RepoID, + Filename: result.Filename, + CommitID: result.CommitID, + UpdatedUnix: result.UpdatedUnix, + Language: result.Language, + Color: result.Color, + Lines: lines, }, nil } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3fc74959cadac..d30c8e521d031 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2092,6 +2092,8 @@ settings.branches.add_new_rule = Add New Rule settings.advanced_settings = Advanced Settings settings.wiki_desc = Enable Repository Wiki settings.use_internal_wiki = Use Built-In Wiki +settings.default_wiki_branch_name = Default Wiki Branch Name +settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch. settings.use_external_wiki = Use External Wiki settings.external_wiki_url = External Wiki URL settings.external_wiki_url_error = The external wiki URL is not a valid URL. @@ -2641,6 +2643,7 @@ find_file.no_matching = No matching file found error.csv.too_large = Can't render this file because it is too large. error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d. error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d. +error.broken_git_hook = Git hooks of this repository seem to be broken. Please follow the documentation to fix them, then push some commits to refresh the status. [graphs] component_loading = Loading %s... diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 4d1ecef61c102..af06a78642601 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -149,8 +149,8 @@ footer.software=ソフトウェアについて footer.links=リンク [heatmap] -number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 個の貢献 -no_contributions=貢献なし +number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 件の実績 +no_contributions=実績なし less=少 more=多 @@ -1510,7 +1510,7 @@ issues.role.member_helper=このユーザーはこのリポジトリを所有し issues.role.collaborator=共同作業者 issues.role.collaborator_helper=このユーザーはリポジトリ上で共同作業するように招待されています。 issues.role.first_time_contributor=初めての貢献者 -issues.role.first_time_contributor_helper=これは、このユーザーのリポジトリへの最初の貢献です。 +issues.role.first_time_contributor_helper=これは、このユーザーによるリポジトリへの最初の貢献です。 issues.role.contributor=貢献者 issues.role.contributor_helper=このユーザーは以前にリポジトリにコミットしています。 issues.re_request_review=レビューを再依頼 @@ -2011,7 +2011,8 @@ settings.mirror_settings.docs.more_information_if_disabled=プッシュミラー settings.mirror_settings.docs.doc_link_title=リポジトリをミラーリングするには? settings.mirror_settings.docs.doc_link_pull_section=ドキュメントの「リモートリポジトリからのプル」セクション。 settings.mirror_settings.docs.pulling_remote_title=リモートリポジトリからのプル -settings.mirror_settings.mirrored_repository=同期するリポジトリ +settings.mirror_settings.mirrored_repository=ミラー元のリポジトリ +settings.mirror_settings.pushed_repository=プッシュ先のリポジトリ settings.mirror_settings.direction=方向 settings.mirror_settings.direction.pull=プル settings.mirror_settings.direction.push=プッシュ @@ -3546,6 +3547,8 @@ runs.actors_no_select=すべてのアクター runs.status_no_select=すべてのステータス runs.no_results=一致する結果はありません。 runs.no_workflows=ワークフローはまだありません。 +runs.no_workflows.quick_start=Gitea Actions の始め方がわからない? ではクイックスタートガイドをご覧ください。 +runs.no_workflows.documentation=Gitea Actions の詳細については、ドキュメントを参照してください。 runs.no_runs=ワークフローはまだ実行されていません。 runs.empty_commit_message=(空のコミットメッセージ) diff --git a/public/assets/img/svg/gitea-twitter.svg b/public/assets/img/svg/gitea-twitter.svg index 5d11c6eaec764..5ed1e264cac1f 100644 --- a/public/assets/img/svg/gitea-twitter.svg +++ b/public/assets/img/svg/gitea-twitter.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 64315108b088c..986305d4235b9 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -133,7 +133,7 @@ func CreateUser(ctx *context.APIContext) { u.UpdatedUnix = u.CreatedUnix } - if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil { + if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil { if user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err) || db.IsErrNameReserved(err) || @@ -209,7 +209,7 @@ func EditUser(ctx *context.APIContext) { } if form.Email != nil { - if err := user_service.AddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil { + if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil { switch { case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): ctx.Error(http.StatusBadRequest, "EmailInvalid", err) diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index 53711bedebfc5..9e36ea0aed3bf 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -14,7 +14,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - files_service "code.gitea.io/gitea/services/repository/files" + commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" ) // NewCommitStatus creates a new CommitStatus @@ -64,7 +64,7 @@ func NewCommitStatus(ctx *context.APIContext) { Description: form.Description, Context: form.Context, } - if err := files_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil { + if err := commitstatus_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil { ctx.Error(http.StatusInternalServerError, "CreateCommitStatus", err) return } diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 4eafe3923dfb8..c5504126f865d 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -8,9 +8,11 @@ import ( "net/http" "strconv" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" repo_module "code.gitea.io/gitea/modules/repository" @@ -27,6 +29,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { // We don't rely on RepoAssignment here because: // a) we don't need the git repo in this function + // OUT OF DATE: we do need the git repo to sync the branch to the db now. // b) our update function will likely change the repository in the db so we will need to refresh it // c) we don't always need the repo @@ -34,7 +37,11 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { repoName := ctx.Params(":repo") // defer getting the repository at this point - as we should only retrieve it if we're going to call update - var repo *repo_model.Repository + var ( + repo *repo_model.Repository + gitRepo *git.Repository + ) + defer gitRepo.Close() // it's safe to call Close on a nil pointer updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs)) wasEmpty := false @@ -87,6 +94,63 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { }) return } + + branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates)) + for _, update := range updates { + if !update.RefFullName.IsBranch() { + continue + } + if repo == nil { + repo = loadRepository(ctx, ownerName, repoName) + if ctx.Written() { + return + } + wasEmpty = repo.IsEmpty + } + + if update.IsDelRef() { + if err := git_model.AddDeletedBranch(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil { + log.Error("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } else { + branchesToSync = append(branchesToSync, update) + } + } + if len(branchesToSync) > 0 { + if gitRepo == nil { + var err error + gitRepo, err = gitrepo.OpenRepository(ctx, repo) + if err != nil { + log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } + + var ( + branchNames = make([]string, 0, len(branchesToSync)) + commitIDs = make([]string, 0, len(branchesToSync)) + ) + for _, update := range branchesToSync { + branchNames = append(branchNames, update.RefFullName.BranchName()) + commitIDs = append(commitIDs, update.NewCommitID) + } + + if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, func(commitID string) (*git.Commit, error) { + return gitRepo.GetCommit(commitID) + }); err != nil { + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ + Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } } // Handle Push Options diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index a34e0d0f0dc5b..671a0d8885ce0 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -177,7 +177,7 @@ func NewUserPost(ctx *context.Context) { u.MustChangePassword = form.MustChangePassword } - if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil { + if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil { switch { case user_model.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true @@ -412,7 +412,7 @@ func EditUserPost(ctx *context.Context) { } if form.Email != "" { - if err := user_service.AddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil { + if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil { switch { case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 04e410543d2ce..da6bef207ad7c 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -123,9 +123,21 @@ func resetLocale(ctx *context.Context, u *user_model.User) error { return nil } +func RedirectAfterLogin(ctx *context.Context) { + redirectTo := ctx.FormString("redirect_to") + if redirectTo == "" { + redirectTo = ctx.GetSiteCookie("redirect_to") + } + middleware.DeleteRedirectToCookie(ctx.Resp) + nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL) + if setting.LandingPageURL == setting.LandingPageLogin { + nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page + } + ctx.RedirectToFirst(redirectTo, nextRedirectTo) +} + func CheckAutoLogin(ctx *context.Context) bool { - // Check auto-login - isSucceed, err := autoSignIn(ctx) + isSucceed, err := autoSignIn(ctx) // try to auto-login if err != nil { if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) { ctx.Flash.Error(ctx.Tr("auth.remember_me.compromised"), true) @@ -138,17 +150,10 @@ func CheckAutoLogin(ctx *context.Context) bool { redirectTo := ctx.FormString("redirect_to") if len(redirectTo) > 0 { middleware.SetRedirectToCookie(ctx.Resp, redirectTo) - } else { - redirectTo = ctx.GetSiteCookie("redirect_to") } if isSucceed { - middleware.DeleteRedirectToCookie(ctx.Resp) - nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL) - if setting.LandingPageURL == setting.LandingPageLogin { - nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page - } - ctx.RedirectToFirst(redirectTo, nextRedirectTo) + RedirectAfterLogin(ctx) return true } @@ -163,6 +168,11 @@ func SignIn(ctx *context.Context) { return } + if ctx.IsSigned { + RedirectAfterLogin(ctx) + return + } + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignIn", err) diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go new file mode 100644 index 0000000000000..c6afbf877c080 --- /dev/null +++ b/routers/web/auth/auth_test.go @@ -0,0 +1,43 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestUserLogin(t *testing.T) { + ctx, resp := contexttest.MockContext(t, "/user/login") + SignIn(ctx) + assert.Equal(t, http.StatusOK, resp.Code) + + ctx, resp = contexttest.MockContext(t, "/user/login") + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, http.StatusSeeOther, resp.Code) + assert.Equal(t, "/", test.RedirectURL(resp)) + + ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to=/other") + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, "/other", test.RedirectURL(resp)) + + ctx, resp = contexttest.MockContext(t, "/user/login") + ctx.Req.AddCookie(&http.Cookie{Name: "redirect_to", Value: "/other-cookie"}) + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, "/other-cookie", test.RedirectURL(resp)) + + ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to="+url.QueryEscape("https://example.com")) + ctx.IsSigned = true + SignIn(ctx) + assert.Equal(t, "/", test.RedirectURL(resp)) +} diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index 64212291e162e..bce807aacdc9f 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -10,6 +10,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" pull_model "code.gitea.io/gitea/models/pull" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -19,6 +20,7 @@ import ( "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" pull_service "code.gitea.io/gitea/services/pull" + user_service "code.gitea.io/gitea/services/user" ) const ( @@ -203,6 +205,10 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori return } ctx.Data["AfterCommitID"] = pullHeadCommitID + ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool { + return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) + } + if origin == "diff" { ctx.HTML(http.StatusOK, tplDiffConversation) } else if origin == "timeline" { diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index f0caf199a2979..b54d29c580b4f 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -35,6 +35,7 @@ import ( "code.gitea.io/gitea/services/forms" repo_service "code.gitea.io/gitea/services/repository" archiver_service "code.gitea.io/gitea/services/repository/archiver" + commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" ) const ( @@ -634,30 +635,14 @@ func SearchRepo(ctx *context.Context) { return } - // collect the latest commit of each repo - // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment - repoBranchNames := make(map[int64]string, len(repos)) - for _, repo := range repos { - repoBranchNames[repo.ID] = repo.DefaultBranch - } - - repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames) + latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos) if err != nil { - log.Error("FindBranchesByRepoAndBranchName: %v", err) - return - } - - // call the database O(1) times to get the commit statuses for all repos - repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll) - if err != nil { - log.Error("GetLatestCommitStatusForPairs: %v", err) + log.Error("FindReposLastestCommitStatuses: %v", err) return } results := make([]*repo_service.WebSearchRepository, len(repos)) for i, repo := range repos { - latestCommitStatus := git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]) - results[i] = &repo_service.WebSearchRepository{ Repository: &api.Repository{ ID: repo.ID, @@ -671,8 +656,11 @@ func SearchRepo(ctx *context.Context) { Link: repo.Link(), Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, }, - LatestCommitStatus: latestCommitStatus, - LocaleLatestCommitStatus: latestCommitStatus.LocaleString(ctx.Locale), + } + + if latestCommitStatuses[i] != nil { + results[i].LatestCommitStatus = latestCommitStatuses[i] + results[i].LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(ctx.Locale) } } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 992a980d9e641..e045e3b8dcc09 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -488,6 +488,13 @@ func SettingsPost(ctx *context.Context) { } } + if form.DefaultWikiBranch != "" { + if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil { + log.Error("ChangeDefaultWikiBranch failed, err: %v", err) + ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch")) + } + } + if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { if !validation.IsValidExternalURL(form.ExternalTrackerURL) { ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 4df10fbea1adb..d47c926fa1780 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -998,6 +998,8 @@ func renderHomeCode(ctx *context.Context) { return } + checkOutdatedBranch(ctx) + checkCitationFile(ctx, entry) if ctx.Written() { return @@ -1064,6 +1066,31 @@ func renderHomeCode(ctx *context.Context) { ctx.HTML(http.StatusOK, tplRepoHome) } +func checkOutdatedBranch(ctx *context.Context) { + if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) { + return + } + + // get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName` + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) + if err != nil { + log.Error("GetBranchCommitID: %v", err) + // Don't return an error page, as it can be rechecked the next time the user opens the page. + return + } + + dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName) + if err != nil { + log.Error("GetBranch: %v", err) + // Don't return an error page, as it can be rechecked the next time the user opens the page. + return + } + + if dbBranch.CommitID != commit.ID.String() { + ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true) + } +} + // RenderUserCards render a page show users according the input template func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) { page := ctx.FormInt("page") diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 91cf727e2c5ce..88b63da88d986 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -93,17 +93,32 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) } func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) { - wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) - if err != nil { - ctx.ServerError("OpenRepository", err) - return nil, nil, err + wikiGitRepo, errGitRepo := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) + if errGitRepo != nil { + ctx.ServerError("OpenRepository", errGitRepo) + return nil, nil, errGitRepo } - commit, err := wikiRepo.GetBranchCommit(wiki_service.DefaultBranch) - if err != nil { - return wikiRepo, nil, err + commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) + if git.IsErrNotExist(errCommit) { + // if the default branch recorded in database is out of sync, then re-sync it + gitRepoDefaultBranch, errBranch := wikiGitRepo.GetDefaultBranch() + if errBranch != nil { + return wikiGitRepo, nil, errBranch + } + // update the default branch in the database + errDb := repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch") + if errDb != nil { + return wikiGitRepo, nil, errDb + } + ctx.Repo.Repository.DefaultWikiBranch = gitRepoDefaultBranch + // retry to get the commit from the correct default branch + commit, errCommit = wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) } - return wikiRepo, commit, nil + if errCommit != nil { + return wikiGitRepo, nil, errCommit + } + return wikiGitRepo, commit, nil } // wikiContentsByEntry returns the contents of the wiki page referenced by the @@ -316,7 +331,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount return wikiRepo, entry @@ -368,7 +383,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["footerContent"] = "" // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount // get page @@ -380,7 +395,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) // get Commit Count commitsHistory, err := wikiRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: wiki_service.DefaultBranch, + Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, Page: page, }) @@ -402,20 +417,17 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) func renderEditPage(ctx *context.Context) { wikiRepo, commit, err := findWikiRepoCommit(ctx) - if err != nil { + defer func() { if wikiRepo != nil { - wikiRepo.Close() + _ = wikiRepo.Close() } + }() + if err != nil { if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return } - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() // get requested pagename pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*")) @@ -584,17 +596,15 @@ func WikiPages(ctx *context.Context) { ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived wikiRepo, commit, err := findWikiRepoCommit(ctx) - if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } - return - } defer func() { if wikiRepo != nil { - wikiRepo.Close() + _ = wikiRepo.Close() } }() + if err != nil { + ctx.Redirect(ctx.Repo.RepoLink + "/wiki") + return + } entries, err := commit.ListEntries() if err != nil { diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 49c83cfef59e7..52e216e6a0ac2 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -9,6 +9,7 @@ import ( "net/url" "testing" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" @@ -79,7 +80,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) { func TestWiki(t *testing.T) { unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages") + ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki") ctx.SetParams("*", "Home") contexttest.LoadRepo(t, ctx, 1) Wiki(ctx) @@ -221,3 +222,32 @@ func TestWikiRaw(t *testing.T) { } } } + +func TestDefaultWikiBranch(t *testing.T) { + unittest.PrepareTestEnv(t) + + assert.NoError(t, repo_model.UpdateRepositoryCols(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"})) + + ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki") + ctx.SetParams("*", "Home") + contexttest.LoadRepo(t, ctx, 1) + assert.Equal(t, "wrong-branch", ctx.Repo.Repository.DefaultWikiBranch) + Wiki(ctx) // after the visiting, the out-of-sync database record will update the branch name to "master" + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.Equal(t, "master", ctx.Repo.Repository.DefaultWikiBranch) + + // invalid branch name should fail + assert.Error(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "the bad name")) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.Equal(t, "master", repo.DefaultWikiBranch) + + // the same branch name, should succeed (actually a no-op) + assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "master")) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.Equal(t, "master", repo.DefaultWikiBranch) + + // change to another name + assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "main")) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.Equal(t, "main", repo.DefaultWikiBranch) +} diff --git a/services/actions/auth.go b/services/actions/auth.go index e0f9a9015dcda..8e934d89a84c8 100644 --- a/services/actions/auth.go +++ b/services/actions/auth.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -21,17 +22,41 @@ type actionsClaims struct { TaskID int64 RunID int64 JobID int64 + Ac string `json:"ac"` } +type actionsCacheScope struct { + Scope string + Permission actionsCachePermission +} + +type actionsCachePermission int + +const ( + actionsCachePermissionRead = 1 << iota + actionsCachePermissionWrite +) + func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) { now := time.Now() + ac, err := json.Marshal(&[]actionsCacheScope{ + { + Scope: "", + Permission: actionsCachePermissionWrite, + }, + }) + if err != nil { + return "", err + } + claims := actionsClaims{ RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)), NotBefore: jwt.NewNumericDate(now), }, Scp: fmt.Sprintf("Actions.Results:%d:%d", runID, jobID), + Ac: string(ac), TaskID: taskID, RunID: runID, JobID: jobID, diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go index 1f62f17f52a86..f73ae8ae4c36a 100644 --- a/services/actions/auth_test.go +++ b/services/actions/auth_test.go @@ -7,6 +7,7 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" "github.com/golang-jwt/jwt/v5" @@ -29,6 +30,14 @@ func TestCreateAuthorizationToken(t *testing.T) { taskIDClaim, ok := claims["TaskID"] assert.True(t, ok, "Has TaskID claim in jwt token") assert.Equal(t, float64(taskID), taskIDClaim, "Supplied taskid must match stored one") + acClaim, ok := claims["ac"] + assert.True(t, ok, "Has ac claim in jwt token") + ac, ok := acClaim.(string) + assert.True(t, ok, "ac claim is a string for buildx gha cache") + scopes := []actionsCacheScope{} + err = json.Unmarshal([]byte(ac), &scopes) + assert.NoError(t, err, "ac claim is a json list for buildx gha cache") + assert.GreaterOrEqual(t, len(scopes), 1, "Expected at least one action cache scope for buildx gha cache") } func TestParseAuthorizationToken(t *testing.T) { diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index b248af1d01834..b0d848b5add15 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -486,6 +486,10 @@ func handleSchedules( // DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error { + if repo.IsEmpty { + return nil + } + gitRepo, err := gitrepo.OpenRepository(context.Background(), repo) if err != nil { return fmt.Errorf("git.OpenRepository: %w", err) diff --git a/services/agit/agit.go b/services/agit/agit.go index 2233fe854746a..eb3bafa906557 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "os" + "strconv" "strings" issues_model "code.gitea.io/gitea/models/issues" @@ -21,26 +22,17 @@ import ( // ProcReceive handle proc receive work func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) { - // TODO: Add more options? - var ( - topicBranch string - title string - description string - forcePush bool - ) - results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs)) - - ownerName := repo.OwnerName - repoName := repo.Name - - topicBranch = opts.GitPushOptions["topic"] - _, forcePush = opts.GitPushOptions["force-push"] + topicBranch := opts.GitPushOptions["topic"] + forcePush, _ := strconv.ParseBool(opts.GitPushOptions["force-push"]) + title := strings.TrimSpace(opts.GitPushOptions["title"]) + description := strings.TrimSpace(opts.GitPushOptions["description"]) // TODO: Add more options? objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) + userName := strings.ToLower(opts.UserName) pusher, err := user_model.GetUserByID(ctx, opts.UserID) if err != nil { - return nil, fmt.Errorf("Failed to get user. Error: %w", err) + return nil, fmt.Errorf("failed to get user. Error: %w", err) } for i := range opts.OldCommitIDs { @@ -85,9 +77,6 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. continue } - var headBranch string - userName := strings.ToLower(opts.UserName) - if len(curentTopicBranch) == 0 { curentTopicBranch = topicBranch } @@ -95,6 +84,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. // because different user maybe want to use same topic, // So it's better to make sure the topic branch name // has user name prefix + var headBranch string if !strings.HasPrefix(curentTopicBranch, userName+"/") { headBranch = userName + "/" + curentTopicBranch } else { @@ -104,21 +94,26 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit) if err != nil { if !issues_model.IsErrPullRequestNotExist(err) { - return nil, fmt.Errorf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %w", ownerName, repoName, err) + return nil, fmt.Errorf("failed to get unmerged agit flow pull request in repository: %s Error: %w", repo.FullName(), err) } - // create a new pull request - if len(title) == 0 { - var has bool - title, has = opts.GitPushOptions["title"] - if !has || len(title) == 0 { - commit, err := gitRepo.GetCommit(opts.NewCommitIDs[i]) - if err != nil { - return nil, fmt.Errorf("Failed to get commit %s in repository: %s/%s Error: %w", opts.NewCommitIDs[i], ownerName, repoName, err) - } - title = strings.Split(commit.CommitMessage, "\n")[0] + var commit *git.Commit + if title == "" || description == "" { + commit, err = gitRepo.GetCommit(opts.NewCommitIDs[i]) + if err != nil { + return nil, fmt.Errorf("failed to get commit %s in repository: %s Error: %w", opts.NewCommitIDs[i], repo.FullName(), err) } - description = opts.GitPushOptions["description"] + } + + // create a new pull request + if title == "" { + title = strings.Split(commit.CommitMessage, "\n")[0] + } + if description == "" { + _, description, _ = strings.Cut(commit.CommitMessage, "\n\n") + } + if description == "" { + description = title } prIssue := &issues_model.Issue{ @@ -160,12 +155,12 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. // update exist pull request if err := pr.LoadBaseRepo(ctx); err != nil { - return nil, fmt.Errorf("Unable to load base repository for PR[%d] Error: %w", pr.ID, err) + return nil, fmt.Errorf("unable to load base repository for PR[%d] Error: %w", pr.ID, err) } oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) if err != nil { - return nil, fmt.Errorf("Unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err) + return nil, fmt.Errorf("unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err) } if oldCommitID == opts.NewCommitIDs[i] { @@ -179,9 +174,11 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. } if !forcePush { - output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()}) + output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1"). + AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]). + RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()}) if err != nil { - return nil, fmt.Errorf("Fail to detect force push: %w", err) + return nil, fmt.Errorf("failed to detect force push: %w", err) } else if len(output) > 0 { results = append(results, private.HookProcReceiveRefResult{ OriginalRef: opts.RefFullNames[i], @@ -195,17 +192,13 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. pr.HeadCommitID = opts.NewCommitIDs[i] if err = pull_service.UpdateRef(ctx, pr); err != nil { - return nil, fmt.Errorf("Failed to update pull ref. Error: %w", err) + return nil, fmt.Errorf("failed to update pull ref. Error: %w", err) } pull_service.AddToTaskQueue(ctx, pr) - pusher, err := user_model.GetUserByID(ctx, opts.UserID) - if err != nil { - return nil, fmt.Errorf("Failed to get user. Error: %w", err) - } err = pr.LoadIssue(ctx) if err != nil { - return nil, fmt.Errorf("Failed to load pull issue. Error: %w", err) + return nil, fmt.Errorf("failed to load pull issue. Error: %w", err) } comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i]) if err == nil && comment != nil { diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index 431017a30dcb6..d3e6de7efe4f9 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -7,6 +7,7 @@ package contexttest import ( gocontext "context" "io" + "maps" "net/http" "net/http/httptest" "net/url" @@ -36,7 +37,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request { } requestURL, err := url.Parse(path) assert.NoError(t, err) - req := &http.Request{Method: method, URL: requestURL, Form: url.Values{}} + req := &http.Request{Method: method, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}} req = req.WithContext(middleware.WithContextData(req.Context())) return req } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 8c3e458d2ff6b..e45a2a1695522 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -133,6 +133,7 @@ type RepoSettingForm struct { EnableCode bool EnableWiki bool EnableExternalWiki bool + DefaultWikiBranch string ExternalWikiURL string EnableIssues bool EnableExternalTracker bool diff --git a/services/repository/branch.go b/services/repository/branch.go index 55cedf5d84bfb..402814fb9a343 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -221,44 +221,91 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri return err } -// syncBranchToDB sync the branch information in the database. It will try to update the branch first, -// if updated success with affect records > 0, then all are done. Because that means the branch has been in the database. -// If no record is affected, that means the branch does not exist in database. So there are two possibilities. -// One is this is a new branch, then we just need to insert the record. Another is the branches haven't been synced, -// then we need to sync all the branches into database. -func syncBranchToDB(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error { - cnt, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit) - if err != nil { - return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err) - } - if cnt > 0 { // This means branch does exist, so it's a normal update. It also means the branch has been synced. - return nil - } +// SyncBranchesToDB sync the branch information in the database. +// It will check whether the branches of the repository have never been synced before. +// If so, it will sync all branches of the repository. +// Otherwise, it will sync the branches that need to be updated. +func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, commitIDs []string, getCommit func(commitID string) (*git.Commit, error)) error { + // Some designs that make the code look strange but are made for performance optimization purposes: + // 1. Sync branches in a batch to reduce the number of DB queries. + // 2. Lazy load commit information since it may be not necessary. + // 3. Exit early if synced all branches of git repo when there's no branch in DB. + // 4. Check the branches in DB if they are already synced. + // + // If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once. + // See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27 + // For the first batch, it will hit optimization 3. + // For other batches, it will hit optimization 4. + + if len(branchNames) != len(commitIDs) { + return fmt.Errorf("branchNames and commitIDs length not match") + } + + return db.WithTx(ctx, func(ctx context.Context) error { + branches, err := git_model.GetBranches(ctx, repoID, branchNames) + if err != nil { + return fmt.Errorf("git_model.GetBranches: %v", err) + } - // if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21, - // we cannot simply insert the branch but need to check we have branches or not - hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{ - RepoID: repoID, - IsDeletedBranch: optional.Some(false), - }.ToConds()) - if err != nil { - return err - } - if !hasBranch { - if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil { - return fmt.Errorf("repo_module.SyncRepoBranches %d:%s failed: %v", repoID, branchName, err) + if len(branches) == 0 { + // if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21, + // we cannot simply insert the branch but need to check we have branches or not + hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{ + RepoID: repoID, + IsDeletedBranch: optional.Some(false), + }.ToConds()) + if err != nil { + return err + } + if !hasBranch { + if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil { + return fmt.Errorf("repo_module.SyncRepoBranches %d failed: %v", repoID, err) + } + return nil + } } - return nil - } - // if database have branches but not this branch, it means this is a new branch - return db.Insert(ctx, &git_model.Branch{ - RepoID: repoID, - Name: branchName, - CommitID: commit.ID.String(), - CommitMessage: commit.Summary(), - PusherID: pusherID, - CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), + branchMap := make(map[string]*git_model.Branch, len(branches)) + for _, branch := range branches { + branchMap[branch.Name] = branch + } + + newBranches := make([]*git_model.Branch, 0, len(branchNames)) + + for i, branchName := range branchNames { + commitID := commitIDs[i] + branch, exist := branchMap[branchName] + if exist && branch.CommitID == commitID { + continue + } + + commit, err := getCommit(branchName) + if err != nil { + return fmt.Errorf("get commit of %s failed: %v", branchName, err) + } + + if exist { + if _, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit); err != nil { + return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err) + } + return nil + } + + // if database have branches but not this branch, it means this is a new branch + newBranches = append(newBranches, &git_model.Branch{ + RepoID: repoID, + Name: branchName, + CommitID: commit.ID.String(), + CommitMessage: commit.Summary(), + PusherID: pusherID, + CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), + }) + } + + if len(newBranches) > 0 { + return db.Insert(ctx, newBranches) + } + return nil }) } diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go new file mode 100644 index 0000000000000..145fc7d53c4e9 --- /dev/null +++ b/services/repository/commitstatus/commitstatus.go @@ -0,0 +1,135 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package commitstatus + +import ( + "context" + "crypto/sha256" + "fmt" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/automerge" +) + +func getCacheKey(repoID int64, brancheName string) string { + hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%d:%s", repoID, brancheName))) + return fmt.Sprintf("commit_status:%x", hashBytes) +} + +func updateCommitStatusCache(ctx context.Context, repoID int64, branchName string, status api.CommitStatusState) error { + c := cache.GetCache() + return c.Put(getCacheKey(repoID, branchName), string(status), 3*24*60) +} + +func deleteCommitStatusCache(ctx context.Context, repoID int64, branchName string) error { + c := cache.GetCache() + return c.Delete(getCacheKey(repoID, branchName)) +} + +// CreateCommitStatus creates a new CommitStatus given a bunch of parameters +// NOTE: All text-values will be trimmed from whitespaces. +// Requires: Repo, Creator, SHA +func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error { + repoPath := repo.RepoPath() + + // confirm that commit is exist + gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) + if err != nil { + return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err) + } + defer closer.Close() + + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) + + commit, err := gitRepo.GetCommit(sha) + if err != nil { + return fmt.Errorf("GetCommit[%s]: %w", sha, err) + } + if len(sha) != objectFormat.FullLength() { + // use complete commit sha + sha = commit.ID.String() + } + + if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{ + Repo: repo, + Creator: creator, + SHA: commit.ID, + CommitStatus: status, + }); err != nil { + return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) + } + + defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) + if err != nil { + return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err) + } + + 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 { + log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) + } + } + + if status.State.IsSuccess() { + if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil { + return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) + } + } + + return nil +} + +// FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache +func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) { + results := make([]*git_model.CommitStatus, len(repos)) + c := cache.GetCache() + + for i, repo := range repos { + status, ok := c.Get(getCacheKey(repo.ID, repo.DefaultBranch)).(string) + if ok && status != "" { + results[i] = &git_model.CommitStatus{State: api.CommitStatusState(status)} + } + } + + // collect the latest commit of each repo + // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment + repoBranchNames := make(map[int64]string, len(repos)) + for i, repo := range repos { + if results[i] == nil { + repoBranchNames[repo.ID] = repo.DefaultBranch + } + } + + repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames) + if err != nil { + return nil, fmt.Errorf("FindBranchesByRepoAndBranchName: %v", err) + } + + // call the database O(1) times to get the commit statuses for all repos + repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll) + if err != nil { + return nil, fmt.Errorf("GetLatestCommitStatusForPairs: %v", err) + } + + for i, repo := range repos { + if results[i] == nil { + results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]) + if results[i].State != "" { + if err := updateCommitStatusCache(ctx, repo.ID, repo.DefaultBranch, results[i].State); err != nil { + log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) + } + } + } + } + + return results, nil +} diff --git a/services/repository/create.go b/services/repository/create.go index c47ce9c413647..8d8c39197dece 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -173,6 +173,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re } repo.DefaultBranch = setting.Repository.DefaultBranch + repo.DefaultWikiBranch = setting.Repository.DefaultBranch if len(opts.DefaultBranch) > 0 { repo.DefaultBranch = opts.DefaultBranch @@ -240,6 +241,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt TrustModel: opts.TrustModel, IsMirror: opts.IsMirror, DefaultBranch: opts.DefaultBranch, + DefaultWikiBranch: setting.Repository.DefaultBranch, ObjectFormatName: opts.ObjectFormatName, } diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go index 512aec7c814c4..e0dad292732e3 100644 --- a/services/repository/files/commit.go +++ b/services/repository/files/commit.go @@ -5,61 +5,13 @@ package files import ( "context" - "fmt" asymkey_model "code.gitea.io/gitea/models/asymkey" - git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/services/automerge" ) -// CreateCommitStatus creates a new CommitStatus given a bunch of parameters -// NOTE: All text-values will be trimmed from whitespaces. -// Requires: Repo, Creator, SHA -func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error { - repoPath := repo.RepoPath() - - // confirm that commit is exist - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) - if err != nil { - return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err) - } - defer closer.Close() - - objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) - - commit, err := gitRepo.GetCommit(sha) - if err != nil { - gitRepo.Close() - return fmt.Errorf("GetCommit[%s]: %w", sha, err) - } else if len(sha) != objectFormat.FullLength() { - // use complete commit sha - sha = commit.ID.String() - } - gitRepo.Close() - - if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{ - Repo: repo, - Creator: creator, - SHA: commit.ID, - CommitStatus: status, - }); err != nil { - return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) - } - - if status.State.IsSuccess() { - if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil { - return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err) - } - } - - return nil -} - // CountDivergingCommits determines how many commits a branch is ahead or behind the repository's base branch func CountDivergingCommits(ctx context.Context, repo *repo_model.Repository, branch string) (*git.DivergeObject, error) { divergence, err := git.GetDivergingCommits(ctx, repo.RepoPath(), repo.DefaultBranch, branch) diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 9500b8f46d19b..95e7c7087c03d 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -220,7 +220,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref } } // Handle links - if entry.IsRegular() || entry.IsLink() { + if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() { downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath)) if err != nil { return nil, err diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 51fdd90f5400e..aae2ddc1200ea 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -25,6 +25,54 @@ import ( "code.gitea.io/gitea/modules/util" ) +func cloneWiki(ctx context.Context, u *user_model.User, opts migration.MigrateOptions, migrateTimeout time.Duration) (string, error) { + wikiPath := repo_model.WikiPath(u.Name, opts.RepoName) + wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr) + if wikiRemotePath == "" { + return "", nil + } + + if err := util.RemoveAll(wikiPath); err != nil { + return "", fmt.Errorf("failed to remove existing wiki dir %q, err: %w", wikiPath, err) + } + + cleanIncompleteWikiPath := func() { + if err := util.RemoveAll(wikiPath); err != nil { + log.Error("Failed to remove incomplete wiki dir %q, err: %v", wikiPath, err) + } + } + if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ + Mirror: true, + Quiet: true, + Timeout: migrateTimeout, + SkipTLSVerify: setting.Migrations.SkipTLSVerify, + }); err != nil { + log.Error("Clone wiki failed, err: %v", err) + cleanIncompleteWikiPath() + return "", err + } + + if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { + cleanIncompleteWikiPath() + return "", err + } + + wikiRepo, err := git.OpenRepository(ctx, wikiPath) + if err != nil { + cleanIncompleteWikiPath() + return "", fmt.Errorf("failed to open wiki repo %q, err: %w", wikiPath, err) + } + defer wikiRepo.Close() + + defaultBranch, err := wikiRepo.GetDefaultBranch() + if err != nil { + cleanIncompleteWikiPath() + return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", wikiPath, err) + } + + return defaultBranch, nil +} + // MigrateRepositoryGitData starts migrating git related data after created migrating repository func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, repo *repo_model.Repository, opts migration.MigrateOptions, @@ -44,21 +92,20 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second - var err error - if err = util.RemoveAll(repoPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err) + if err := util.RemoveAll(repoPath); err != nil { + return repo, fmt.Errorf("failed to remove existing repo dir %q, err: %w", repoPath, err) } - if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{ + if err := git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{ Mirror: true, Quiet: true, Timeout: migrateTimeout, SkipTLSVerify: setting.Migrations.SkipTLSVerify, }); err != nil { if errors.Is(err, context.DeadlineExceeded) { - return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err) + return repo, fmt.Errorf("clone timed out, consider increasing [git.timeout] MIGRATE in app.ini, underlying err: %w", err) } - return repo, fmt.Errorf("Clone: %w", err) + return repo, fmt.Errorf("clone error: %w", err) } if err := git.WriteCommitGraph(ctx, repoPath); err != nil { @@ -66,37 +113,18 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } if opts.Wiki { - wikiPath := repo_model.WikiPath(u.Name, opts.RepoName) - wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr) - if len(wikiRemotePath) > 0 { - if err := util.RemoveAll(wikiPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) - } - - if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ - Mirror: true, - Quiet: true, - Timeout: migrateTimeout, - Branch: "master", - SkipTLSVerify: setting.Migrations.SkipTLSVerify, - }); err != nil { - log.Warn("Clone wiki: %v", err) - if err := util.RemoveAll(wikiPath); err != nil { - return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) - } - } else { - if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { - return repo, err - } - } + defaultWikiBranch, err := cloneWiki(ctx, u, opts, migrateTimeout) + if err != nil { + return repo, fmt.Errorf("clone wiki error: %w", err) } + repo.DefaultWikiBranch = defaultWikiBranch } if repo.OwnerID == u.ID { repo.Owner = u } - if err = repo_module.CheckDaemonExportOK(ctx, repo); err != nil { + if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { return repo, fmt.Errorf("checkDaemonExportOK: %w", err) } diff --git a/services/repository/push.go b/services/repository/push.go index 9aaf0e1c9bcab..89a3127902a59 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -11,7 +11,6 @@ import ( "time" "code.gitea.io/gitea/models/db" - git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" @@ -259,10 +258,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] } - if err = syncBranchToDB(ctx, repo.ID, opts.PusherID, branch, newCommit); err != nil { - return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err) - } - notify_service.PushCommits(ctx, pusher, repo, opts, commits) // Cache for big repository @@ -275,10 +270,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { // close all related pulls log.Error("close related pull request failed: %v", err) } - - if err := git_model.AddDeletedBranch(ctx, repo.ID, branch, pusher.ID); err != nil { - return fmt.Errorf("AddDeletedBranch %s:%s failed: %v", repo.FullName(), branch, err) - } } // Even if user delete a branch on a repository which he didn't watch, he will be watch that. diff --git a/services/user/email.go b/services/user/email.go index 07e19bc6883b8..5c0de708e9a8e 100644 --- a/services/user/email.go +++ b/services/user/email.go @@ -14,12 +14,13 @@ import ( "code.gitea.io/gitea/modules/util" ) -func AddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error { +// AdminAddOrSetPrimaryEmailAddress is used by admins to add or set a user's primary email address +func AdminAddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error { if strings.EqualFold(u.Email, emailStr) { return nil } - if err := user_model.ValidateEmail(emailStr); err != nil { + if err := user_model.ValidateEmailForAdmin(emailStr); err != nil { return err } diff --git a/services/user/email_test.go b/services/user/email_test.go index 8f419b69f991f..b40f86b6a68fe 100644 --- a/services/user/email_test.go +++ b/services/user/email_test.go @@ -10,11 +10,13 @@ import ( organization_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "github.com/gobwas/glob" "github.com/stretchr/testify/assert" ) -func TestAddOrSetPrimaryEmailAddress(t *testing.T) { +func TestAdminAddOrSetPrimaryEmailAddress(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 27}) @@ -28,7 +30,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) { assert.NotEqual(t, "new-primary@example.com", primary.Email) assert.Equal(t, user.Email, primary.Email) - assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com")) + assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com")) primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID) assert.NoError(t, err) @@ -39,7 +41,19 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) { assert.NoError(t, err) assert.Len(t, emails, 2) - assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com")) + setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")} + defer func() { + setting.Service.EmailDomainAllowList = []glob.Glob{} + }() + + assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary2@example2.com")) + + primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID) + assert.NoError(t, err) + assert.Equal(t, "new-primary2@example2.com", primary.Email) + assert.Equal(t, user.Email, primary.Email) + + assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com")) primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID) assert.NoError(t, err) @@ -48,7 +62,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) { emails, err = user_model.GetEmailAddresses(db.DefaultContext, user.ID) assert.NoError(t, err) - assert.Len(t, emails, 2) + assert.Len(t, emails, 3) } func TestReplacePrimaryEmailAddress(t *testing.T) { diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 50d52d3140fe8..6f1ca120b0a27 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -6,18 +6,22 @@ package wiki import ( "context" + "errors" "fmt" "os" "strings" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/sync" + "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" repo_service "code.gitea.io/gitea/services/repository" ) @@ -25,10 +29,7 @@ import ( // TODO: use clustered lock (unique queue? or *abuse* cache) var wikiWorkingPool = sync.NewExclusivePool() -const ( - DefaultRemote = "origin" - DefaultBranch = "master" -) +const DefaultRemote = "origin" // InitWiki initializes a wiki for repository, // it does nothing when repository already has wiki. @@ -41,25 +42,25 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error { return fmt.Errorf("InitRepository: %w", err) } else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) - } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil { - return fmt.Errorf("unable to set default wiki branch to master: %w", err) + } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix + repo.DefaultWikiBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil { + return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err) } return nil } // prepareGitPath try to find a suitable file path with file name by the given raw wiki name. // return: existence, prepared file path with name, error -func prepareGitPath(gitRepo *git.Repository, wikiPath WebPath) (bool, string, error) { +func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) { unescaped := string(wikiPath) + ".md" gitPath := WebPathToGitPath(wikiPath) // Look for both files - filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, gitPath) + filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath) if err != nil { - if strings.Contains(err.Error(), "Not a valid object name master") { - return false, gitPath, nil + if strings.Contains(err.Error(), "Not a valid object name") { + return false, gitPath, nil // branch doesn't exist } - log.Error("%v", err) + log.Error("Wiki LsTree failed, err: %v", err) return false, gitPath, err } @@ -95,7 +96,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return fmt.Errorf("InitWiki: %w", err) } - hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), DefaultBranch) + hasDefaultBranch := git.IsBranchExist(ctx, repo.WikiPath(), repo.DefaultWikiBranch) basePath, err := repo_module.CreateTemporaryPath("update-wiki") if err != nil { @@ -112,8 +113,8 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model Shared: true, } - if hasMasterBranch { - cloneOpts.Branch = DefaultBranch + if hasDefaultBranch { + cloneOpts.Branch = repo.DefaultWikiBranch } if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil { @@ -128,14 +129,14 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model } defer gitRepo.Close() - if hasMasterBranch { + if hasDefaultBranch { if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil { log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err) return fmt.Errorf("fnable to read HEAD tree to index in: %s %w", basePath, err) } } - isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, newWikiName) + isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, newWikiName) if err != nil { return err } @@ -151,7 +152,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model isOldWikiExist := true oldWikiPath := newWikiPath if oldWikiName != newWikiName { - isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, oldWikiName) + isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, repo.DefaultWikiBranch, oldWikiName) if err != nil { return err } @@ -200,7 +201,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model } else { commitTreeOpts.NoGPGSign = true } - if hasMasterBranch { + if hasDefaultBranch { commitTreeOpts.Parents = []string{"HEAD"} } @@ -212,7 +213,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ Remote: DefaultRemote, - Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch), + Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch), Env: repo_module.FullPushingEnvironment( doer, doer, @@ -269,7 +270,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{ Bare: true, Shared: true, - Branch: DefaultBranch, + Branch: repo.DefaultWikiBranch, }); err != nil { log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err) return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err) @@ -287,7 +288,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err) } - found, wikiPath, err := prepareGitPath(gitRepo, wikiName) + found, wikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, wikiName) if err != nil { return err } @@ -331,7 +332,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ Remote: DefaultRemote, - Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch), + Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch), Env: repo_module.FullPushingEnvironment( doer, doer, @@ -358,3 +359,37 @@ func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error { system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath()) return nil } + +func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, newBranch string) error { + if !git.IsValidRefPattern(newBranch) { + return fmt.Errorf("invalid branch name: %s", newBranch) + } + return db.WithTx(ctx, func(ctx context.Context) error { + repo.DefaultWikiBranch = newBranch + if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_wiki_branch"); err != nil { + return fmt.Errorf("unable to update database: %w", err) + } + + gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo) + if errors.Is(err, util.ErrNotExist) { + return nil // no git repo on storage, no need to do anything else + } else if err != nil { + return fmt.Errorf("unable to open repository: %w", err) + } + defer gitRepo.Close() + + oldDefBranch, err := gitRepo.GetDefaultBranch() + if err != nil { + return fmt.Errorf("unable to get default branch: %w", err) + } + if oldDefBranch == newBranch { + return nil + } + + err = gitRepo.RenameBranch(oldDefBranch, newBranch) + if err != nil { + return fmt.Errorf("unable to rename default branch: %w", err) + } + return nil + }) +} diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index 59c77060f2c27..0a18cffa25310 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -170,7 +170,7 @@ func TestRepository_AddWikiPage(t *testing.T) { return } defer gitRepo.Close() - masterTree, err := gitRepo.GetTree(DefaultBranch) + masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) assert.NoError(t, err) gitPath := WebPathToGitPath(webPath) entry, err := masterTree.GetTreeEntryByPath(gitPath) @@ -215,7 +215,7 @@ func TestRepository_EditWikiPage(t *testing.T) { // Now need to show that the page has been added: gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) assert.NoError(t, err) - masterTree, err := gitRepo.GetTree(DefaultBranch) + masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) assert.NoError(t, err) gitPath := WebPathToGitPath(webPath) entry, err := masterTree.GetTreeEntryByPath(gitPath) @@ -242,7 +242,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) { return } defer gitRepo.Close() - masterTree, err := gitRepo.GetTree(DefaultBranch) + masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) assert.NoError(t, err) gitPath := WebPathToGitPath("Home") _, err = masterTree.GetTreeEntryByPath(gitPath) @@ -280,7 +280,7 @@ func TestPrepareWikiFileName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { webPath := UserTitleToWebPath("", tt.arg) - existence, newWikiPath, err := prepareGitPath(gitRepo, webPath) + existence, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, webPath) if (err != nil) != tt.wantErr { assert.NoError(t, err) return @@ -312,7 +312,7 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) { } defer gitRepo.Close() - existence, newWikiPath, err := prepareGitPath(gitRepo, "Home") + existence, newWikiPath, err := prepareGitPath(gitRepo, "master", "Home") assert.False(t, existence) assert.NoError(t, err) assert.EqualValues(t, "Home.md", newWikiPath) diff --git a/tailwind.config.js b/tailwind.config.js index fb17980568cb2..63a5387d19043 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -29,8 +29,10 @@ export default { content: [ isProduction && '!./templates/devtest/**/*', isProduction && '!./web_src/js/standalone/devtest.js', + '!./templates/swagger/v1_json.tmpl', + '!./templates/user/auth/oidc_wellknown.tmpl', './templates/**/*.tmpl', - './web_src/**/*.{js,vue}', + './web_src/js/**/*.{js,vue}', ].filter(Boolean), blocklist: [ // classes that don't work without CSS variables from "@tailwind base" which we don't use diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index bcd80368e663c..29fbb5f039759 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -47,8 +47,8 @@ {{range .Emails}} {{.Name}} - {{.FullName}} - {{.Email}} + {{.FullName}} + {{.Email}} {{if .IsPrimary}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .CanChange}} diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 1f86803d55def..aef48154242bc 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -62,8 +62,8 @@ {{end}} {{.Package.Type.Name}} - {{.Package.Name}} - {{.Version.Version}} + {{.Package.Name}} + {{.Version.Version}} {{.Creator.Name}} {{if .Repository}} diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index 8fdc80fc70494..e9ce17ac9078b 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -96,7 +96,7 @@ {{ctx.Locale.Tr "admin.users.remote"}} {{end}} - {{.Email}} + {{.Email}} {{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 51eeea405a4d7..4f48dc82c3270 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -14,7 +14,7 @@