diff --git a/Makefile b/Makefile index ce4dec21a2..2e2c86376b 100644 --- a/Makefile +++ b/Makefile @@ -704,6 +704,7 @@ fomantic: cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/ cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build + $(SED_INPLACE) -e 's/\r//g' $(FOMANTIC_WORK_DIR)/build/semantic.css $(FOMANTIC_WORK_DIR)/build/semantic.js rm -f $(FOMANTIC_WORK_DIR)/build/*.min.* .PHONY: webpack diff --git a/integrations/integration_test.go b/integrations/integration_test.go index 0a41546554..ce21eb2ef7 100644 --- a/integrations/integration_test.go +++ b/integrations/integration_test.go @@ -275,7 +275,7 @@ func prepareTestEnv(t testing.TB, skip ...int) func() { assert.NoError(t, unittest.LoadFixtures()) assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) - assert.NoError(t, git.InitWithConfigSync(context.Background())) + assert.NoError(t, git.InitOnceWithSync(context.Background())) ownerDirs, err := os.ReadDir(setting.RepoRootPath) if err != nil { assert.NoError(t, err, "unable to read the new repo root: %v\n", err) @@ -576,7 +576,7 @@ func resetFixtures(t *testing.T) { assert.NoError(t, unittest.LoadFixtures()) assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) - assert.NoError(t, git.InitWithConfigSync(context.Background())) + assert.NoError(t, git.InitOnceWithSync(context.Background())) ownerDirs, err := os.ReadDir(setting.RepoRootPath) if err != nil { assert.NoError(t, err, "unable to read the new repo root: %v\n", err) diff --git a/integrations/migration-test/migration_test.go b/integrations/migration-test/migration_test.go index 45e31ff9ac..83c31d8018 100644 --- a/integrations/migration-test/migration_test.go +++ b/integrations/migration-test/migration_test.go @@ -62,7 +62,7 @@ func initMigrationTest(t *testing.T) func() { assert.True(t, len(setting.RepoRootPath) != 0) assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) - assert.NoError(t, git.InitWithConfigSync(context.Background())) + assert.NoError(t, git.InitOnceWithSync(context.Background())) ownerDirs, err := os.ReadDir(setting.RepoRootPath) if err != nil { assert.NoError(t, err, "unable to read the new repo root: %v\n", err) diff --git a/models/migrations/migrations_test.go b/models/migrations/migrations_test.go index 2c4d21d974..0c8e74f734 100644 --- a/models/migrations/migrations_test.go +++ b/models/migrations/migrations_test.go @@ -203,7 +203,7 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En deferFn := PrintCurrentTest(t, ourSkip) assert.NoError(t, os.RemoveAll(setting.RepoRootPath)) assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) - assert.NoError(t, git.InitWithConfigSync(context.Background())) + assert.NoError(t, git.InitOnceWithSync(context.Background())) ownerDirs, err := os.ReadDir(setting.RepoRootPath) if err != nil { assert.NoError(t, err, "unable to read the new repo root: %v\n", err) diff --git a/models/repo/mirror.go b/models/repo/mirror.go index bd83d24424..8f96e8cee1 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -19,12 +19,6 @@ import ( // ErrMirrorNotExist mirror does not exist error var ErrMirrorNotExist = errors.New("Mirror does not exist") -// RemoteMirrorer defines base methods for pull/push mirrors. -type RemoteMirrorer interface { - GetRepository() *Repository - GetRemoteName() string -} - // Mirror represents mirror information of a repository. type Mirror struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/repo/repo.go b/models/repo/repo.go index 3fd6b94eb1..57d85435eb 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -414,6 +414,9 @@ func (repo *Repository) ComposeMetas() map[string]string { switch unit.ExternalTrackerConfig().ExternalTrackerStyle { case markup.IssueNameStyleAlphanumeric: metas["style"] = markup.IssueNameStyleAlphanumeric + case markup.IssueNameStyleRegexp: + metas["style"] = markup.IssueNameStyleRegexp + metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern default: metas["style"] = markup.IssueNameStyleNumeric } diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 8c17d6138c..da3e19dece 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -76,9 +76,10 @@ func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) { // ExternalTrackerConfig describes external tracker config type ExternalTrackerConfig struct { - ExternalTrackerURL string - ExternalTrackerFormat string - ExternalTrackerStyle string + ExternalTrackerURL string + ExternalTrackerFormat string + ExternalTrackerStyle string + ExternalTrackerRegexpPattern string } // FromDB fills up a ExternalTrackerConfig from serialized format. diff --git a/models/repo_test.go b/models/repo_test.go index c9e66398d1..f554ff16a6 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -74,6 +74,9 @@ func TestMetas(t *testing.T) { externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleNumeric testSuccess(markup.IssueNameStyleNumeric) + externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp + testSuccess(markup.IssueNameStyleRegexp) + repo, err := repo_model.GetRepositoryByID(3) assert.NoError(t, err) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 2a366836fe..d1a4498510 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -117,7 +117,7 @@ func MainTest(m *testing.M, testOpts *TestOptions) { if err = CopyDir(filepath.Join(testOpts.GiteaRootPath, "integrations", "gitea-repositories-meta"), setting.RepoRootPath); err != nil { fatalTestError("util.CopyDir: %v\n", err) } - if err = git.InitWithConfigSync(context.Background()); err != nil { + if err = git.InitOnceWithSync(context.Background()); err != nil { fatalTestError("git.Init: %v\n", err) } @@ -202,7 +202,7 @@ func PrepareTestEnv(t testing.TB) { assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta") assert.NoError(t, CopyDir(metaPath, setting.RepoRootPath)) - assert.NoError(t, git.InitWithConfigSync(context.Background())) + assert.NoError(t, git.InitOnceWithSync(context.Background())) ownerDirs, err := os.ReadDir(setting.RepoRootPath) assert.NoError(t, err) diff --git a/modules/git/git.go b/modules/git/git.go index 5817bd2c7f..0459f57dde 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -34,15 +34,12 @@ var ( GitExecutable = "git" // DefaultContext is the default context to run git commands in - // will be overwritten by InitWithConfigSync with HammerContext + // will be overwritten by InitXxx with HammerContext DefaultContext = context.Background() // SupportProcReceive version >= 2.29.0 SupportProcReceive bool - // initMutex is used to avoid Golang's data race error. see the comments below. - initMutex sync.Mutex - gitVersion *version.Version ) @@ -131,15 +128,6 @@ func VersionInfo() string { return fmt.Sprintf(format, args...) } -// InitSimple initializes git module with a very simple step, no config changes, no global command arguments. -// This method doesn't change anything to filesystem -func InitSimple(ctx context.Context) error { - initMutex.Lock() - defer initMutex.Unlock() - - return initSimpleInternal(ctx) -} - // HomeDir is the home dir for git to store the global config file used by Gitea internally func HomeDir() string { if setting.RepoRootPath == "" { @@ -153,11 +141,9 @@ func HomeDir() string { return setting.RepoRootPath } -func initSimpleInternal(ctx context.Context) error { - // at the moment, when running integration tests, the git.InitXxx would be called twice. - // one is called by the GlobalInitInstalled, one is called by TestMain. - // so the init functions should be protected by a mutex to avoid Golang's data race error. - +// InitSimple initializes git module with a very simple step, no config changes, no global command arguments. +// This method doesn't change anything to filesystem. At the moment, it is only used by "git serv" sub-command, no data-race +func InitSimple(ctx context.Context) error { DefaultContext = ctx if setting.Git.Timeout.Default > 0 { @@ -174,35 +160,47 @@ func initSimpleInternal(ctx context.Context) error { return nil } -// InitWithConfigSync initializes git module. This method may create directories or write files into filesystem -func InitWithConfigSync(ctx context.Context) error { - initMutex.Lock() - defer initMutex.Unlock() +var initOnce sync.Once - err := initSimpleInternal(ctx) +// InitOnceWithSync initializes git module with version check and change global variables, sync gitconfig. +// This method will update the global variables ONLY ONCE (just like git.CheckLFSVersion -- which is not ideal too), +// otherwise there will be data-race problem at the moment. +func InitOnceWithSync(ctx context.Context) (err error) { + initOnce.Do(func() { + err = InitSimple(ctx) + if err != nil { + return + } + + // Since git wire protocol has been released from git v2.18 + if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { + globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2") + } + + // By default partial clones are disabled, enable them from git v2.22 + if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil { + globalCommandArgs = append(globalCommandArgs, "-c", "uploadpack.allowfilter=true", "-c", "uploadpack.allowAnySHA1InWant=true") + } + + // Explicitly disable credential helper, otherwise Git credentials might leak + if CheckGitVersionAtLeast("2.9") == nil { + globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") + } + + SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil + }) if err != nil { return err } + return syncGitConfig() +} - if err = os.MkdirAll(setting.RepoRootPath, os.ModePerm); err != nil { +// syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem) +func syncGitConfig() (err error) { + if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil { return fmt.Errorf("unable to create directory %s, err: %w", setting.RepoRootPath, err) } - if CheckGitVersionAtLeast("2.9") == nil { - // Explicitly disable credential helper, otherwise Git credentials might leak - globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") - } - - // Since git wire protocol has been released from git v2.18 - if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { - globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2") - } - - // By default partial clones are disabled, enable them from git v2.22 - if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil { - globalCommandArgs = append(globalCommandArgs, "-c", "uploadpack.allowfilter=true", "-c", "uploadpack.allowAnySHA1InWant=true") - } - // Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults" // TODO: need to confirm whether users really need to change these values manually. It seems that these values are dummy only and not really used. // If these values are not really used, then they can be set (overwritten) directly without considering about existence. @@ -235,17 +233,15 @@ func InitWithConfigSync(ctx context.Context) error { } } - if CheckGitVersionAtLeast("2.29") == nil { + if SupportProcReceive { // set support for AGit flow if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { return err } - SupportProcReceive = true } else { if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil { return err } - SupportProcReceive = false } if runtime.GOOS == "windows" { diff --git a/modules/git/git_test.go b/modules/git/git_test.go index 5b1cd820e8..061c876cde 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -28,7 +28,7 @@ func testRun(m *testing.M) error { defer util.RemoveAll(repoRootPath) setting.RepoRootPath = repoRootPath - if err = InitWithConfigSync(context.Background()); err != nil { + if err = InitOnceWithSync(context.Background()); err != nil { return fmt.Errorf("failed to call Init: %w", err) } diff --git a/modules/git/remote.go b/modules/git/remote.go index b2a2e6d7ab..cbb4ac6126 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -6,11 +6,12 @@ package git import ( "context" - "net/url" + + giturl "code.gitea.io/gitea/modules/git/url" ) -// GetRemoteAddress returns the url of a specific remote of the repository. -func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (*url.URL, error) { +// GetRemoteAddress returns remote url of git repository in the repoPath with special remote name +func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) { var cmd *Command if CheckGitVersionAtLeast("2.7") == nil { cmd = NewCommand(ctx, "remote", "get-url", remoteName) @@ -20,11 +21,20 @@ func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (*url.UR result, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) if err != nil { - return nil, err + return "", err } if len(result) > 0 { result = result[:len(result)-1] } - return url.Parse(result) + return result, nil +} + +// GetRemoteURL returns the url of a specific remote of the repository. +func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.GitURL, error) { + addr, err := GetRemoteAddress(ctx, repoPath, remoteName) + if err != nil { + return nil, err + } + return giturl.Parse(addr) } diff --git a/modules/git/url/url.go b/modules/git/url/url.go new file mode 100644 index 0000000000..b41cfab7ef --- /dev/null +++ b/modules/git/url/url.go @@ -0,0 +1,90 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package url + +import ( + "fmt" + stdurl "net/url" + "strings" +) + +// ErrWrongURLFormat represents an error with wrong url format +type ErrWrongURLFormat struct { + URL string +} + +func (err ErrWrongURLFormat) Error() string { + return fmt.Sprintf("git URL %s format is wrong", err.URL) +} + +// GitURL represents a git URL +type GitURL struct { + *stdurl.URL + extraMark int // 0 no extra 1 scp 2 file path with no prefix +} + +// String returns the URL's string +func (u *GitURL) String() string { + switch u.extraMark { + case 0: + return u.URL.String() + case 1: + return fmt.Sprintf("%s@%s:%s", u.User.Username(), u.Host, u.Path) + case 2: + return u.Path + default: + return "" + } +} + +// Parse parse all kinds of git URL +func Parse(remote string) (*GitURL, error) { + if strings.Contains(remote, "://") { + u, err := stdurl.Parse(remote) + if err != nil { + return nil, err + } + return &GitURL{URL: u}, nil + } else if strings.Contains(remote, "@") && strings.Contains(remote, ":") { + url := stdurl.URL{ + Scheme: "ssh", + } + squareBrackets := false + lastIndex := -1 + FOR: + for i := 0; i < len(remote); i++ { + switch remote[i] { + case '@': + url.User = stdurl.User(remote[:i]) + lastIndex = i + 1 + case ':': + if !squareBrackets { + url.Host = strings.ReplaceAll(remote[lastIndex:i], "%25", "%") + if len(remote) <= i+1 { + return nil, ErrWrongURLFormat{URL: remote} + } + url.Path = remote[i+1:] + break FOR + } + case '[': + squareBrackets = true + case ']': + squareBrackets = false + } + } + return &GitURL{ + URL: &url, + extraMark: 1, + }, nil + } + + return &GitURL{ + URL: &stdurl.URL{ + Scheme: "file", + Path: remote, + }, + extraMark: 2, + }, nil +} diff --git a/modules/git/url/url_test.go b/modules/git/url/url_test.go new file mode 100644 index 0000000000..611bef8672 --- /dev/null +++ b/modules/git/url/url_test.go @@ -0,0 +1,167 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package url + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseGitURLs(t *testing.T) { + kases := []struct { + kase string + expected *GitURL + }{ + { + kase: "git@127.0.0.1:go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "127.0.0.1", + Path: "go-gitea/gitea.git", + }, + extraMark: 1, + }, + }, + { + kase: "git@[fe80:14fc:cec5:c174:d88%2510]:go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "[fe80:14fc:cec5:c174:d88%10]", + Path: "go-gitea/gitea.git", + }, + extraMark: 1, + }, + }, + { + kase: "git@[::1]:go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "[::1]", + Path: "go-gitea/gitea.git", + }, + extraMark: 1, + }, + }, + { + kase: "git@github.com:go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "github.com", + Path: "go-gitea/gitea.git", + }, + extraMark: 1, + }, + }, + { + kase: "ssh://git@github.com/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "github.com", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "ssh://git@[::1]/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "[::1]", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "/repositories/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "file", + Path: "/repositories/go-gitea/gitea.git", + }, + extraMark: 2, + }, + }, + { + kase: "file:///repositories/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "file", + Path: "/repositories/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "https://github.com/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "https://git:git@github.com/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + User: url.UserPassword("git", "git"), + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "https://[fe80:14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "https", + Host: "[fe80:14fc:cec5:c174:d88%10]:20", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + + { + kase: "git://github.com/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "git", + Host: "github.com", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + } + + for _, kase := range kases { + t.Run(kase.kase, func(t *testing.T) { + u, err := Parse(kase.kase) + assert.NoError(t, err) + assert.EqualValues(t, kase.expected.extraMark, u.extraMark) + assert.EqualValues(t, *kase.expected, *u) + }) + } +} diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index 344be78144..a72f26d5f0 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -203,6 +203,8 @@ func File(numLines int, fileName, language string, code []byte) []string { content = "\n" } else if content == `` { content += "\n" + } else if content == `` { + content += "\n" } content = strings.TrimSuffix(content, ``) content = strings.TrimPrefix(content, ``) diff --git a/modules/highlight/highlight_test.go b/modules/highlight/highlight_test.go index 2f305bb589..e5dfedd2b3 100644 --- a/modules/highlight/highlight_test.go +++ b/modules/highlight/highlight_test.go @@ -5,11 +5,13 @@ package highlight import ( - "reflect" + "strings" "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "github.com/stretchr/testify/assert" "gopkg.in/ini.v1" ) @@ -20,83 +22,83 @@ func TestFile(t *testing.T) { numLines int fileName string code string - want []string + want string }{ { name: ".drone.yml", numLines: 12, fileName: ".drone.yml", - code: `kind: pipeline -name: default + code: util.Dedent(` + kind: pipeline + name: default -steps: -- name: test - image: golang:1.13 - environment: - GOPROXY: https://goproxy.cn - commands: - - go get -u - - go build -v - - go test -v -race -coverprofile=coverage.txt -covermode=atomic -`, - want: []string{ - `kind: pipeline`, - `name: default`, - ``, - `steps:`, - `- name: test`, - ` image: golang:1.13`, - ` environment:`, - ` GOPROXY: https://goproxy.cn`, - ` commands:`, - ` - go get -u`, - ` - go build -v`, - ` - go test -v -race -coverprofile=coverage.txt -covermode=atomic -`, - ` -`, - }, + steps: + - name: test + image: golang:1.13 + environment: + GOPROXY: https://goproxy.cn + commands: + - go get -u + - go build -v + - go test -v -race -coverprofile=coverage.txt -covermode=atomic + `), + want: util.Dedent(` + kind: pipeline + name: default + + steps: + - name: test + image: golang:1.13 + environment: + GOPROXY: https://goproxy.cn + commands: + - go get -u + - go build -v + - go test -v -race -coverprofile=coverage.txt -covermode=atomic + `), }, { name: ".drone.yml - trailing space", numLines: 13, fileName: ".drone.yml", - code: `kind: pipeline -name: default ` + ` + code: strings.Replace(util.Dedent(` + kind: pipeline + name: default -steps: -- name: test - image: golang:1.13 - environment: - GOPROXY: https://goproxy.cn - commands: - - go get -u - - go build -v - - go test -v -race -coverprofile=coverage.txt -covermode=atomic - `, - want: []string{ - `kind: pipeline`, - `name: default `, - ``, - `steps:`, - `- name: test`, - ` image: golang:1.13`, - ` environment:`, - ` GOPROXY: https://goproxy.cn`, - ` commands:`, - ` - go get -u`, - ` - go build -v`, - ` - go test -v -race -coverprofile=coverage.txt -covermode=atomic`, - ` `, - }, + steps: + - name: test + image: golang:1.13 + environment: + GOPROXY: https://goproxy.cn + commands: + - go get -u + - go build -v + - go test -v -race -coverprofile=coverage.txt -covermode=atomic + `)+"\n", "name: default", "name: default ", 1), + want: util.Dedent(` + kind: pipeline + name: default + + steps: + - name: test + image: golang:1.13 + environment: + GOPROXY: https://goproxy.cn + commands: + - go get -u + - go build -v + - go test -v -race -coverprofile=coverage.txt -covermode=atomic + + + + `), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := File(tt.numLines, tt.fileName, "", []byte(tt.code)); !reflect.DeepEqual(got, tt.want) { - t.Errorf("File() = %v, want %v", got, tt.want) - } + got := strings.Join(File(tt.numLines, tt.fileName, "", []byte(tt.code)), "\n") + assert.Equal(t, tt.want, got) }) } } diff --git a/modules/indexer/stats/indexer_test.go b/modules/indexer/stats/indexer_test.go index a335972c21..a4a8b63241 100644 --- a/modules/indexer/stats/indexer_test.go +++ b/modules/indexer/stats/indexer_test.go @@ -13,7 +13,6 @@ import ( "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" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" @@ -30,10 +29,6 @@ func TestMain(m *testing.M) { } func TestRepoStatsIndex(t *testing.T) { - if err := git.InitWithConfigSync(context.Background()); !assert.NoError(t, err) { - return - } - assert.NoError(t, unittest.PrepareTestDatabase()) setting.Cfg = ini.Empty() diff --git a/modules/markup/html.go b/modules/markup/html.go index c5d36e701f..69d9ba3ef2 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/common" "code.gitea.io/gitea/modules/references" + "code.gitea.io/gitea/modules/regexplru" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" @@ -33,6 +34,7 @@ import ( const ( IssueNameStyleNumeric = "numeric" IssueNameStyleAlphanumeric = "alphanumeric" + IssueNameStyleRegexp = "regexp" ) var ( @@ -815,19 +817,35 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { ) next := node.NextSibling + for node != nil && node != next { - _, exttrack := ctx.Metas["format"] - alphanum := ctx.Metas["style"] == IssueNameStyleAlphanumeric + _, hasExtTrackFormat := ctx.Metas["format"] // Repos with external issue trackers might still need to reference local PRs // We need to concern with the first one that shows up in the text, whichever it is - found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum) - if exttrack && alphanum { - if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 { - if !found || ref2.RefLocation.Start < ref.RefLocation.Start { - found = true - ref = ref2 - } + isNumericStyle := ctx.Metas["style"] == "" || ctx.Metas["style"] == IssueNameStyleNumeric + foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle) + + switch ctx.Metas["style"] { + case "", IssueNameStyleNumeric: + found, ref = foundNumeric, refNumeric + case IssueNameStyleAlphanumeric: + found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data) + case IssueNameStyleRegexp: + pattern, err := regexplru.GetCompiled(ctx.Metas["regexp"]) + if err != nil { + return + } + found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern) + } + + // Repos with external issue trackers might still need to reference local PRs + // We need to concern with the first one that shows up in the text, whichever it is + if hasExtTrackFormat && !isNumericStyle { + // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that + if foundNumeric && refNumeric.RefLocation.Start < ref.RefLocation.Start { + found = foundNumeric + ref = refNumeric } } if !found { @@ -836,7 +854,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { var link *html.Node reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] - if exttrack && !ref.IsPull { + if hasExtTrackFormat && !ref.IsPull { ctx.Metas["index"] = ref.Issue res, err := vars.Expand(ctx.Metas["format"], ctx.Metas) @@ -869,7 +887,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { // Decorate action keywords if actionable var keyword *html.Node - if references.IsXrefActionable(ref, exttrack, alphanum) { + if references.IsXrefActionable(ref, hasExtTrackFormat) { keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End]) } else { keyword = &html.Node{ diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index f0eb3253e1..25b0f7b7a5 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -21,8 +21,8 @@ const ( TestRepoURL = TestAppURL + TestOrgRepo + "/" ) -// alphanumLink an HTML link to an alphanumeric-style issue -func alphanumIssueLink(baseURL, class, name string) string { +// externalIssueLink an HTML link to an alphanumeric-style issue +func externalIssueLink(baseURL, class, name string) string { return link(util.URLJoin(baseURL, name), class, name) } @@ -54,6 +54,13 @@ var alphanumericMetas = map[string]string{ "style": IssueNameStyleAlphanumeric, } +var regexpMetas = map[string]string{ + "format": "https://someurl.com/{user}/{repo}/{index}", + "user": "someUser", + "repo": "someRepo", + "style": IssueNameStyleRegexp, +} + // these values should match the TestOrgRepo const above var localMetas = map[string]string{ "user": "gogits", @@ -184,7 +191,7 @@ func TestRender_IssueIndexPattern4(t *testing.T) { test := func(s, expectedFmt string, names ...string) { links := make([]interface{}, len(names)) for i, name := range names { - links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue ref-external-issue", name) + links[i] = externalIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue ref-external-issue", name) } expected := fmt.Sprintf(expectedFmt, links...) testRenderIssueIndexPattern(t, s, expected, &RenderContext{Metas: alphanumericMetas}) @@ -194,6 +201,43 @@ func TestRender_IssueIndexPattern4(t *testing.T) { test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890") } +func TestRender_IssueIndexPattern5(t *testing.T) { + setting.AppURL = TestAppURL + + // regexp: render inputs without valid mentions + test := func(s, expectedFmt, pattern string, ids, names []string) { + metas := regexpMetas + metas["regexp"] = pattern + links := make([]interface{}, len(ids)) + for i, id := range ids { + links[i] = link(util.URLJoin("https://someurl.com/someUser/someRepo/", id), "ref-issue ref-external-issue", names[i]) + } + + expected := fmt.Sprintf(expectedFmt, links...) + testRenderIssueIndexPattern(t, s, expected, &RenderContext{Metas: metas}) + } + + test("abc ISSUE-123 def", "abc %s def", + "ISSUE-(\\d+)", + []string{"123"}, + []string{"ISSUE-123"}, + ) + + test("abc (ISSUE 123) def", "abc %s def", + "\\(ISSUE (\\d+)\\)", + []string{"123"}, + []string{"(ISSUE 123)"}, + ) + + test("abc ISSUE-123 def", "abc %s def", + "(ISSUE-(\\d+))", + []string{"ISSUE-123"}, + []string{"ISSUE-123"}, + ) + + testRenderIssueIndexPattern(t, "will not match", "will not match", &RenderContext{Metas: regexpMetas}) +} + func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { if ctx.URLPrefix == "" { ctx.URLPrefix = TestAppURL @@ -202,7 +246,7 @@ func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *Rend var buf strings.Builder err := postProcess(ctx, []processor{issueIndexPatternProcessor}, strings.NewReader(input), &buf) assert.NoError(t, err) - assert.Equal(t, expected, buf.String()) + assert.Equal(t, expected, buf.String(), "input=%q", input) } func TestRender_AutoLink(t *testing.T) { diff --git a/modules/references/references.go b/modules/references/references.go index 630e621043..7f5086d093 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -351,6 +351,24 @@ func FindRenderizableReferenceNumeric(content string, prOnly bool) (bool, *Rende } } +// FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string. +func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bool, *RenderizableReference) { + match := pattern.FindStringSubmatchIndex(content) + if len(match) < 4 { + return false, nil + } + + action, location := findActionKeywords([]byte(content), match[2]) + + return true, &RenderizableReference{ + Issue: content[match[2]:match[3]], + RefLocation: &RefSpan{Start: match[0], End: match[1]}, + Action: action, + ActionLocation: location, + IsPull: false, + } +} + // FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string. func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) { match := issueAlphanumericPattern.FindStringSubmatchIndex(content) @@ -547,7 +565,7 @@ func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) { } // IsXrefActionable returns true if the xref action is actionable (i.e. produces a result when resolved) -func IsXrefActionable(ref *RenderizableReference, extTracker, alphaNum bool) bool { +func IsXrefActionable(ref *RenderizableReference, extTracker bool) bool { if extTracker { // External issues cannot be automatically closed return false diff --git a/modules/regexplru/regexplru.go b/modules/regexplru/regexplru.go new file mode 100644 index 0000000000..97c7cff4c1 --- /dev/null +++ b/modules/regexplru/regexplru.go @@ -0,0 +1,45 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package regexplru + +import ( + "regexp" + + "code.gitea.io/gitea/modules/log" + + lru "github.com/hashicorp/golang-lru" +) + +var lruCache *lru.Cache + +func init() { + var err error + lruCache, err = lru.New(1000) + if err != nil { + log.Fatal("failed to new LRU cache, err: %v", err) + } +} + +// GetCompiled works like regexp.Compile, the compiled expr or error is stored in LRU cache +func GetCompiled(expr string) (r *regexp.Regexp, err error) { + v, ok := lruCache.Get(expr) + if !ok { + r, err = regexp.Compile(expr) + if err != nil { + lruCache.Add(expr, err) + return nil, err + } + lruCache.Add(expr, r) + } else { + r, ok = v.(*regexp.Regexp) + if !ok { + if err, ok = v.(error); ok { + return nil, err + } + panic("impossible") + } + } + return r, nil +} diff --git a/modules/regexplru/regexplru_test.go b/modules/regexplru/regexplru_test.go new file mode 100644 index 0000000000..041f0dcfb9 --- /dev/null +++ b/modules/regexplru/regexplru_test.go @@ -0,0 +1,27 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package regexplru + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRegexpLru(t *testing.T) { + r, err := GetCompiled("a") + assert.NoError(t, err) + assert.True(t, r.MatchString("a")) + + r, err = GetCompiled("a") + assert.NoError(t, err) + assert.True(t, r.MatchString("a")) + + assert.EqualValues(t, 1, lruCache.Len()) + + _, err = GetCompiled("(") + assert.Error(t, err) + assert.EqualValues(t, 2, lruCache.Len()) +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index ef7b70c09f..03e0e9899b 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" + giturl "code.gitea.io/gitea/modules/git/url" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -971,20 +972,35 @@ type remoteAddress struct { Password string } -func mirrorRemoteAddress(ctx context.Context, m repo_model.RemoteMirrorer) remoteAddress { +func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string) remoteAddress { a := remoteAddress{} - - u, err := git.GetRemoteAddress(ctx, m.GetRepository().RepoPath(), m.GetRemoteName()) - if err != nil { - log.Error("GetRemoteAddress %v", err) + if !m.IsMirror { return a } - if u.User != nil { - a.Username = u.User.Username() - a.Password, _ = u.User.Password() + remoteURL := m.OriginalURL + if remoteURL == "" { + var err error + remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName) + if err != nil { + log.Error("GetRemoteURL %v", err) + return a + } + } + + u, err := giturl.Parse(remoteURL) + if err != nil { + log.Error("giturl.Parse %v", err) + return a + } + + if u.Scheme != "ssh" && u.Scheme != "file" { + if u.User != nil { + a.Username = u.User.Username() + a.Password, _ = u.User.Password() + } + u.User = nil } - u.User = nil a.Address = u.String() return a diff --git a/modules/util/util.go b/modules/util/util.go index 351a345473..1017117874 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -9,6 +9,7 @@ import ( "crypto/rand" "errors" "math/big" + "regexp" "strconv" "strings" @@ -191,3 +192,35 @@ var titleCaser = cases.Title(language.English) func ToTitleCase(s string) string { return titleCaser.String(s) } + +var ( + whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$") + leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])") +) + +// Dedent removes common indentation of a multi-line string along with whitespace around it +// Based on https://github.com/lithammer/dedent +func Dedent(s string) string { + var margin string + + s = whitespaceOnly.ReplaceAllString(s, "") + indents := leadingWhitespace.FindAllStringSubmatch(s, -1) + + for i, indent := range indents { + if i == 0 { + margin = indent[1] + } else if strings.HasPrefix(indent[1], margin) { + continue + } else if strings.HasPrefix(margin, indent[1]) { + margin = indent[1] + } else { + margin = "" + break + } + } + + if margin != "" { + s = regexp.MustCompile("(?m)^"+margin).ReplaceAllString(s, "") + } + return strings.TrimSpace(s) +} diff --git a/modules/util/util_test.go b/modules/util/util_test.go index ca5bd87eae..91b0ef9455 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -225,3 +225,10 @@ func TestToTitleCase(t *testing.T) { assert.Equal(t, ToTitleCase(`foo bar baz`), `Foo Bar Baz`) assert.Equal(t, ToTitleCase(`FOO BAR BAZ`), `Foo Bar Baz`) } + +func TestDedent(t *testing.T) { + assert.Equal(t, Dedent(` + foo + bar + `), "foo\n\tbar") +} diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index af6f019e9d..ea4e2a105d 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -955,6 +955,7 @@ branch.included=Включено topic.done=Готово + [org] org_name_holder=Име на организацията org_full_name_holder=Пълно име на организацията diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 5dafbfdb16..896b4ee970 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -2183,6 +2183,7 @@ topic.done=Hotovo topic.count_prompt=Nelze vybrat více než 25 témat topic.format_prompt=Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků. + error.csv.too_large=Tento soubor nelze vykreslit, protože je příliš velký. error.csv.unexpected=Tento soubor nelze vykreslit, protože obsahuje neočekávaný znak na řádku %d ve sloupci %d. error.csv.invalid_field_count=Soubor nelze vykreslit, protože má nesprávný počet polí na řádku %d. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 442168cf69..c6cd975238 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -2218,6 +2218,7 @@ topic.done=Fertig topic.count_prompt=Du kannst nicht mehr als 25 Themen auswählen topic.format_prompt=Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein. + error.csv.too_large=Diese Datei kann nicht gerendert werden, da sie zu groß ist. error.csv.unexpected=Diese Datei kann nicht gerendert werden, da sie ein unerwartetes Zeichen in Zeile %d und Spalte %d enthält. error.csv.invalid_field_count=Diese Datei kann nicht gerendert werden, da sie eine falsche Anzahl an Feldern in Zeile %d hat. diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 0a5cbbd3e1..11d8855f96 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -2,6 +2,7 @@ home=Αρχική dashboard=Κεντρικός Πίνακας explore=Εξερεύνηση help=Βοήθεια +logo=Λογότυπο sign_in=Είσοδος sign_in_with=Είσοδος με sign_out=Έξοδος @@ -716,6 +717,9 @@ generate_token_success=Το νέο διακριτικό σας έχει δημι generate_token_name_duplicate=Το %s έχει ήδη χρησιμοποιηθεί ως όνομα εφαρμογής. Παρακαλούμε χρησιμοποιήστε ένα νέο. delete_token=Διαγραφή access_token_deletion=Διαγραφή Διακριτικού Πρόσβασης +access_token_deletion_cancel_action=Άκυρο +access_token_deletion_confirm_action=Διαγραφή +access_token_deletion_desc=Η διαγραφή ενός διακριτικού θα ανακαλέσει οριστικά την πρόσβαση στο λογαριασμό σας για εφαρμογές που το χρησιμοποιούν. Συνέχεια; delete_token_success=Το διακριτικό έχει διαγραφεί. Οι εφαρμογές που το χρησιμοποιούν δεν έχουν πλέον πρόσβαση στο λογαριασμό σας. manage_oauth2_applications=Διαχείριση Εφαρμογών Oauth2 @@ -858,6 +862,7 @@ default_branch=Προεπιλεγμένος Κλάδος default_branch_helper=Ο προεπιλεγμένος κλάδος είναι ο βασικός κλάδος για pull requests και υποβολές κώδικα. mirror_prune=Καθαρισμός mirror_prune_desc=Αφαίρεση παρωχημένων αναφορών απομακρυσμένης-παρακολούθησης +mirror_interval=Διάστημα ανανέωσης ειδώλου (έγκυρες μονάδες ώρας είναι 'h', 'm', 's'). 0 για απενεργοποίηση του αυτόματου συγχρονισμού. (Ελάχιστο διάστημα: %s) mirror_interval_invalid=Το χρονικό διάστημα του ειδώλου δεν είναι έγκυρο. mirror_address=Κλωνοποίηση Από Το URL mirror_address_desc=Τοποθετήστε όλα τα απαιτούμενα διαπιστευτήρια στην ενότητα Εξουσιοδότηση. @@ -1688,7 +1693,7 @@ activity.period.filter_label=Περίοδος: activity.period.daily=1 ημέρα activity.period.halfweekly=3 ημέρες activity.period.weekly=1 εβδομάδα -activity.period.monthly=1 μήνας +activity.period.monthly=1 μήνα activity.period.quarterly=3 μήνες activity.period.semiyearly=6 μήνες activity.period.yearly=1 έτος @@ -2281,6 +2286,9 @@ topic.done=Ολοκληρώθηκε topic.count_prompt=Δεν μπορείτε να επιλέξετε περισσότερα από 25 θέματα topic.format_prompt=Τα θέματα πρέπει να ξεκινούν με γράμμα ή αριθμό, μπορούν να περιλαμβάνουν παύλες ('-') και μπορεί να είναι έως 35 χαρακτήρες. +find_file.go_to_file=Μετάβαση στο αρχείο +find_file.no_matching=Δεν ταιριάζει κανένα αρχείο + error.csv.too_large=Δεν είναι δυνατή η απόδοση αυτού του αρχείου επειδή είναι πολύ μεγάλο. error.csv.unexpected=Δεν είναι δυνατή η απόδοση αυτού του αρχείου, επειδή περιέχει έναν μη αναμενόμενο χαρακτήρα στη γραμμή %d και στη στήλη %d. error.csv.invalid_field_count=Δεν είναι δυνατή η απόδοση αυτού του αρχείου, επειδή έχει λάθος αριθμό πεδίων στη γραμμή %d. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index b9ba6e1136..b1c3247315 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1568,14 +1568,7 @@ pulls.squash_merge_pull_request = Create squash commit pulls.merge_manually = Manually merged pulls.merge_commit_id = The merge commit ID pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed -pulls.merge_pull_request_now = Merge Pull Request Now -pulls.rebase_merge_pull_request_now = Rebase and Merge Now -pulls.rebase_merge_commit_pull_request_now = Rebase and Merge Now (--no-ff) -pulls.squash_merge_pull_request_now = Squash and Merge Now -pulls.merge_pull_request_on_status_success = Merge Pull Request When All Checks Succeed -pulls.rebase_merge_pull_request_on_status_success = Rebase and Merge When All Checks Succeed -pulls.rebase_merge_commit_pull_request_on_status_success = Rebase and Merge (--no-ff) When All Checks Succeed -pulls.squash_merge_pull_request_on_status_success = Squash and Merge When All Checks Succeed + pulls.invalid_merge_option = You cannot use this merge option for this pull request. pulls.merge_conflict = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy pulls.merge_conflict_summary = Error Message @@ -1606,14 +1599,18 @@ pulls.reopened_at = `reopened this pull request %[2] pulls.merge_instruction_hint = `You can also view command line instructions.` pulls.merge_instruction_step1_desc = From your project repository, check out a new branch and test the changes. pulls.merge_instruction_step2_desc = Merge the changes and update on Gitea. -pulls.merge_on_status_success = The pull request was scheduled to merge when all checks succeed. -pulls.merge_on_status_success_already_scheduled = This pull request is already scheduled to merge when all checks succeed. -pulls.pr_has_pending_merge_on_success = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. -pulls.merge_pull_on_success_cancel = Cancel auto merge -pulls.pull_request_not_scheduled = This pull request is not scheduled to auto merge. -pulls.pull_request_schedule_canceled = The auto merge was canceled for this pull request. -pulls.pull_request_scheduled_auto_merge = `scheduled this pull request to auto merge when all checks succeed %[1]s` -pulls.pull_request_canceled_scheduled_auto_merge = `canceled auto merging this pull request when all checks succeed %[1]s` + +pulls.auto_merge_button_when_succeed = (When checks succeed) +pulls.auto_merge_when_succeed = Auto merge when all checks succeed +pulls.auto_merge_newly_scheduled = The pull request was scheduled to merge when all checks succeed. +pulls.auto_merge_has_pending_schedule = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. + +pulls.auto_merge_cancel_schedule = Cancel auto merge +pulls.auto_merge_not_scheduled = This pull request is not scheduled to auto merge. +pulls.auto_merge_canceled_schedule = The auto merge was canceled for this pull request. + +pulls.auto_merge_newly_scheduled_comment = `scheduled this pull request to auto merge when all checks succeed %[1]s` +pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull request when all checks succeed %[1]s` milestones.new = New Milestone milestones.open_tab = %d Open @@ -1811,6 +1808,9 @@ settings.tracker_url_format_error = The external issue tracker URL format is not settings.tracker_issue_style = External Issue Tracker Number Format settings.tracker_issue_style.numeric = Numeric settings.tracker_issue_style.alphanumeric = Alphanumeric +settings.tracker_issue_style.regexp = Regular Expression +settings.tracker_issue_style.regexp_pattern = Regular Expression Pattern +settings.tracker_issue_style.regexp_pattern_desc = The first captured group will be used in place of {index}. settings.tracker_url_format_desc = Use the placeholders {user}, {repo} and {index} for the username, repository name and issue index. settings.enable_timetracker = Enable Time Tracking settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 5c98da03b9..a35cffe495 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -2198,6 +2198,7 @@ topic.done=Hecho topic.count_prompt=No puede seleccionar más de 25 temas topic.format_prompt=Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo. + error.csv.too_large=No se puede renderizar este archivo porque es demasiado grande. error.csv.unexpected=No se puede procesar este archivo porque contiene un carácter inesperado en la línea %d y la columna %d. error.csv.invalid_field_count=No se puede procesar este archivo porque tiene un número incorrecto de campos en la línea %d. diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index a465c00145..9cf067a010 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -2110,6 +2110,7 @@ topic.done=انجام شد topic.count_prompt=شما نمی توانید بیش از 25 موضوع انتخاب کنید topic.format_prompt=موضوع می‌بایستی با حروف یا شماره ها شروع شود. و می‌تواند شامل دَش ('-') باشد و طول آن تا 35 کارکتر نیز امکانپذیر است. + error.csv.too_large=نمی توان این فایل را رندر کرد زیرا بسیار بزرگ است. error.csv.unexpected=نمی توان این فایل را رندر کرد زیرا حاوی یک کاراکتر غیرمنتظره در خط %d و ستون %d است. error.csv.invalid_field_count=نمی توان این فایل را رندر کرد زیرا تعداد فیلدهای آن در خط %d اشتباه است. diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index da96086cdc..bad4b505f7 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -971,6 +971,7 @@ topic.manage_topics=Hallitse aiheita topic.done=Valmis + [org] org_name_holder=Organisaatio org_full_name_holder=Organisaation täydellinen nimi diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 4ee872901c..667205e06b 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1973,6 +1973,7 @@ topic.count_prompt=Vous ne pouvez pas sélectionner plus de 25 sujets topic.format_prompt=Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères. + [org] org_name_holder=Nom de l'organisation org_full_name_holder=Nom complet de l'organisation diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 790f11faf5..3c58383cc9 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -1305,6 +1305,7 @@ topic.manage_topics=Témák kezelése topic.done=Kész + [org] org_name_holder=Szervezet neve org_full_name_holder=Szervezet teljes neve diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 8d49ce2ac1..41a9e84ad7 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -1019,6 +1019,7 @@ branch.deleted_by=Dihapus oleh %s + [org] org_name_holder=Nama Organisasi org_full_name_holder=Organisasi Nama Lengkap diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 2d0b62507f..b28196e80c 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -1131,6 +1131,7 @@ tag.confirm_create_tag=Skapa merki topic.done=Í lagi + [org] repo_updated=Uppfært people=Fólk diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 0335f10449..a6bd6ad1bb 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1781,6 +1781,7 @@ topic.count_prompt=Non puoi selezionare più di 25 argomenti topic.format_prompt=Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri. + [org] org_name_holder=Nome dell'Organizzazione org_full_name_holder=Nome completo dell'organizzazione diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 3699ac37b3..24e3a56480 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -2281,6 +2281,7 @@ topic.done=完了 topic.count_prompt=選択できるのは25トピックまでです。 topic.format_prompt=トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。 + error.csv.too_large=このファイルは大きすぎるため表示できません。 error.csv.unexpected=このファイルは %d 行目の %d 文字目に予期しない文字が含まれているため表示できません。 error.csv.invalid_field_count=このファイルは %d 行目のフィールドの数が正しくないため表示できません。 diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 625c31a5c7..ed48ada226 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -1173,6 +1173,7 @@ topic.done=완료 topic.count_prompt=25개 이상의 토픽을 선택하실 수 없습니다. + [org] org_name_holder=조직 이름 org_full_name_holder=조직 전체 이름 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 537c04d3d8..13aea77c79 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -2186,6 +2186,7 @@ topic.done=Gatavs topic.count_prompt=Nevar pievienot vairāk kā 25 tēmas topic.format_prompt=Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara. + error.csv.too_large=Nevar attēlot šo failu, jo tas ir pārāk liels. error.csv.unexpected=Nevar attēlot šo failu, jo tas satur neparedzētu simbolu %d. līnijas %d. kolonnā. error.csv.invalid_field_count=Nevar attēlot šo failu, jo tas satur nepareizu skaitu ar laukiem %d. līnijā. diff --git a/options/locale/locale_ml-IN.ini b/options/locale/locale_ml-IN.ini index ece816f891..285c7e8e4b 100644 --- a/options/locale/locale_ml-IN.ini +++ b/options/locale/locale_ml-IN.ini @@ -746,6 +746,7 @@ settings.event_issues=ഇഷ്യൂകള്‍ + [org] diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index efdd4e632b..66c3fee81b 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1833,6 +1833,7 @@ topic.count_prompt=Je kunt niet meer dan 25 onderwerpen selecteren topic.format_prompt=Onderwerpen moeten beginnen met een letter of nummer, kunnen streepjes bevatten ('-') en kunnen maximaal 35 tekens lang zijn. + [org] org_name_holder=Organisatienaam org_full_name_holder=Volledige naam organisatie diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index db96b31126..2bbf821664 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -2056,6 +2056,7 @@ topic.done=Gotowe topic.count_prompt=Nie możesz wybrać więcej, niż 25 tematów topic.format_prompt=Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków. + error.csv.too_large=Nie można wyświetlić tego pliku, ponieważ jest on zbyt duży. error.csv.unexpected=Nie można renderować tego pliku, ponieważ zawiera nieoczekiwany znak w wierszu %d i kolumnie %d. error.csv.invalid_field_count=Nie można renderować tego pliku, ponieważ ma nieprawidłową liczbę pól w wierszu %d. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 7094e07347..658d1f4355 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -2268,6 +2268,7 @@ topic.done=Feito topic.count_prompt=Você não pode selecionar mais de 25 tópicos topic.format_prompt=Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres. + error.csv.too_large=Não é possível renderizar este arquivo porque ele é muito grande. error.csv.unexpected=Não é possível renderizar este arquivo porque ele contém um caractere inesperado na linha %d e coluna %d. error.csv.invalid_field_count=Não é possível renderizar este arquivo porque ele tem um número errado de campos na linha %d. diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index e675de4bae..7656f0c52b 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -2285,6 +2285,9 @@ topic.done=Concluído topic.count_prompt=Não pode escolher mais do que 25 tópicos topic.format_prompt=Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres. +find_file.go_to_file=Ir para o ficheiro +find_file.no_matching=Não foi encontrado qualquer ficheiro correspondente + error.csv.too_large=Não é possível apresentar este ficheiro por ser demasiado grande. error.csv.unexpected=Não é possível apresentar este ficheiro porque contém um caractere inesperado na linha %d e coluna %d. error.csv.invalid_field_count=Não é possível apresentar este ficheiro porque tem um número errado de campos na linha %d. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 2d99b043f9..377ef1cb31 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -2203,6 +2203,7 @@ topic.done=Сохранить topic.count_prompt=Вы не можете выбрать более 25 тем topic.format_prompt=Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов. + error.csv.too_large=Не удается отобразить этот файл, потому что он слишком большой. error.csv.unexpected=Не удается отобразить этот файл, потому что он содержит неожиданный символ в строке %d и столбце %d. error.csv.invalid_field_count=Не удается отобразить этот файл, потому что он имеет неправильное количество полей в строке %d. diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 460d61c7c4..397a851821 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -2046,6 +2046,7 @@ topic.done=සිදු topic.count_prompt=ඔබට 25 මාතෘකා වලට වඩා තෝරා ගත නොහැක topic.format_prompt=මාතෘකා අකුරකින් හෝ අංකයකින් ආරම්භ කළ යුතුය, දෂ්ට කිරීම් ඇතුළත් කළ හැකිය ('-') සහ අක්ෂර 35 ක් දිගු විය හැකිය. + error.csv.too_large=එය ඉතා විශාල නිසා මෙම ගොනුව විදැහුම්කරණය කළ නොහැක. error.csv.unexpected=%d පේළියේ සහ %dතීරුවේ අනපේක්ෂිත චරිතයක් අඩංගු බැවින් මෙම ගොනුව විදැහුම්කරණය කළ නොහැක. error.csv.invalid_field_count=මෙම ගොනුව රේඛාවේ වැරදි ක්ෂේත්ර සංඛ්යාවක් ඇති බැවින් එය විදැහුම්කරණය කළ නොහැක %d. diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 1afca6b778..25bb0aeff2 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -1619,6 +1619,7 @@ topic.count_prompt=Du kan inte välja fler än 25 ämnen topic.format_prompt=Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa. + [org] org_name_holder=Organisationsnamn org_full_name_holder=Organisationens Fullständiga Namn diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 6cabad7a69..c7bdb9fc65 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -2057,6 +2057,7 @@ topic.done=Bitti topic.count_prompt=25'ten fazla konu seçemezsiniz topic.format_prompt=Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir. + error.csv.too_large=Bu dosya çok büyük olduğu için işlenemiyor. error.csv.unexpected=%d satırı ve %d sütununda beklenmeyen bir karakter içerdiğinden bu dosya işlenemiyor. error.csv.invalid_field_count=%d satırında yanlış sayıda alan olduğundan bu dosya işlenemiyor. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 791a3bc105..3e83932a2d 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -2118,6 +2118,7 @@ topic.done=Готово topic.count_prompt=Ви не можете вибрати більше 25 тем topic.format_prompt=Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів. + error.csv.too_large=Не вдається відобразити цей файл, тому що він завеликий. error.csv.unexpected=Не вдається відобразити цей файл, тому що він містить неочікуваний символ в рядку %d і стовпці %d. error.csv.invalid_field_count=Не вдається відобразити цей файл, тому що він має неправильну кількість полів у рядку %d. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 0f28fa8f34..2cc448282b 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -2283,6 +2283,7 @@ topic.done=保存 topic.count_prompt=您最多选择25个主题 topic.format_prompt=主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符 + error.csv.too_large=无法渲染此文件,因为它太大了。 error.csv.unexpected=无法渲染此文件,因为它包含了意外字符,其位于第 %d 行和第 %d 列。 error.csv.invalid_field_count=无法渲染此文件,因为它在第 %d 行中的字段数有误。 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index b12fda1600..97d3b8984c 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -559,6 +559,7 @@ release.downloads=下載附件 + [org] org_name_holder=組織名稱 org_full_name_holder=組織全名 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index ef478990f5..8ce92f03c9 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -715,6 +715,9 @@ generate_token_success=已經產生新的 Token。請立刻複製它,因為他 generate_token_name_duplicate=應用程式名稱 %s 已被使用,請換一個試試。 delete_token=刪除 access_token_deletion=刪除 Access Token +access_token_deletion_cancel_action=取消 +access_token_deletion_confirm_action=刪除 +access_token_deletion_desc=刪除 Token 後,使用此 Token 的應用程式將無法再存取您的帳戶,此動作不可還原。是否繼續? delete_token_success=已刪除 Token。使用此 Token 的應用程式無法再存取您的帳戶。 manage_oauth2_applications=管理 OAuth2 應用程式 @@ -857,6 +860,7 @@ default_branch=預設分支 default_branch_helper=預設分支是合併請求和提交程式碼的基礎分支。 mirror_prune=裁減 mirror_prune_desc=刪除過時的遠端追蹤參考 +mirror_interval=鏡像間隔 (有效時間單位為 'h'、'm'、's'),設為 0 以停用自動同步。(最小間隔: %s) mirror_interval_invalid=鏡像週期無效 mirror_address=從 URL Clone mirror_address_desc=在授權資訊中填入必要的資料。 @@ -1856,7 +1860,7 @@ settings.confirm_wiki_delete=刪除 Wiki 資料 settings.wiki_deletion_success=已刪除儲存庫的 Wiki 資料。 settings.delete=刪除本儲存庫 settings.delete_desc=刪除儲存庫是永久的且不可還原。 -settings.delete_notices_1=- 此操作不可還原。 +settings.delete_notices_1=- 此動作不可還原。 settings.delete_notices_2=- 此操作將永久刪除 %s 儲存庫,包括程式碼、問題、留言、Wiki 資料和協作者設定。 settings.delete_notices_fork_1=- 在此儲存庫刪除後,它的 fork 將會變成獨立儲存庫。 settings.deletion_success=這個儲存庫已被刪除。 @@ -2257,6 +2261,9 @@ topic.done=完成 topic.count_prompt=您最多能選擇 25 個主題 topic.format_prompt=主題必須以字母或數字為開頭,可包含連接號「-」且最長為 35 個字元。 +find_file.go_to_file=移至檔案 +find_file.no_matching=找不到符合的檔案 + error.csv.too_large=無法渲染此檔案,因為它太大了。 error.csv.unexpected=無法渲染此檔案,因為它包含了未預期的字元,於第 %d 行第 %d 列。 error.csv.invalid_field_count=無法渲染此檔案,因為它第 %d 行的欄位數量有誤。 diff --git a/routers/init.go b/routers/init.go index 9b6a770f27..2898c44607 100644 --- a/routers/init.go +++ b/routers/init.go @@ -102,7 +102,7 @@ func GlobalInitInstalled(ctx context.Context) { log.Fatal("Gitea is not installed") } - mustInitCtx(ctx, git.InitWithConfigSync) + mustInitCtx(ctx, git.InitOnceWithSync) log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir()) git.CheckLFSVersion() diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 8df4ccc607..d698f1c49a 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" + pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -36,6 +37,7 @@ import ( "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/utils" asymkey_service "code.gitea.io/gitea/services/asymkey" + "code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/gitdiff" pull_service "code.gitea.io/gitea/services/pull" @@ -966,6 +968,22 @@ func MergePullRequest(ctx *context.Context) { message += "\n\n" + form.MergeMessageField } + if form.MergeWhenChecksSucceed { + // delete all scheduled auto merges + _ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID) + // schedule auto merge + scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) + if err != nil { + ctx.ServerError("ScheduleAutoMerge", err) + return + } else if scheduled { + // nothing more to do ... + ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_newly_scheduled")) + ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pr.Index)) + return + } + } + if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) @@ -1070,6 +1088,26 @@ func MergePullRequest(ctx *context.Context) { ctx.Redirect(issue.Link()) } +// CancelAutoMergePullRequest cancels a scheduled pr +func CancelAutoMergePullRequest(ctx *context.Context) { + issue := checkPullInfo(ctx) + if ctx.Written() { + return + } + + if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil { + if db.IsErrNotExist(err) { + ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled")) + ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index)) + return + } + ctx.ServerError("RemoveScheduledAutoMerge", err) + return + } + ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_canceled_schedule")) + ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index)) +} + func stopTimerIfAvailable(user *user_model.User, issue *models.Issue) error { if models.StopwatchExists(user.ID, issue.ID) { if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil { diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index 1a7a41ae91..6083d17fa5 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -215,22 +215,24 @@ func SettingsPost(ctx *context.Context) { return } - u, _ := git.GetRemoteAddress(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Mirror.GetRemoteName()) + u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Mirror.GetRemoteName()) + if err != nil { + ctx.Data["Err_MirrorAddress"] = true + handleSettingRemoteAddrError(ctx, err, form) + return + } if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() { form.MirrorPassword, _ = u.User.Password() } - address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword) - if err == nil { - err = migrations.IsMigrateURLAllowed(address, ctx.Doer) - } + err = migrations.IsMigrateURLAllowed(u.String(), ctx.Doer) if err != nil { ctx.Data["Err_MirrorAddress"] = true handleSettingRemoteAddrError(ctx, err, form) return } - if err := mirror_service.UpdateAddress(ctx, ctx.Repo.Mirror, address); err != nil { + if err := mirror_service.UpdateAddress(ctx, ctx.Repo.Mirror, u.String()); err != nil { ctx.ServerError("UpdateAddress", err) return } @@ -434,9 +436,10 @@ func SettingsPost(ctx *context.Context) { RepoID: repo.ID, Type: unit_model.TypeExternalTracker, Config: &repo_model.ExternalTrackerConfig{ - ExternalTrackerURL: form.ExternalTrackerURL, - ExternalTrackerFormat: form.TrackerURLFormat, - ExternalTrackerStyle: form.TrackerIssueStyle, + ExternalTrackerURL: form.ExternalTrackerURL, + ExternalTrackerFormat: form.TrackerURLFormat, + ExternalTrackerStyle: form.TrackerIssueStyle, + ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern, }, }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) diff --git a/routers/web/web.go b/routers/web/web.go index bf4c4662af..88a446d067 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1127,6 +1127,7 @@ func RegisterRoutes(m *web.Route) { m.Get(".patch", repo.DownloadPullPatch) m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(forms.MergePullRequestForm{}), repo.MergePullRequest) + m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest) m.Post("/update", repo.UpdatePullRequest) m.Post("/set_allow_maintainer_edit", bindIgnErr(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 2bcb91f8c3..738a77d2bb 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -141,6 +141,7 @@ type RepoSettingForm struct { ExternalTrackerURL string TrackerURLFormat string TrackerIssueStyle string + ExternalTrackerRegexpPattern string EnableCloseIssuesViaCommitInAnyBranch bool EnableProjects bool EnablePackages bool diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index caa81f0fe9..f4c527bbdc 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -210,9 +210,10 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo } gitArgs = append(gitArgs, m.GetRemoteName()) - remoteAddr, remoteErr := git.GetRemoteAddress(ctx, repoPath, m.GetRemoteName()) + remoteURL, remoteErr := git.GetRemoteURL(ctx, repoPath, m.GetRemoteName()) if remoteErr != nil { log.Error("SyncMirrors [repo: %-v]: GetRemoteAddress Error %v", m.Repo, remoteErr) + return nil, false } stdoutBuilder := strings.Builder{} @@ -291,7 +292,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo if m.LFS && setting.LFS.StartServer { log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) - endpoint := lfs.DetermineEndpoint(remoteAddr.String(), m.LFSEndpoint) + endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint) lfsClient := lfs.NewClient(endpoint, nil) if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil { log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err) diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 138ebb737b..2927bed72b 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -131,7 +131,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second performPush := func(path string) error { - remoteAddr, err := git.GetRemoteAddress(ctx, path, m.RemoteName) + remoteURL, err := git.GetRemoteURL(ctx, path, m.RemoteName) if err != nil { log.Error("GetRemoteAddress(%s) Error %v", path, err) return errors.New("Unexpected error") @@ -147,7 +147,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { } defer gitRepo.Close() - endpoint := lfs.DetermineEndpoint(remoteAddr.String(), "") + endpoint := lfs.DetermineEndpoint(remoteURL.String(), "") lfsClient := lfs.NewClient(endpoint, nil) if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil { return util.SanitizeErrorCredentialURLs(err) diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 2d963d67c8..cfac37cd11 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -37,7 +37,9 @@ {{end}} - {{if .IsMirror}}{{end}} + {{if .IsMirror}} + {{$address := MirrorRemoteAddress $.Context . $.Mirror.GetRemoteName}} +
{{$.i18n.Tr "repo.mirror_from"}} {{$address.Address}}
{{end}} {{if .IsFork}}
{{$.i18n.Tr "repo.forked_from"}} {{.BaseRepo.FullName}}
{{end}} {{if .IsGenerated}}
{{$.i18n.Tr "repo.generated_from"}} {{.TemplateRepo.FullName}}
{{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 235f4c8fc2..0258a9f969 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -843,8 +843,8 @@ {{svg "octicon-git-merge" 16}} {{.Poster.GetDisplayName}} - {{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.pull_request_scheduled_auto_merge" $createdStr | Safe}} - {{else}}{{$.i18n.Tr "repo.pulls.pull_request_canceled_scheduled_auto_merge" $createdStr | Safe}}{{end}} + {{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} + {{else}}{{$.i18n.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} {{end}} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index c764138fa0..d2282f07f6 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -251,8 +251,14 @@ {{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }} {{end}} + {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}} - {{if and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} + + {{/* admin can merge without checks, writer can merge when checkes succeed */}} + {{$canMergeNow := and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} + {{/* admin and writer both can make an auto merge schedule */}} + + {{if $canMergeNow}} {{if $notAllOverridableChecksOk}}
{{svg "octicon-dot-fill"}} @@ -277,7 +283,6 @@ {{end}} {{end}} - {{$canAutoMerge = true}} {{if (gt .Issue.PullRequest.CommitsBehind 0)}}
@@ -317,112 +322,111 @@
{{end}} - {{if and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} - {{if .AllowMerge}} - {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} - {{$approvers := .Issue.PullRequest.GetApprovers}} - {{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}} + {{if .AllowMerge}} {{/* user is allowed to merge */}} + {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} + {{$approvers := .Issue.PullRequest.GetApprovers}} + {{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}} + {{$hasPendingPullRequestMergeTip := ""}} + {{if .HasPendingPullRequestMerge}} + {{$createdPRMergeStr := TimeSinceUnix .PendingPullRequestMerge.CreatedUnix $.i18n.Lang}} + {{$hasPendingPullRequestMergeTip = $.i18n.Tr "repo.pulls.auto_merge_has_pending_schedule" .PendingPullRequestMerge.Doer.Name $createdPRMergeStr}} + {{end}} +
+ + const generalHideAutoMerge = mergeForm.canMergeNow && mergeForm.allOverridableChecksOk; // if this PR can be merged now, then hide the auto merge + mergeForm['mergeStyles'] = [ + { + 'name': 'merge', + 'allowed': {{$prUnit.PullRequestsConfig.AllowMerge}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_pull_request"}}, + 'mergeTitleFieldText': defaultMergeTitle, + 'mergeMessageFieldText': defaultMergeMessage, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'rebase', + 'allowed': {{$prUnit.PullRequestsConfig.AllowRebase}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}, + 'hideMergeMessageTexts': true, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'rebase-merge', + 'allowed': {{$prUnit.PullRequestsConfig.AllowRebaseMerge}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}, + 'mergeTitleFieldText': defaultMergeTitle, + 'mergeMessageFieldText': defaultMergeMessage, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'squash', + 'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}, + 'mergeTitleFieldText': defaultSquashMergeTitle, + 'mergeMessageFieldText': defaultMergeMessage, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'manually-merged', + 'allowed': {{and $prUnit.PullRequestsConfig.AllowManualMerge $.IsRepoAdmin}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_manually"}}, + 'hideMergeMessageTexts': true, + 'hideAutoMerge': true, + } + ]; + window.config.pageData.pullRequestMergeForm = mergeForm; + })(); + -
+
- {{if .ShowMergeInstructions}} -
{{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}}
- - {{end}} - {{else}} -
-
- {{svg "octicon-x"}} - {{$.i18n.Tr "repo.pulls.no_merge_desc"}} -
-
- {{svg "octicon-info"}} - {{$.i18n.Tr "repo.pulls.no_merge_helper"}} -
+ {{if .ShowMergeInstructions}} + {{template "repo/issue/view_content/pull_merge_instruction" (dict "i18n" .i18n "Issue" .Issue)}} {{end}} {{else}} + {{/* no merge style was set in repo setting: not or ($prUnit.PullRequestsConfig.AllowMerge ...) */}}
+
+ {{svg "octicon-x"}} + {{$.i18n.Tr "repo.pulls.no_merge_desc"}} +
{{svg "octicon-info"}} - {{$.i18n.Tr "repo.pulls.no_merge_access"}} + {{$.i18n.Tr "repo.pulls.no_merge_helper"}}
- {{end}} - {{end}} + {{end}} {{/* end if the repo was set to use any merge style */}} + {{else}} + {{/* user is not allowed to merge */}} +
+
+ {{svg "octicon-info"}} + {{$.i18n.Tr "repo.pulls.no_merge_access"}} +
+ {{end}} {{/* end if user is allowed to merge or not */}} {{else}} {{/* Merge conflict without specific file. Suggest manual merge, only if all reviews and status checks OK. */}} {{if .IsBlockedByApprovals}} diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl new file mode 100644 index 0000000000..0ed70860f3 --- /dev/null +++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl @@ -0,0 +1,19 @@ +
{{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}}
+ diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index f44d9c98ad..68cbd4de2c 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -91,7 +91,7 @@ {{if .Repository.IsMirror}} - {{(MirrorRemoteAddress $.Context .Mirror).Address}} + {{(MirrorRemoteAddress $.Context .Repository .Mirror.GetRemoteName).Address}} {{$.i18n.Tr "repo.settings.mirror_settings.direction.pull"}} {{.Mirror.UpdatedUnix.AsTime}} @@ -119,7 +119,7 @@
- {{$address := MirrorRemoteAddress $.Context .Mirror}} + {{$address := MirrorRemoteAddress $.Context .Repository .Mirror.GetRemoteName}}
@@ -168,7 +168,7 @@ {{range .PushMirrors}} - {{$address := MirrorRemoteAddress $.Context .}} + {{$address := MirrorRemoteAddress $.Context $.Repository .GetRemoteName}} {{$address.Address}} {{$.i18n.Tr "repo.settings.mirror_settings.direction.push"}} {{if .LastUpdateUnix}}{{.LastUpdateUnix.AsTime}}{{else}}{{$.i18n.Tr "never"}}{{end}} {{if .LastError}}
{{$.i18n.Tr "error"}}
{{end}} @@ -361,16 +361,27 @@
{{$externalTracker := (.Repository.MustGetUnit $.UnitTypeExternalTracker)}} {{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}} - - + +
- - + +
+
+
+ + +
+
+ +
+ + +

{{.i18n.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}

diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index abcc227bec..8f67f1cee1 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -4,15 +4,17 @@
+
{{if eq .SignedUserName .Owner.Name}} - + {{avatar .Owner 290}} {{else}} - + {{avatar .Owner 290}} {{end}} +
{{if .Owner.FullName}}{{.Owner.FullName}}{{end}} {{.Owner.Name}} diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index 40398a65cb..75fbceb800 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -1,9 +1,23 @@ @@ -68,6 +115,7 @@ export default { mergeTitleFieldValue: '', mergeMessageFieldValue: '', deleteBranchAfterMerge: false, + autoMergeWhenSucceed: false, mergeStyle: '', mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles @@ -82,6 +130,13 @@ export default { showActionForm: false, }), + computed: { + mergeButtonStyleClass() { + if (this.mergeForm.allOverridableChecksOk) return 'green'; + return this.autoMergeWhenSucceed ? 'blue' : 'red'; + } + }, + watch: { mergeStyle(val) { this.mergeStyleDetail = this.mergeForm.mergeStyles.find((e) => e.name === val); @@ -90,7 +145,7 @@ export default { created() { this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0); - this.mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed)?.name; + this.switchMergeStyle(this.mergeForm.mergeStyles.find((e) => e.allowed)?.name, !this.mergeForm.canMergeNow); }, mounted() { @@ -111,7 +166,11 @@ export default { this.deleteBranchAfterMerge = this.mergeForm.defaultDeleteBranchAfterMerge; this.mergeTitleFieldValue = this.mergeStyleDetail.mergeTitleFieldText; this.mergeMessageFieldValue = this.mergeStyleDetail.mergeMessageFieldText; - } + }, + switchMergeStyle(name, autoMerge = false) { + this.mergeStyle = name; + this.autoMergeWhenSucceed = autoMerge; + }, }, }; @@ -124,4 +183,59 @@ export default { .ui.checkbox label { cursor: pointer; } + +/* make the dropdown list left-aligned */ +.ui.merge-button { + position: relative; +} +.ui.merge-button .ui.dropdown { + position: static; +} +.ui.merge-button > .ui.dropdown:last-child > .menu:not(.left) { + left: 0; + right: auto; +} +.ui.merge-button .ui.dropdown .menu > .item { + display: flex; + align-items: stretch; + padding: 0 !important; /* polluted by semantic.css: .ui.dropdown .menu > .item { !important } */ +} + +/* merge style list item */ +.action-text { + padding: 0.8rem; + flex: 1 +} + +.auto-merge-small { + width: 40px; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} +.auto-merge-small .auto-merge-tip { + display: none; + left: 38px; + top: -1px; + bottom: -1px; + position: absolute; + align-items: center; + color: var(--color-info-text); + background-color: var(--color-info-bg); + border: 1px solid var(--color-info-border); + border-left: none; + padding-right: 1rem; +} + +.auto-merge-small:hover { + color: var(--color-info-text); + background-color: var(--color-info-bg); + border: 1px solid var(--color-info-border); +} + +.auto-merge-small:hover .auto-merge-tip { + display: flex; +} + diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 53471b30cf..6cdde6a1e4 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -462,6 +462,11 @@ export function initRepository() { if (typeof $(this).data('context') !== 'undefined') $($(this).data('context')).addClass('disabled'); } }); + const $trackerIssueStyleRadios = $('.js-tracker-issue-style'); + $trackerIssueStyleRadios.on('change input', () => { + const checkedVal = $trackerIssueStyleRadios.filter(':checked').val(); + $('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp'); + }); } // Labels diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 926f0a5d05..9c39852c30 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -1,6 +1,7 @@ import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg'; import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.svg'; import octiconCopy from '../../public/img/svg/octicon-copy.svg'; +import octiconClock from '../../public/img/svg/octicon-clock.svg'; import octiconGitMerge from '../../public/img/svg/octicon-git-merge.svg'; import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg'; import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg'; @@ -23,6 +24,7 @@ export const svgs = { 'octicon-chevron-down': octiconChevronDown, 'octicon-chevron-right': octiconChevronRight, 'octicon-copy': octiconCopy, + 'octicon-clock': octiconClock, 'octicon-git-merge': octiconGitMerge, 'octicon-git-pull-request': octiconGitPullRequest, 'octicon-issue-closed': octiconIssueClosed, diff --git a/web_src/less/_base.less b/web_src/less/_base.less index c029cb9485..4d7f69e3b3 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -2003,14 +2003,6 @@ table th[data-sortt-desc] { margin-right: 0 !important; } -/* limit width of all direct dropdown menu children */ -/* https://github.com/go-gitea/gitea/pull/10835 */ -.dropdown:not(.selection) > .menu:not(.review-box) > *:not(.header) { - max-width: 300px; - overflow-x: hidden; - text-overflow: ellipsis; -} - .ui.dropdown .menu .item { border-radius: 0; } diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index d73cb90330..37a5017fbd 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -1055,10 +1055,6 @@ .merge-section { background-color: var(--color-box-body); - .item { - padding: .25rem 0; - } - .item-section { display: flex; align-items: center; diff --git a/web_src/less/_user.less b/web_src/less/_user.less index e192986819..4576d1c9e7 100644 --- a/web_src/less/_user.less +++ b/web_src/less/_user.less @@ -44,27 +44,20 @@ } } } - #profile-avatar { background: none; padding: 1rem 1rem .25rem; - + justify-content: center; img { width: 100%; height: auto; object-fit: contain; margin: 0; - } - - @media @mediaSm { - height: 250px; - overflow: hidden; - - img { - max-height: 767px; - max-width: 767px; + @media @mediaSm { + width: 30vw; } } + } @media @mediaSm {