Changes
22 changed files (+397/-81)
-
-
@@ -18,6 +18,7 @@ linters:- ineffassign - revive - gofumpt - depguard enable-all: false disable-all: true fast: false
-
@@ -65,7 +66,15 @@ linters-settings:- name: modifies-value-receiver gofumpt: extra-rules: true lang-version: 1.18 lang-version: "1.18" depguard: # TODO: use depguard to replace import checks in gitea-vet list-type: denylist # Check the list against standard lib. include-go-root: true packages-with-error-message: - encoding/json: "use gitea's modules/json instead of encoding/json" - github.com/unknwon/com: "use gitea's util and replacements" issues: exclude-rules:
-
@@ -153,5 +162,6 @@ issues:- path: models/user/openid.go linters: - golint - linters: staticcheck - linters: - staticcheck text: "strings.Title is deprecated: The rule Title uses for word boundaries does not handle Unicode punctuation properly. Use golang.org/x/text/cases instead."
-
-
-
@@ -29,8 +29,7 @@ func TestGoGet(t *testing.T) {<body> go get --insecure %[1]s:%[2]s/blah/glah </body> </html> `, setting.Domain, setting.HTTPPort, setting.AppURL) </html>`, setting.Domain, setting.HTTPPort, setting.AppURL) assert.Equal(t, expected, resp.Body.String()) }
-
-
-
@@ -24,7 +24,6 @@ import ("code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" "github.com/unknwon/com" "xorm.io/xorm" "xorm.io/xorm/names" )
-
@@ -204,7 +203,7 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.EndeferFn := PrintCurrentTest(t, ourSkip) assert.NoError(t, os.RemoveAll(setting.RepoRootPath)) assert.NoError(t, com.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), assert.NoError(t, util.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) ownerDirs, err := os.ReadDir(setting.RepoRootPath) if err != nil {
-
-
-
@@ -10,10 +10,10 @@ import ("code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/nosql" "code.gitea.io/gitea/modules/util" "gitea.com/go-chi/cache" "github.com/go-redis/redis/v8" "github.com/unknwon/com" ) // RedisCacher represents a redis cache adapter implementation.
-
@@ -29,15 +29,15 @@ type RedisCacher struct {func (c *RedisCacher) Put(key string, val interface{}, expire int64) error { key = c.prefix + key if expire == 0 { if err := c.c.Set(graceful.GetManager().HammerContext(), key, com.ToStr(val), 0).Err(); err != nil { if err := c.c.Set(graceful.GetManager().HammerContext(), key, util.ToStr(val), 0).Err(); err != nil { return err } } else { dur, err := time.ParseDuration(com.ToStr(expire) + "s") dur, err := time.ParseDuration(util.ToStr(expire) + "s") if err != nil { return err } if err = c.c.Set(graceful.GetManager().HammerContext(), key, com.ToStr(val), dur).Err(); err != nil { if err = c.c.Set(graceful.GetManager().HammerContext(), key, util.ToStr(val), dur).Err(); err != nil { return err } }
-
-
-
@@ -31,13 +31,13 @@ import ("code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/auth" "gitea.com/go-chi/cache" "gitea.com/go-chi/session" chi "github.com/go-chi/chi/v5" "github.com/unknwon/com" "github.com/unrolled/render" "golang.org/x/crypto/pbkdf2" )
-
@@ -475,7 +475,7 @@ func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {} key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) text, err = com.AESGCMDecrypt(key, text) text, err = util.AESGCMDecrypt(key, text) return string(text), err == nil }
-
@@ -489,7 +489,7 @@ func (ctx *Context) SetSuperSecureCookie(secret, name, value string, expiry int)// CookieEncrypt encrypts a given value using the provided secret func (ctx *Context) CookieEncrypt(secret, value string) string { key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) text, err := com.AESGCMEncrypt(key, []byte(value)) text, err := util.AESGCMEncrypt(key, []byte(value)) if err != nil { panic("error encrypting cookie: " + err.Error()) }
-
-
-
@@ -19,13 +19,14 @@package context import ( "encoding/base32" "fmt" "net/http" "time" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "github.com/unknwon/com" ) // CSRF represents a CSRF service and is used to get the current token and validate a suspect token.
-
@@ -162,7 +163,12 @@ func prepareOptions(options []CsrfOptions) CsrfOptions {// Defaults. if len(opt.Secret) == 0 { opt.Secret = string(com.RandomCreateBytes(10)) randBytes, err := util.CryptoRandomBytes(8) if err != nil { // this panic can be handled by the recover() in http handlers panic(fmt.Errorf("failed to generate random bytes: %w", err)) } opt.Secret = base32.StdEncoding.EncodeToString(randBytes) } if len(opt.Header) == 0 { opt.Header = "X-CSRFToken"
-
@@ -211,7 +217,7 @@ func Csrfer(opt CsrfOptions, ctx *Context) CSRF {x.ID = "0" uid := ctx.Session.Get(opt.SessionKey) if uid != nil { x.ID = com.ToStr(uid) x.ID = util.ToStr(uid) } needsNew := false
-
-
-
@@ -8,6 +8,7 @@ package contextimport ( "context" "fmt" "html" "io" "net/http" "net/url"
-
@@ -29,7 +30,6 @@ import (asymkey_service "code.gitea.io/gitea/services/asymkey" "github.com/editorconfig/editorconfig-core-go/v2" "github.com/unknwon/com" ) // IssueTemplateDirCandidates issue templates directory
-
@@ -308,11 +308,9 @@ func EarlyResponseForGoGetMeta(ctx *Context) {ctx.PlainText(http.StatusBadRequest, "invalid repository path") return } ctx.PlainText(http.StatusOK, com.Expand(`<meta name="go-import" content="{GoGetImport} git {CloneLink}">`, map[string]string{ "GoGetImport": ComposeGoGetImport(username, reponame), "CloneLink": repo_model.ComposeHTTPSCloneURL(username, reponame), })) goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(username, reponame), repo_model.ComposeHTTPSCloneURL(username, reponame)) htmlMeta := fmt.Sprintf(`<meta name="go-import" content="%s">`, html.EscapeString(goImportContent)) ctx.PlainText(http.StatusOK, htmlMeta) } // RedirectToRepo redirect to a differently-named repository
-
-
-
@@ -8,7 +8,7 @@ package jsonimport ( "bytes" "encoding/binary" "encoding/json" "encoding/json" //nolint:depguard "io" jsoniter "github.com/json-iterator/go"
-
-
-
@@ -21,9 +21,9 @@ import ("code.gitea.io/gitea/modules/markup/common" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" "github.com/unknwon/com" "golang.org/x/net/html" "golang.org/x/net/html/atom" "mvdan.cc/xurls/v2"
-
@@ -838,7 +838,14 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] if exttrack && !ref.IsPull { ctx.Metas["index"] = ref.Issue link = createLink(com.Expand(ctx.Metas["format"], ctx.Metas), reftext, "ref-issue ref-external-issue") res, err := vars.Expand(ctx.Metas["format"], ctx.Metas) if err != nil { // here we could just log the error and continue the rendering log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err) } link = createLink(res, reftext, "ref-issue ref-external-issue") } else { // Path determines the type of link that will be rendered. It's unknown at this point whether // the linked item is actually a PR or an issue. Luckily it's of no real consequence because
-
-
-
@@ -22,10 +22,9 @@ import ("code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" "github.com/unknwon/com" ) var (
-
@@ -250,8 +249,13 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir,"CloneURL.HTTPS": cloneLink.HTTPS, "OwnerName": repo.OwnerName, } res, err := vars.Expand(string(data), match) if err != nil { // here we could just log the error and continue the rendering log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err) } if err = os.WriteFile(filepath.Join(tmpDir, "README.md"), []byte(com.Expand(string(data), match)), 0o644); err != nil { []byte(res), 0o644); err != nil { return fmt.Errorf("write README.md: %v", err) }
-
-
-
@@ -27,7 +27,6 @@ import ("code.gitea.io/gitea/modules/user" "code.gitea.io/gitea/modules/util" "github.com/unknwon/com" gossh "golang.org/x/crypto/ssh" ini "gopkg.in/ini.v1" )
-
@@ -612,7 +611,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) {Cfg.NameMapper = ini.SnackCase homeDir, err := com.HomeDir() homeDir, err := util.HomeDir() if err != nil { log.Fatal("Failed to get home directory: %v", err) }
-
-
-
@@ -5,9 +5,7 @@package sync import ( "github.com/unknwon/com" ) import "code.gitea.io/gitea/modules/util" // UniqueQueue is a queue which guarantees only one instance of same // identity is in the line. Instances with same identity will be
-
@@ -73,13 +71,13 @@ func (q *UniqueQueue) Queue() <-chan string {// Exist returns true if there is an instance with given identity // exists in the queue. func (q *UniqueQueue) Exist(id interface{}) bool { return q.table.IsRunning(com.ToStr(id)) return q.table.IsRunning(util.ToStr(id)) } // AddFunc adds new instance to the queue with a custom runnable function, // the queue is blocked until the function exits. func (q *UniqueQueue) AddFunc(id interface{}, fn func()) { idStr := com.ToStr(id) idStr := util.ToStr(id) q.table.lock.Lock() if _, ok := q.table.pool[idStr]; ok { q.table.lock.Unlock()
-
@@ -105,5 +103,5 @@ func (q *UniqueQueue) Add(id interface{}) {// Remove removes instance from the queue. func (q *UniqueQueue) Remove(id interface{}) { q.table.Stop(com.ToStr(id)) q.table.Stop(util.ToStr(id)) }
-
-
-
@@ -0,0 +1,93 @@// 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 vars import ( "fmt" "strings" "unicode" "unicode/utf8" ) // ErrWrongSyntax represents a wrong syntax with a template type ErrWrongSyntax struct { Template string } func (err ErrWrongSyntax) Error() string { return fmt.Sprintf("wrong syntax found in %s", err.Template) } // ErrVarMissing represents an error that no matched variable type ErrVarMissing struct { Template string Var string } func (err ErrVarMissing) Error() string { return fmt.Sprintf("the variable %s is missing for %s", err.Var, err.Template) } // Expand replaces all variables like {var} by `vars` map, it always returns the expanded string regardless of errors // if error occurs, the error part doesn't change and is returned as it is. func Expand(template string, vars map[string]string) (string, error) { // in the future, if necessary, we can introduce some escape-char, // for example: it will use `#' as a reversed char, templates will use `{#{}` to do escape and output char '{'. var buf strings.Builder var err error posBegin := 0 strLen := len(template) for posBegin < strLen { // find the next `{` pos := strings.IndexByte(template[posBegin:], '{') if pos == -1 { buf.WriteString(template[posBegin:]) break } // copy texts between vars buf.WriteString(template[posBegin : posBegin+pos]) // find the var between `{` and `}`/end posBegin += pos posEnd := posBegin + 1 for posEnd < strLen { if template[posEnd] == '}' { posEnd++ break } // in the future, if we need to support escape chars, we can do: if (isEscapeChar) { posEnd+=2 } posEnd++ } // the var part, it can be "{", "{}", "{..." or or "{...}" part := template[posBegin:posEnd] posBegin = posEnd if part == "{}" || part[len(part)-1] != '}' { // treat "{}" or "{..." as error err = ErrWrongSyntax{Template: template} buf.WriteString(part) } else { // now we get a valid key "{...}" key := part[1 : len(part)-1] keyFirst, _ := utf8.DecodeRuneInString(key) if unicode.IsSpace(keyFirst) || unicode.IsPunct(keyFirst) || unicode.IsControl(keyFirst) { // the if key doesn't start with a letter, then we do not treat it as a var now buf.WriteString(part) } else { // look up in the map if val, ok := vars[key]; ok { buf.WriteString(val) } else { // write the non-existing var as it is buf.WriteString(part) err = ErrVarMissing{Template: template, Var: key} } } } } return buf.String(), err }
-
-
-
@@ -0,0 +1,72 @@// 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 vars import ( "testing" "github.com/stretchr/testify/assert" ) func TestExpandVars(t *testing.T) { kases := []struct { tmpl string data map[string]string out string error bool }{ { tmpl: "{a}", data: map[string]string{ "a": "1", }, out: "1", }, { tmpl: "expand {a}, {b} and {c}, with non-var { } {#}", data: map[string]string{ "a": "1", "b": "2", "c": "3", }, out: "expand 1, 2 and 3, with non-var { } {#}", }, { tmpl: "䏿–‡å†…容 {一}, {二} å’Œ {三} 䏿–‡ç»“å°¾", data: map[string]string{ "一": "11", "二": "22", "三": "33", }, out: "䏿–‡å†…容 11, 22 å’Œ 33 䏿–‡ç»“å°¾", }, { tmpl: "expand {{a}, {b} and {c}", data: map[string]string{ "a": "foo", "b": "bar", }, out: "expand {{a}, bar and {c}", error: true, }, { tmpl: "expand } {} and {", out: "expand } {} and {", error: true, }, } for _, kase := range kases { t.Run(kase.tmpl, func(t *testing.T) { res, err := Expand(kase.tmpl, kase.data) assert.EqualValues(t, kase.out, res) if kase.error { assert.Error(t, err) } else { assert.NoError(t, err) } }) } }
-
-
modules/util/copy.go (deleted)
-
@@ -1,20 +0,0 @@// Copyright 2020 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 util import ( "github.com/unknwon/com" ) // CopyFile copies file from source to target path. func CopyFile(src, dest string) error { return com.Copy(src, dest) } // CopyDir copy files recursively from source to target directory. // It returns error when error occurs in underlying functions. func CopyDir(srcPath, destPath string) error { return com.CopyDir(srcPath, destPath) }
-
-
-
@@ -9,7 +9,7 @@ import () // ReadAtMost reads at most len(buf) bytes from r into buf. // It returns the number of bytes copied. n is only less then len(buf) if r provides fewer bytes. // It returns the number of bytes copied. n is only less than len(buf) if r provides fewer bytes. // If EOF occurs while reading, err will be nil. func ReadAtMost(r io.Reader, buf []byte) (n int, err error) { n, err = io.ReadFull(r, buf)
-
-
modules/util/legacy.go (new)
-
@@ -0,0 +1,84 @@// 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 util import ( "crypto/aes" "crypto/cipher" "crypto/rand" "errors" "github.com/unknwon/com" //nolint:depguard ) // CopyFile copies file from source to target path. func CopyFile(src, dest string) error { return com.Copy(src, dest) } // CopyDir copy files recursively from source to target directory. // It returns error when error occurs in underlying functions. func CopyDir(srcPath, destPath string) error { return com.CopyDir(srcPath, destPath) } // ToStr converts any interface to string. should be replaced. func ToStr(value interface{}, args ...int) string { return com.ToStr(value, args...) } // ToSnakeCase converts a string to snake_case. should be replaced. func ToSnakeCase(str string) string { return com.ToSnakeCase(str) } // AESGCMEncrypt (from legacy package): encrypts plaintext with the given key using AES in GCM mode. should be replaced. func AESGCMEncrypt(key, plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, err } ciphertext := gcm.Seal(nil, nonce, plaintext, nil) return append(nonce, ciphertext...), nil } // AESGCMDecrypt (from legacy package): decrypts ciphertext with the given key using AES in GCM mode. should be replaced. func AESGCMDecrypt(key, ciphertext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } size := gcm.NonceSize() if len(ciphertext)-size <= 0 { return nil, errors.New("ciphertext is empty") } nonce := ciphertext[:size] ciphertext = ciphertext[size:] plainText, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, err } return plainText, nil }
-
-
-
@@ -0,0 +1,37 @@// 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 util import ( "crypto/aes" "crypto/rand" "testing" "github.com/stretchr/testify/assert" "github.com/unknwon/com" //nolint:depguard ) func TestAESGCM(t *testing.T) { t.Parallel() key := make([]byte, aes.BlockSize) _, err := rand.Read(key) assert.NoError(t, err) plaintext := []byte("this will be encrypted") ciphertext, err := AESGCMEncrypt(key, plaintext) assert.NoError(t, err) decrypted, err := AESGCMDecrypt(key, ciphertext) assert.NoError(t, err) assert.Equal(t, plaintext, decrypted) // at the moment, we make sure the result is the same as the legacy package, this assertion can be removed in next round refactoring legacy, err := com.AESGCMDecrypt(key, ciphertext) assert.NoError(t, err) assert.Equal(t, legacy, plaintext) }
-
-
-
@@ -154,6 +154,10 @@ func StatDir(rootPath string, includeDir ...bool) ([]string, error) {return statDir(rootPath, "", isIncludeDir, false, false) } func isOSWindows() bool { return runtime.GOOS == "windows" } // FileURLToPath extracts the path information from a file://... url. func FileURLToPath(u *url.URL) (string, error) { if u.Scheme != "file" {
-
@@ -162,7 +166,7 @@ func FileURLToPath(u *url.URL) (string, error) {path := u.Path if runtime.GOOS != "windows" { if !isOSWindows() { return path, nil }
-
@@ -173,3 +177,24 @@ func FileURLToPath(u *url.URL) (string, error) {} return path, nil } // HomeDir returns path of '~'(in Linux) on Windows, // it returns error when the variable does not exist. func HomeDir() (home string, err error) { // TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually) // so at the moment we can not use `user.Current().HomeDir` if isOSWindows() { home = os.Getenv("USERPROFILE") if home == "" { home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") } } else { home = os.Getenv("HOME") } if home == "" { return "", errors.New("cannot get home directory") } return home, nil }
-
-
-
@@ -10,10 +10,10 @@ import ("strings" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" "gitea.com/go-chi/binding" "github.com/unknwon/com" ) // Form form binding interface
-
@@ -22,7 +22,7 @@ type Form interface {} func init() { binding.SetNameMapper(com.ToSnakeCase) binding.SetNameMapper(util.ToSnakeCase) } // AssignForm assign form values back to the template data.
-
@@ -43,7 +43,7 @@ func AssignForm(form interface{}, data map[string]interface{}) {if fieldName == "-" { continue } else if len(fieldName) == 0 { fieldName = com.ToSnakeCase(field.Name) fieldName = util.ToSnakeCase(field.Name) } data[fieldName] = val.Field(i).Interface()
-
-
-
@@ -5,6 +5,8 @@package web import ( "fmt" "html" "net/http" "net/url" "path"
-
@@ -14,8 +16,6 @@ import ("code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/unknwon/com" ) func goGet(ctx *context.Context) {
-
@@ -65,23 +65,23 @@ func goGet(ctx *context.Context) {if appURL.Scheme == string(setting.HTTP) { insecure = "--insecure " } ctx.RespHeader().Set("Content-Type", "text/html") ctx.Status(http.StatusOK) _, _ = ctx.Write([]byte(com.Expand(`<!doctype html> goGetImport := context.ComposeGoGetImport(ownerName, trimmedRepoName) goImportContent := fmt.Sprintf("%s git %s", goGetImport, repo_model.ComposeHTTPSCloneURL(ownerName, repoName) /*CloneLink*/) goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/) goGetCli := fmt.Sprintf("go get %s%s", insecure, goGetImport) res := fmt.Sprintf(`<!doctype html> <html> <head> <meta name="go-import" content="{GoGetImport} git {CloneLink}"> <meta name="go-source" content="{GoGetImport} _ {GoDocDirectory} {GoDocFile}"> <meta name="go-import" content="%s"> <meta name="go-source" content="%s"> </head> <body> go get {Insecure}{GoGetImport} %s </body> </html> `, map[string]string{ "GoGetImport": context.ComposeGoGetImport(ownerName, trimmedRepoName), "CloneLink": repo_model.ComposeHTTPSCloneURL(ownerName, repoName), "GoDocDirectory": prefix + "{/dir}", "GoDocFile": prefix + "{/dir}/{file}#L{line}", "Insecure": insecure, }))) </html>`, html.EscapeString(goImportContent), html.EscapeString(goSourceContent), html.EscapeString(goGetCli)) ctx.RespHeader().Set("Content-Type", "text/html") _, _ = ctx.Write([]byte(res)) }
-
-
-
@@ -35,6 +35,7 @@ import ("code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/upload" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web"
-
@@ -43,8 +44,6 @@ import ("code.gitea.io/gitea/services/forms" issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" "github.com/unknwon/com" ) const (
-
@@ -1113,7 +1112,13 @@ func ViewIssue(ctx *context.Context) {if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" { metas := ctx.Repo.Repository.ComposeMetas() metas["index"] = ctx.Params(":index") ctx.Redirect(com.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)) res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas) if err != nil { log.Error("unable to expand template vars for issue url. issue: %s, err: %v", metas["index"], err) ctx.ServerError("Expand", err) return } ctx.Redirect(res) return } } else if err != nil && !repo_model.IsErrUnitTypeNotExist(err) {
-