mirror of
https://github.com/go-gitea/gitea
synced 2024-11-18 08:04:25 +00:00
Merge branch 'main' into feature-activitypub
This commit is contained in:
commit
f7da251c5d
1
Makefile
1
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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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" {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
90
modules/git/url/url.go
Normal file
90
modules/git/url/url.go
Normal file
@ -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
|
||||
}
|
167
modules/git/url/url_test.go
Normal file
167
modules/git/url/url_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -203,6 +203,8 @@ func File(numLines int, fileName, language string, code []byte) []string {
|
||||
content = "\n"
|
||||
} else if content == `</span><span class="w">` {
|
||||
content += "\n</span>"
|
||||
} else if content == `</span></span><span class="line"><span class="cl">` {
|
||||
content += "\n"
|
||||
}
|
||||
content = strings.TrimSuffix(content, `<span class="w">`)
|
||||
content = strings.TrimPrefix(content, `</span>`)
|
||||
|
@ -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{
|
||||
`<span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">pipeline</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default</span>`,
|
||||
`</span></span><span class="line"><span class="cl">`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">steps</span><span class="p">:</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">test</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.13</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span><span class="nt">GOPROXY</span><span class="p">:</span><span class="w"> </span><span class="l">https://goproxy.cn</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">commands</span><span class="p">:</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span>- <span class="l">go get -u</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go build -v</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go test -v -race -coverprofile=coverage.txt -covermode=atomic</span><span class="w">
|
||||
</span></span></span>`,
|
||||
`<span class="w">
|
||||
</span>`,
|
||||
},
|
||||
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(`
|
||||
<span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">pipeline</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default</span>
|
||||
</span></span><span class="line"><span class="cl">
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">steps</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">test</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.13</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span><span class="nt">GOPROXY</span><span class="p">:</span><span class="w"> </span><span class="l">https://goproxy.cn</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">commands</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span>- <span class="l">go get -u</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go build -v</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go test -v -race -coverprofile=coverage.txt -covermode=atomic</span></span></span>
|
||||
`),
|
||||
},
|
||||
{
|
||||
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{
|
||||
`<span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">pipeline</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default </span>`,
|
||||
`</span></span><span class="line"><span class="cl">`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">steps</span><span class="p">:</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">test</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.13</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span><span class="nt">GOPROXY</span><span class="p">:</span><span class="w"> </span><span class="l">https://goproxy.cn</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">commands</span><span class="p">:</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span>- <span class="l">go get -u</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go build -v</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go test -v -race -coverprofile=coverage.txt -covermode=atomic</span>`,
|
||||
`</span></span><span class="line"><span class="cl"><span class="w"> </span></span></span>`,
|
||||
},
|
||||
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(`
|
||||
<span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">pipeline</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default </span>
|
||||
</span></span><span class="line"><span class="cl">
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">steps</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">test</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.13</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span><span class="nt">GOPROXY</span><span class="p">:</span><span class="w"> </span><span class="l">https://goproxy.cn</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">commands</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span>- <span class="l">go get -u</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go build -v</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go test -v -race -coverprofile=coverage.txt -covermode=atomic</span>
|
||||
</span></span>
|
||||
<span class="w">
|
||||
</span>
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
45
modules/regexplru/regexplru.go
Normal file
45
modules/regexplru/regexplru.go
Normal file
@ -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
|
||||
}
|
27
modules/regexplru/regexplru_test.go
Normal file
27
modules/regexplru/regexplru_test.go
Normal file
@ -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())
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -955,6 +955,7 @@ branch.included=Включено
|
||||
topic.done=Готово
|
||||
|
||||
|
||||
|
||||
[org]
|
||||
org_name_holder=Име на организацията
|
||||
org_full_name_holder=Пълно име на организацията
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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=Το <strong>%s</strong> έχει ήδη χρησιμοποιηθεί ως όνομα εφαρμογής. Παρακαλούμε χρησιμοποιήστε ένα νέο.
|
||||
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.
|
||||
|
@ -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 <a id="%[1]s" href="#%[1]s">%[2]
|
||||
pulls.merge_instruction_hint = `You can also view <a class="show-instruction">command line instructions</a>.`
|
||||
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 <code>{index}</code>.
|
||||
settings.tracker_url_format_desc = Use the placeholders <code>{user}</code>, <code>{repo}</code> and <code>{index}</code> 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
|
||||
|
@ -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.
|
||||
|
@ -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 اشتباه است.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1019,6 +1019,7 @@ branch.deleted_by=Dihapus oleh %s
|
||||
|
||||
|
||||
|
||||
|
||||
[org]
|
||||
org_name_holder=Nama Organisasi
|
||||
org_full_name_holder=Organisasi Nama Lengkap
|
||||
|
@ -1131,6 +1131,7 @@ tag.confirm_create_tag=Skapa merki
|
||||
topic.done=Í lagi
|
||||
|
||||
|
||||
|
||||
[org]
|
||||
repo_updated=Uppfært
|
||||
people=Fólk
|
||||
|
@ -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
|
||||
|
@ -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 行目のフィールドの数が正しくないため表示できません。
|
||||
|
@ -1173,6 +1173,7 @@ topic.done=완료
|
||||
topic.count_prompt=25개 이상의 토픽을 선택하실 수 없습니다.
|
||||
|
||||
|
||||
|
||||
[org]
|
||||
org_name_holder=조직 이름
|
||||
org_full_name_holder=조직 전체 이름
|
||||
|
@ -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ā.
|
||||
|
@ -746,6 +746,7 @@ settings.event_issues=ഇഷ്യൂകള്
|
||||
|
||||
|
||||
|
||||
|
||||
[org]
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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 行中的字段数有误。
|
||||
|
@ -559,6 +559,7 @@ release.downloads=下載附件
|
||||
|
||||
|
||||
|
||||
|
||||
[org]
|
||||
org_name_holder=組織名稱
|
||||
org_full_name_holder=組織全名
|
||||
|
@ -715,6 +715,9 @@ generate_token_success=已經產生新的 Token。請立刻複製它,因為他
|
||||
generate_token_name_duplicate=應用程式名稱 <strong>%s</strong> 已被使用,請換一個試試。
|
||||
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=- 此操作<strong>不可</strong>還原。
|
||||
settings.delete_notices_1=- 此動作<strong>不可</strong>還原。
|
||||
settings.delete_notices_2=- 此操作將永久刪除 <strong>%s</strong> 儲存庫,包括程式碼、問題、留言、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 行的欄位數量有誤。
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -141,6 +141,7 @@ type RepoSettingForm struct {
|
||||
ExternalTrackerURL string
|
||||
TrackerURLFormat string
|
||||
TrackerIssueStyle string
|
||||
ExternalTrackerRegexpPattern string
|
||||
EnableCloseIssuesViaCommitInAnyBranch bool
|
||||
EnableProjects bool
|
||||
EnablePackages bool
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -37,7 +37,9 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .IsMirror}}<div class="fork-flag">{{$.i18n.Tr "repo.mirror_from"}} <a target="_blank" rel="noopener noreferrer" href="{{if .SanitizedOriginalURL}}{{.SanitizedOriginalURL}}{{else}}{{(MirrorRemoteAddress $.Context $.Mirror).Address}}{{end}}">{{if .SanitizedOriginalURL}}{{.SanitizedOriginalURL}}{{else}}{{(MirrorRemoteAddress $.Context $.Mirror).Address}}{{end}}</a></div>{{end}}
|
||||
{{if .IsMirror}}
|
||||
{{$address := MirrorRemoteAddress $.Context . $.Mirror.GetRemoteName}}
|
||||
<div class="fork-flag">{{$.i18n.Tr "repo.mirror_from"}} <a target="_blank" rel="noopener noreferrer" href="{{$address.Address}}">{{$address.Address}}</a></div>{{end}}
|
||||
{{if .IsFork}}<div class="fork-flag">{{$.i18n.Tr "repo.forked_from"}} <a href="{{.BaseRepo.Link}}">{{.BaseRepo.FullName}}</a></div>{{end}}
|
||||
{{if .IsGenerated}}<div class="fork-flag">{{$.i18n.Tr "repo.generated_from"}} <a href="{{.TemplateRepo.Link}}">{{.TemplateRepo.FullName}}</a></div>{{end}}
|
||||
</div>
|
||||
|
@ -843,8 +843,8 @@
|
||||
<span class="badge">{{svg "octicon-git-merge" 16}}</span>
|
||||
<span class="text grey">
|
||||
<a class="author" href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a>
|
||||
{{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}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -251,8 +251,14 @@
|
||||
{{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }}
|
||||
</div>
|
||||
{{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}}
|
||||
<div class="item">
|
||||
<i class="icon icon-octicon">{{svg "octicon-dot-fill"}}</i>
|
||||
@ -277,7 +283,6 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{$canAutoMerge = true}}
|
||||
{{if (gt .Issue.PullRequest.CommitsBehind 0)}}
|
||||
<div class="ui divider"></div>
|
||||
<div class="item item-section">
|
||||
@ -317,112 +322,111 @@
|
||||
</div>
|
||||
{{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}}
|
||||
<div class="ui divider"></div>
|
||||
<script>
|
||||
<!-- /* eslint-disable */ -->
|
||||
(() => {
|
||||
const defaultMergeTitle = {{.DefaultMergeMessage}};
|
||||
const defaultSquashMergeTitle = {{.DefaultSquashMergeMessage}};
|
||||
const defaultMergeMessage = 'Reviewed-on: ' + {{$.Issue.HTMLURL}} + '\n' + {{$approvers}};
|
||||
const mergeForm = {
|
||||
'baseLink': {{.Link}},
|
||||
'textCancel': {{$.i18n.Tr "cancel"}},
|
||||
'textDeleteBranch': {{$.i18n.Tr "repo.branch.delete" .HeadTarget}},
|
||||
'textAutoMergeButtonWhenSucceed': {{$.i18n.Tr "repo.pulls.auto_merge_button_when_succeed"}},
|
||||
'textAutoMergeWhenSucceed': {{$.i18n.Tr "repo.pulls.auto_merge_when_succeed"}},
|
||||
'textAutoMergeCancelSchedule': {{$.i18n.Tr "repo.pulls.auto_merge_cancel_schedule"}},
|
||||
|
||||
<div class="ui divider"></div>
|
||||
'canMergeNow': {{$canMergeNow}},
|
||||
'allOverridableChecksOk': {{not $notAllOverridableChecksOk}},
|
||||
'pullHeadCommitID': {{.PullHeadCommitID}},
|
||||
'isPullBranchDeletable': {{.IsPullBranchDeletable}},
|
||||
'defaultDeleteBranchAfterMerge': {{$prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}},
|
||||
'mergeMessageFieldPlaceHolder': {{$.i18n.Tr "repo.editor.commit_message_desc"}},
|
||||
|
||||
<script>
|
||||
<!-- /* eslint-disable */ -->
|
||||
(() => {
|
||||
const defaultMergeTitle = {{.DefaultMergeMessage}};
|
||||
const defaultSquashMergeTitle = {{.DefaultSquashMergeMessage}};
|
||||
const defaultMergeMessage = 'Reviewed-on: ' + {{$.Issue.HTMLURL}} + '\n' + {{$approvers}};
|
||||
const mergeForm = {
|
||||
'baseLink': {{.Link}},
|
||||
'textCancel': {{$.i18n.Tr "cancel"}},
|
||||
'textDeleteBranch': {{$.i18n.Tr "repo.branch.delete" .HeadTarget}},
|
||||
'hasPendingPullRequestMerge': {{.HasPendingPullRequestMerge}},
|
||||
'hasPendingPullRequestMergeTip': {{$hasPendingPullRequestMergeTip}},
|
||||
};
|
||||
|
||||
'allOverridableChecksOk': {{not $notAllOverridableChecksOk}},
|
||||
'pullHeadCommitID': {{.PullHeadCommitID}},
|
||||
'isPullBranchDeletable': {{.IsPullBranchDeletable}},
|
||||
'defaultDeleteBranchAfterMerge': {{$prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}},
|
||||
'mergeMessageFieldPlaceHolder': {{$.i18n.Tr "repo.editor.commit_message_desc"}},
|
||||
};
|
||||
mergeForm['mergeStyles'] = [
|
||||
{
|
||||
'name': 'merge',
|
||||
'allowed': {{$prUnit.PullRequestsConfig.AllowMerge}},
|
||||
'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_pull_request"}},
|
||||
'mergeTitleFieldText': defaultMergeTitle,
|
||||
'mergeMessageFieldText': defaultMergeMessage,
|
||||
},
|
||||
{
|
||||
'name': 'rebase',
|
||||
'allowed': {{$prUnit.PullRequestsConfig.AllowRebase}},
|
||||
'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}},
|
||||
'hideMergeMessageTexts': true,
|
||||
},
|
||||
{
|
||||
'name': 'rebase-merge',
|
||||
'allowed': {{$prUnit.PullRequestsConfig.AllowRebaseMerge}},
|
||||
'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}},
|
||||
'mergeTitleFieldText': defaultMergeTitle,
|
||||
'mergeMessageFieldText': defaultMergeMessage,
|
||||
},
|
||||
{
|
||||
'name': 'squash',
|
||||
'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}},
|
||||
'textDoMerge': {{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}},
|
||||
'mergeTitleFieldText': defaultSquashMergeTitle,
|
||||
'mergeMessageFieldText': defaultMergeMessage,
|
||||
},
|
||||
{
|
||||
'name': 'manually-merged',
|
||||
'allowed': {{and $prUnit.PullRequestsConfig.AllowManualMerge $.IsRepoAdmin}},
|
||||
'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_manually"}},
|
||||
'hideMergeMessageTexts': true,
|
||||
}
|
||||
];
|
||||
window.config.pageData.pullRequestMergeForm = mergeForm;
|
||||
})();
|
||||
</script>
|
||||
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;
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div id="pull-request-merge-form"></div>
|
||||
<div id="pull-request-merge-form"></div>
|
||||
|
||||
{{if .ShowMergeInstructions}}
|
||||
<div class="instruct-toggle mt-3"> {{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}} </div>
|
||||
<div class="instruct-content" style="display:none">
|
||||
<div class="ui divider"></div>
|
||||
<div><h3 class="di">{{$.i18n.Tr "step1"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step1_desc"}}</div>
|
||||
<div class="ui secondary segment">
|
||||
{{if eq .Issue.PullRequest.Flow 0}}
|
||||
<div>git checkout -b {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{.Issue.PullRequest.HeadBranch}} {{.Issue.PullRequest.BaseBranch}}</div>
|
||||
<div>git pull {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.HTMLURL}}{{else}}origin{{end}} {{.Issue.PullRequest.HeadBranch}}</div>
|
||||
{{else}}
|
||||
<div>git fetch origin {{.Issue.PullRequest.GetGitRefName}}:{{.Issue.PullRequest.HeadBranch}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div><h3 class="di">{{$.i18n.Tr "step2"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step2_desc"}}</div>
|
||||
<div class="ui secondary segment">
|
||||
<div>git checkout {{.Issue.PullRequest.BaseBranch}}</div>
|
||||
<div>git merge --no-ff {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{.Issue.PullRequest.HeadBranch}}</div>
|
||||
<div>git push origin {{.Issue.PullRequest.BaseBranch}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="ui divider"></div>
|
||||
<div class="item text red">
|
||||
{{svg "octicon-x"}}
|
||||
{{$.i18n.Tr "repo.pulls.no_merge_desc"}}
|
||||
</div>
|
||||
<div class="item">
|
||||
{{svg "octicon-info"}}
|
||||
{{$.i18n.Tr "repo.pulls.no_merge_helper"}}
|
||||
</div>
|
||||
{{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 ...) */}}
|
||||
<div class="ui divider"></div>
|
||||
<div class="item text red">
|
||||
{{svg "octicon-x"}}
|
||||
{{$.i18n.Tr "repo.pulls.no_merge_desc"}}
|
||||
</div>
|
||||
<div class="item">
|
||||
{{svg "octicon-info"}}
|
||||
{{$.i18n.Tr "repo.pulls.no_merge_access"}}
|
||||
{{$.i18n.Tr "repo.pulls.no_merge_helper"}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}} {{/* end if the repo was set to use any merge style */}}
|
||||
{{else}}
|
||||
{{/* user is not allowed to merge */}}
|
||||
<div class="ui divider"></div>
|
||||
<div class="item">
|
||||
{{svg "octicon-info"}}
|
||||
{{$.i18n.Tr "repo.pulls.no_merge_access"}}
|
||||
</div>
|
||||
{{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}}
|
||||
|
@ -0,0 +1,19 @@
|
||||
<div class="instruct-toggle mt-3"> {{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}} </div>
|
||||
<div class="instruct-content" style="display:none">
|
||||
<div class="ui divider"></div>
|
||||
<div><h3 class="di">{{$.i18n.Tr "step1"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step1_desc"}}</div>
|
||||
<div class="ui secondary segment">
|
||||
{{if eq $.Issue.PullRequest.Flow 0}}
|
||||
<div>git checkout -b {{if ne $.Issue.PullRequest.HeadRepo.ID $.Issue.PullRequest.BaseRepo.ID}}{{$.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{$.Issue.PullRequest.HeadBranch}} {{$.Issue.PullRequest.BaseBranch}}</div>
|
||||
<div>git pull {{if ne $.Issue.PullRequest.HeadRepo.ID $.Issue.PullRequest.BaseRepo.ID}}{{$.Issue.PullRequest.HeadRepo.HTMLURL}}{{else}}origin{{end}} {{$.Issue.PullRequest.HeadBranch}}</div>
|
||||
{{else}}
|
||||
<div>git fetch origin {{$.Issue.PullRequest.GetGitRefName}}:{{$.Issue.PullRequest.HeadBranch}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div><h3 class="di">{{$.i18n.Tr "step2"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step2_desc"}}</div>
|
||||
<div class="ui secondary segment">
|
||||
<div>git checkout {{$.Issue.PullRequest.BaseBranch}}</div>
|
||||
<div>git merge --no-ff {{if ne $.Issue.PullRequest.HeadRepo.ID $.Issue.PullRequest.BaseRepo.ID}}{{$.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{$.Issue.PullRequest.HeadBranch}}</div>
|
||||
<div>git push origin {{$.Issue.PullRequest.BaseBranch}}</div>
|
||||
</div>
|
||||
</div>
|
@ -91,7 +91,7 @@
|
||||
{{if .Repository.IsMirror}}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{(MirrorRemoteAddress $.Context .Mirror).Address}}</td>
|
||||
<td>{{(MirrorRemoteAddress $.Context .Repository .Mirror.GetRemoteName).Address}}</td>
|
||||
<td>{{$.i18n.Tr "repo.settings.mirror_settings.direction.pull"}}</td>
|
||||
<td>{{.Mirror.UpdatedUnix.AsTime}}</td>
|
||||
<td class="right aligned">
|
||||
@ -119,7 +119,7 @@
|
||||
<label for="interval">{{.i18n.Tr "repo.mirror_interval" .MinimumMirrorInterval}}</label>
|
||||
<input id="interval" name="interval" value="{{.MirrorInterval}}">
|
||||
</div>
|
||||
{{$address := MirrorRemoteAddress $.Context .Mirror}}
|
||||
{{$address := MirrorRemoteAddress $.Context .Repository .Mirror.GetRemoteName}}
|
||||
<div class="field {{if .Err_MirrorAddress}}error{{end}}">
|
||||
<label for="mirror_address">{{.i18n.Tr "repo.mirror_address"}}</label>
|
||||
<input id="mirror_address" name="mirror_address" value="{{$address.Address}}" required>
|
||||
@ -168,7 +168,7 @@
|
||||
<tbody>
|
||||
{{range .PushMirrors}}
|
||||
<tr>
|
||||
{{$address := MirrorRemoteAddress $.Context .}}
|
||||
{{$address := MirrorRemoteAddress $.Context $.Repository .GetRemoteName}}
|
||||
<td>{{$address.Address}}</td>
|
||||
<td>{{$.i18n.Tr "repo.settings.mirror_settings.direction.push"}}</td>
|
||||
<td>{{if .LastUpdateUnix}}{{.LastUpdateUnix.AsTime}}{{else}}{{$.i18n.Tr "never"}}{{end}} {{if .LastError}}<div class="ui red label tooltip" data-content="{{.LastError}}">{{$.i18n.Tr "error"}}</div>{{end}}</td>
|
||||
@ -361,16 +361,27 @@
|
||||
<div class="ui radio checkbox">
|
||||
{{$externalTracker := (.Repository.MustGetUnit $.UnitTypeExternalTracker)}}
|
||||
{{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}}
|
||||
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="numeric" {{if $externalTrackerStyle}}{{if eq $externalTrackerStyle "numeric"}}checked=""{{end}}{{end}}/>
|
||||
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">(#1234)</span></label>
|
||||
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="numeric" {{if eq $externalTrackerStyle "numeric"}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.numeric"}} <span class="ui light grey text">#1234</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input class="hidden" tabindex="0" name="tracker_issue_style" type="radio" value="alphanumeric" {{if $externalTrackerStyle}}{{if eq $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle "alphanumeric"}}checked=""{{end}}{{end}} />
|
||||
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">(ABC-123, DEFG-234)</span></label>
|
||||
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="alphanumeric" {{if eq $externalTrackerStyle "alphanumeric"}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.alphanumeric"}} <span class="ui light grey text">ABC-123 , DEFG-234</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input class="js-tracker-issue-style" name="tracker_issue_style" type="radio" value="regexp" {{if eq $externalTrackerStyle "regexp"}}checked{{end}}>
|
||||
<label>{{.i18n.Tr "repo.settings.tracker_issue_style.regexp"}} <span class="ui light grey text">(ISSUE-\d+) , ISSUE-(\d+)</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if ne $externalTrackerStyle "regexp"}}disabled{{end}}" id="tracker-issue-style-regex-box">
|
||||
<label for="external_tracker_regexp_pattern">{{.i18n.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label>
|
||||
<input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}">
|
||||
<p class="help">{{.i18n.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,15 +4,17 @@
|
||||
<div class="ui stackable grid">
|
||||
<div class="ui five wide column">
|
||||
<div class="ui card">
|
||||
<div id="profile-avatar" class="content df"/>
|
||||
{{if eq .SignedUserName .Owner.Name}}
|
||||
<a class="image tooltip" href="{{AppSubUrl}}/user/settings" id="profile-avatar" data-content="{{.i18n.Tr "user.change_avatar"}}" data-position="bottom center">
|
||||
<a class="image tooltip" href="{{AppSubUrl}}/user/settings" data-content="{{.i18n.Tr "user.change_avatar"}}" data-position="bottom center">
|
||||
{{avatar .Owner 290}}
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="image" id="profile-avatar">
|
||||
<span class="image">
|
||||
{{avatar .Owner 290}}
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="content word-break profile-avatar-name">
|
||||
{{if .Owner.FullName}}<span class="header text center">{{.Owner.FullName}}</span>{{end}}
|
||||
<span class="username text center">{{.Owner.Name}}</span>
|
||||
|
@ -1,9 +1,23 @@
|
||||
<template>
|
||||
<!--
|
||||
if this component is shown, either the user is admin (can do merge without checks), or they is a writer who has the permission to do merge
|
||||
if the user is a writer and can't do merge now (canMergeNow==false), then only show the Auto Merge for them
|
||||
How to test the UI manually:
|
||||
* Method 1: manually set some variables in pull.tmpl, eg: {{$notAllOverridableChecksOk = true}} {{$canMergeNow = false}}
|
||||
* Method 2: make a protected branch, then set state=pending/success :
|
||||
curl -X POST ${root_url}/api/v1/repos/${owner}/${repo}/statuses/${sha} \
|
||||
-H "accept: application/json" -H "authorization: Basic $base64_auth" -H "Content-Type: application/json" \
|
||||
-d '{"context": "test/context", "description": "description", "state": "${state}", "target_url": "http://localhost"}'
|
||||
-->
|
||||
<div>
|
||||
<!-- eslint-disable -->
|
||||
<div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"></div>
|
||||
|
||||
<div class="ui form" v-if="showActionForm">
|
||||
<form :action="mergeForm.baseLink+'/merge'" method="post">
|
||||
<input type="hidden" name="_csrf" :value="csrfToken">
|
||||
<input type="hidden" name="head_commit_id" v-model="mergeForm.pullHeadCommitID">
|
||||
<input type="hidden" name="merge_when_checks_succeed" v-model="autoMergeWhenSucceed">
|
||||
|
||||
<template v-if="!mergeStyleDetail.hideMergeMessageTexts">
|
||||
<div class="field">
|
||||
@ -14,39 +28,72 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button class="ui button" :class="[mergeForm.allOverridableChecksOk?'green':'red']" type="submit" name="do" :value="mergeStyle">
|
||||
<button class="ui button" :class="mergeButtonStyleClass" type="submit" name="do" :value="mergeStyle">
|
||||
{{ mergeStyleDetail.textDoMerge }}
|
||||
<template v-if="autoMergeWhenSucceed">
|
||||
{{ mergeForm.textAutoMergeButtonWhenSucceed }}
|
||||
</template>
|
||||
</button>
|
||||
|
||||
<button class="ui button merge-cancel" @click="toggleActionForm(false)">
|
||||
{{ mergeForm.textCancel }}
|
||||
</button>
|
||||
|
||||
<div class="ui checkbox ml-2" v-if="mergeForm.isPullBranchDeletable">
|
||||
<div class="ui checkbox ml-2" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed">
|
||||
<input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge">
|
||||
<label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<template v-if="!showActionForm">
|
||||
<div class="ui buttons merge-button" :class="[mergeForm.allOverridableChecksOk?'green':'red']" @click="toggleActionForm(true)">
|
||||
<div v-if="!showActionForm" class="df">
|
||||
<!-- the merge button -->
|
||||
<div class="ui buttons merge-button" :class="mergeButtonStyleClass" @click="toggleActionForm(true)" >
|
||||
<button class="ui button">
|
||||
<svg-icon name="octicon-git-merge"/>
|
||||
<span class="button-text">{{ mergeStyleDetail.textDoMerge }}</span>
|
||||
<span class="button-text">
|
||||
{{ mergeStyleDetail.textDoMerge }}
|
||||
<template v-if="autoMergeWhenSucceed">
|
||||
{{ mergeForm.textAutoMergeButtonWhenSucceed }}
|
||||
</template>
|
||||
</span>
|
||||
</button>
|
||||
<div class="ui dropdown icon button no-text" @click.stop="showMergeStyleMenu = !showMergeStyleMenu" v-if="mergeStyleAllowedCount>1">
|
||||
<svg-icon name="octicon-triangle-down" :size="14"/>
|
||||
<div class="menu" :class="{'show':showMergeStyleMenu}">
|
||||
<template v-for="msd in mergeForm.mergeStyles">
|
||||
<div class="item" v-if="msd.allowed" :key="msd.name" @click.stop="mergeStyle=msd.name">
|
||||
{{ msd.textDoMerge }}
|
||||
<!-- if can merge now, show one action "merge now", and an action "auto merge when succeed" -->
|
||||
<div class="item" v-if="msd.allowed && mergeForm.canMergeNow" :key="msd.name" @click.stop="switchMergeStyle(msd.name)">
|
||||
<div class="action-text">
|
||||
{{ msd.textDoMerge }}
|
||||
</div>
|
||||
<div v-if="!msd.hideAutoMerge" class="auto-merge-small" @click.stop="switchMergeStyle(msd.name, true)">
|
||||
<svg-icon name="octicon-clock" :size="14"/>
|
||||
<div class="auto-merge-tip">
|
||||
{{ mergeForm.textAutoMergeWhenSucceed }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- if can NOT merge now, only show one action "auto merge when succeed" -->
|
||||
<div class="item" v-if="msd.allowed && !mergeForm.canMergeNow && !msd.hideAutoMerge" :key="msd.name" @click.stop="switchMergeStyle(msd.name, true)">
|
||||
<div class="action-text">
|
||||
{{ msd.textDoMerge }} {{ mergeForm.textAutoMergeButtonWhenSucceed }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- the cancel auto merge button -->
|
||||
<form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="ml-4">
|
||||
<input type="hidden" name="_csrf" :value="csrfToken">
|
||||
<button class="ui button">
|
||||
{{ mergeForm.textAutoMergeCancelSchedule }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1055,10 +1055,6 @@
|
||||
.merge-section {
|
||||
background-color: var(--color-box-body);
|
||||
|
||||
.item {
|
||||
padding: .25rem 0;
|
||||
}
|
||||
|
||||
.item-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user