1f1ecda541
* Store original author info for migrated issues and comments Keep original author name for displaying in Gitea interface and also store original author user ID for potential future use in linking accounts from old location. * Add original_url for repo Store the original URL for a migrated repo Clean up migrations/tests * fix migration * fix golangci-lint * make 'make revive' happy also * Modify templates to use OriginalAuthor if set Use the original author name in templates if it is set rather than the user who migrated/currently owns the issues * formatting fixes * make generate-swagger * Use default avatar for imported comments * Remove no longer used IgnoreIssueAuthor option * Add OriginalAuthorID to swagger also
554 lines
14 KiB
Go
554 lines
14 KiB
Go
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
// Copyright 2014 The Gogs 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 templates
|
|
|
|
import (
|
|
"bytes"
|
|
"container/list"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"html"
|
|
"html/template"
|
|
"mime"
|
|
"net/url"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/modules/util"
|
|
|
|
"code.gitea.io/gitea/models"
|
|
"code.gitea.io/gitea/modules/base"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/markup"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
|
|
"golang.org/x/net/html/charset"
|
|
"golang.org/x/text/transform"
|
|
"gopkg.in/editorconfig/editorconfig-core-go.v1"
|
|
)
|
|
|
|
// NewFuncMap returns functions for injecting to templates
|
|
func NewFuncMap() []template.FuncMap {
|
|
return []template.FuncMap{map[string]interface{}{
|
|
"GoVer": func() string {
|
|
return strings.Title(runtime.Version())
|
|
},
|
|
"UseHTTPS": func() bool {
|
|
return strings.HasPrefix(setting.AppURL, "https")
|
|
},
|
|
"AppName": func() string {
|
|
return setting.AppName
|
|
},
|
|
"AppSubUrl": func() string {
|
|
return setting.AppSubURL
|
|
},
|
|
"AppUrl": func() string {
|
|
return setting.AppURL
|
|
},
|
|
"AppVer": func() string {
|
|
return setting.AppVer
|
|
},
|
|
"AppBuiltWith": func() string {
|
|
return setting.AppBuiltWith
|
|
},
|
|
"AppDomain": func() string {
|
|
return setting.Domain
|
|
},
|
|
"DisableGravatar": func() bool {
|
|
return setting.DisableGravatar
|
|
},
|
|
"DefaultShowFullName": func() bool {
|
|
return setting.UI.DefaultShowFullName
|
|
},
|
|
"ShowFooterTemplateLoadTime": func() bool {
|
|
return setting.ShowFooterTemplateLoadTime
|
|
},
|
|
"LoadTimes": func(startTime time.Time) string {
|
|
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
|
|
},
|
|
"AvatarLink": base.AvatarLink,
|
|
"Safe": Safe,
|
|
"SafeJS": SafeJS,
|
|
"Str2html": Str2html,
|
|
"TimeSince": base.TimeSince,
|
|
"TimeSinceUnix": base.TimeSinceUnix,
|
|
"RawTimeSince": base.RawTimeSince,
|
|
"FileSize": base.FileSize,
|
|
"Subtract": base.Subtract,
|
|
"EntryIcon": base.EntryIcon,
|
|
"MigrationIcon": MigrationIcon,
|
|
"Add": func(a, b int) int {
|
|
return a + b
|
|
},
|
|
"ActionIcon": ActionIcon,
|
|
"DateFmtLong": func(t time.Time) string {
|
|
return t.Format(time.RFC1123Z)
|
|
},
|
|
"DateFmtShort": func(t time.Time) string {
|
|
return t.Format("Jan 02, 2006")
|
|
},
|
|
"SizeFmt": func(s int64) string {
|
|
return base.FileSize(s)
|
|
},
|
|
"List": List,
|
|
"SubStr": func(str string, start, length int) string {
|
|
if len(str) == 0 {
|
|
return ""
|
|
}
|
|
end := start + length
|
|
if length == -1 {
|
|
end = len(str)
|
|
}
|
|
if len(str) < end {
|
|
return str
|
|
}
|
|
return str[start:end]
|
|
},
|
|
"EllipsisString": base.EllipsisString,
|
|
"DiffTypeToStr": DiffTypeToStr,
|
|
"DiffLineTypeToStr": DiffLineTypeToStr,
|
|
"Sha1": Sha1,
|
|
"ShortSha": base.ShortSha,
|
|
"MD5": base.EncodeMD5,
|
|
"ActionContent2Commits": ActionContent2Commits,
|
|
"PathEscape": url.PathEscape,
|
|
"EscapePound": func(str string) string {
|
|
return strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(str)
|
|
},
|
|
"PathEscapeSegments": util.PathEscapeSegments,
|
|
"URLJoin": util.URLJoin,
|
|
"RenderCommitMessage": RenderCommitMessage,
|
|
"RenderCommitMessageLink": RenderCommitMessageLink,
|
|
"RenderCommitBody": RenderCommitBody,
|
|
"RenderNote": RenderNote,
|
|
"IsMultilineCommitMessage": IsMultilineCommitMessage,
|
|
"ThemeColorMetaTag": func() string {
|
|
return setting.UI.ThemeColorMetaTag
|
|
},
|
|
"MetaAuthor": func() string {
|
|
return setting.UI.Meta.Author
|
|
},
|
|
"MetaDescription": func() string {
|
|
return setting.UI.Meta.Description
|
|
},
|
|
"MetaKeywords": func() string {
|
|
return setting.UI.Meta.Keywords
|
|
},
|
|
"FilenameIsImage": func(filename string) bool {
|
|
mimeType := mime.TypeByExtension(filepath.Ext(filename))
|
|
return strings.HasPrefix(mimeType, "image/")
|
|
},
|
|
"TabSizeClass": func(ec *editorconfig.Editorconfig, filename string) string {
|
|
if ec != nil {
|
|
def := ec.GetDefinitionForFilename(filename)
|
|
if def.TabWidth > 0 {
|
|
return fmt.Sprintf("tab-size-%d", def.TabWidth)
|
|
}
|
|
}
|
|
return "tab-size-8"
|
|
},
|
|
"SubJumpablePath": func(str string) []string {
|
|
var path []string
|
|
index := strings.LastIndex(str, "/")
|
|
if index != -1 && index != len(str) {
|
|
path = append(path, str[0:index+1], str[index+1:])
|
|
} else {
|
|
path = append(path, str)
|
|
}
|
|
return path
|
|
},
|
|
"JsonPrettyPrint": func(in string) string {
|
|
var out bytes.Buffer
|
|
err := json.Indent(&out, []byte(in), "", " ")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return out.String()
|
|
},
|
|
"DisableGitHooks": func() bool {
|
|
return setting.DisableGitHooks
|
|
},
|
|
"DisableImportLocal": func() bool {
|
|
return !setting.ImportLocalPaths
|
|
},
|
|
"TrN": TrN,
|
|
"Dict": func(values ...interface{}) (map[string]interface{}, error) {
|
|
if len(values)%2 != 0 {
|
|
return nil, errors.New("invalid dict call")
|
|
}
|
|
dict := make(map[string]interface{}, len(values)/2)
|
|
for i := 0; i < len(values); i += 2 {
|
|
key, ok := values[i].(string)
|
|
if !ok {
|
|
return nil, errors.New("dict keys must be strings")
|
|
}
|
|
dict[key] = values[i+1]
|
|
}
|
|
return dict, nil
|
|
},
|
|
"Printf": fmt.Sprintf,
|
|
"Escape": Escape,
|
|
"Sec2Time": models.SecToTime,
|
|
"ParseDeadline": func(deadline string) []string {
|
|
return strings.Split(deadline, "|")
|
|
},
|
|
"DefaultTheme": func() string {
|
|
return setting.UI.DefaultTheme
|
|
},
|
|
"dict": func(values ...interface{}) (map[string]interface{}, error) {
|
|
if len(values) == 0 {
|
|
return nil, errors.New("invalid dict call")
|
|
}
|
|
|
|
dict := make(map[string]interface{})
|
|
|
|
for i := 0; i < len(values); i++ {
|
|
switch key := values[i].(type) {
|
|
case string:
|
|
i++
|
|
if i == len(values) {
|
|
return nil, errors.New("specify the key for non array values")
|
|
}
|
|
dict[key] = values[i]
|
|
case map[string]interface{}:
|
|
m := values[i].(map[string]interface{})
|
|
for i, v := range m {
|
|
dict[i] = v
|
|
}
|
|
default:
|
|
return nil, errors.New("dict values must be maps")
|
|
}
|
|
}
|
|
return dict, nil
|
|
},
|
|
"percentage": func(n int, values ...int) float32 {
|
|
var sum = 0
|
|
for i := 0; i < len(values); i++ {
|
|
sum += values[i]
|
|
}
|
|
return float32(n) * 100 / float32(sum)
|
|
},
|
|
}}
|
|
}
|
|
|
|
// Safe render raw as HTML
|
|
func Safe(raw string) template.HTML {
|
|
return template.HTML(raw)
|
|
}
|
|
|
|
// SafeJS renders raw as JS
|
|
func SafeJS(raw string) template.JS {
|
|
return template.JS(raw)
|
|
}
|
|
|
|
// Str2html render Markdown text to HTML
|
|
func Str2html(raw string) template.HTML {
|
|
return template.HTML(markup.Sanitize(raw))
|
|
}
|
|
|
|
// Escape escapes a HTML string
|
|
func Escape(raw string) string {
|
|
return html.EscapeString(raw)
|
|
}
|
|
|
|
// List traversings the list
|
|
func List(l *list.List) chan interface{} {
|
|
e := l.Front()
|
|
c := make(chan interface{})
|
|
go func() {
|
|
for e != nil {
|
|
c <- e.Value
|
|
e = e.Next()
|
|
}
|
|
close(c)
|
|
}()
|
|
return c
|
|
}
|
|
|
|
// Sha1 returns sha1 sum of string
|
|
func Sha1(str string) string {
|
|
return base.EncodeSha1(str)
|
|
}
|
|
|
|
// ToUTF8WithErr converts content to UTF8 encoding
|
|
func ToUTF8WithErr(content []byte) (string, error) {
|
|
charsetLabel, err := base.DetectEncoding(content)
|
|
if err != nil {
|
|
return "", err
|
|
} else if charsetLabel == "UTF-8" {
|
|
return string(base.RemoveBOMIfPresent(content)), nil
|
|
}
|
|
|
|
encoding, _ := charset.Lookup(charsetLabel)
|
|
if encoding == nil {
|
|
return string(content), fmt.Errorf("Unknown encoding: %s", charsetLabel)
|
|
}
|
|
|
|
// If there is an error, we concatenate the nicely decoded part and the
|
|
// original left over. This way we won't lose data.
|
|
result, n, err := transform.Bytes(encoding.NewDecoder(), content)
|
|
if err != nil {
|
|
result = append(result, content[n:]...)
|
|
}
|
|
|
|
result = base.RemoveBOMIfPresent(result)
|
|
|
|
return string(result), err
|
|
}
|
|
|
|
// ToUTF8WithFallback detects the encoding of content and coverts to UTF-8 if possible
|
|
func ToUTF8WithFallback(content []byte) []byte {
|
|
charsetLabel, err := base.DetectEncoding(content)
|
|
if err != nil || charsetLabel == "UTF-8" {
|
|
return base.RemoveBOMIfPresent(content)
|
|
}
|
|
|
|
encoding, _ := charset.Lookup(charsetLabel)
|
|
if encoding == nil {
|
|
return content
|
|
}
|
|
|
|
// If there is an error, we concatenate the nicely decoded part and the
|
|
// original left over. This way we won't lose data.
|
|
result, n, err := transform.Bytes(encoding.NewDecoder(), content)
|
|
if err != nil {
|
|
return append(result, content[n:]...)
|
|
}
|
|
|
|
return base.RemoveBOMIfPresent(result)
|
|
}
|
|
|
|
// ToUTF8 converts content to UTF8 encoding and ignore error
|
|
func ToUTF8(content string) string {
|
|
res, _ := ToUTF8WithErr([]byte(content))
|
|
return res
|
|
}
|
|
|
|
// ReplaceLeft replaces all prefixes 'oldS' in 's' with 'newS'.
|
|
func ReplaceLeft(s, oldS, newS string) string {
|
|
oldLen, newLen, i, n := len(oldS), len(newS), 0, 0
|
|
for ; i < len(s) && strings.HasPrefix(s[i:], oldS); n++ {
|
|
i += oldLen
|
|
}
|
|
|
|
// simple optimization
|
|
if n == 0 {
|
|
return s
|
|
}
|
|
|
|
// allocating space for the new string
|
|
curLen := n*newLen + len(s[i:])
|
|
replacement := make([]byte, curLen)
|
|
|
|
j := 0
|
|
for ; j < n*newLen; j += newLen {
|
|
copy(replacement[j:j+newLen], newS)
|
|
}
|
|
|
|
copy(replacement[j:], s[i:])
|
|
return string(replacement)
|
|
}
|
|
|
|
// RenderCommitMessage renders commit message with XSS-safe and special links.
|
|
func RenderCommitMessage(msg, urlPrefix string, metas map[string]string) template.HTML {
|
|
return RenderCommitMessageLink(msg, urlPrefix, "", metas)
|
|
}
|
|
|
|
// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
|
|
// default url, handling for special links.
|
|
func RenderCommitMessageLink(msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
|
|
cleanMsg := template.HTMLEscapeString(msg)
|
|
// we can safely assume that it will not return any error, since there
|
|
// shouldn't be any special HTML.
|
|
fullMessage, err := markup.RenderCommitMessage([]byte(cleanMsg), urlPrefix, urlDefault, metas)
|
|
if err != nil {
|
|
log.Error("RenderCommitMessage: %v", err)
|
|
return ""
|
|
}
|
|
msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n")
|
|
if len(msgLines) == 0 {
|
|
return template.HTML("")
|
|
}
|
|
return template.HTML(msgLines[0])
|
|
}
|
|
|
|
// RenderCommitBody extracts the body of a commit message without its title.
|
|
func RenderCommitBody(msg, urlPrefix string, metas map[string]string) template.HTML {
|
|
cleanMsg := template.HTMLEscapeString(msg)
|
|
fullMessage, err := markup.RenderCommitMessage([]byte(cleanMsg), urlPrefix, "", metas)
|
|
if err != nil {
|
|
log.Error("RenderCommitMessage: %v", err)
|
|
return ""
|
|
}
|
|
body := strings.Split(strings.TrimSpace(string(fullMessage)), "\n")
|
|
if len(body) == 0 {
|
|
return template.HTML("")
|
|
}
|
|
return template.HTML(strings.Join(body[1:], "\n"))
|
|
}
|
|
|
|
// RenderNote renders the contents of a git-notes file as a commit message.
|
|
func RenderNote(msg, urlPrefix string, metas map[string]string) template.HTML {
|
|
cleanMsg := template.HTMLEscapeString(msg)
|
|
fullMessage, err := markup.RenderCommitMessage([]byte(cleanMsg), urlPrefix, "", metas)
|
|
if err != nil {
|
|
log.Error("RenderNote: %v", err)
|
|
return ""
|
|
}
|
|
return template.HTML(string(fullMessage))
|
|
}
|
|
|
|
// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
|
|
func IsMultilineCommitMessage(msg string) bool {
|
|
return strings.Count(strings.TrimSpace(msg), "\n") >= 1
|
|
}
|
|
|
|
// Actioner describes an action
|
|
type Actioner interface {
|
|
GetOpType() models.ActionType
|
|
GetActUserName() string
|
|
GetRepoUserName() string
|
|
GetRepoName() string
|
|
GetRepoPath() string
|
|
GetRepoLink() string
|
|
GetBranch() string
|
|
GetContent() string
|
|
GetCreate() time.Time
|
|
GetIssueInfos() []string
|
|
}
|
|
|
|
// ActionIcon accepts an action operation type and returns an icon class name.
|
|
func ActionIcon(opType models.ActionType) string {
|
|
switch opType {
|
|
case models.ActionCreateRepo, models.ActionTransferRepo:
|
|
return "repo"
|
|
case models.ActionCommitRepo, models.ActionPushTag, models.ActionDeleteTag, models.ActionDeleteBranch:
|
|
return "git-commit"
|
|
case models.ActionCreateIssue:
|
|
return "issue-opened"
|
|
case models.ActionCreatePullRequest:
|
|
return "git-pull-request"
|
|
case models.ActionCommentIssue:
|
|
return "comment-discussion"
|
|
case models.ActionMergePullRequest:
|
|
return "git-merge"
|
|
case models.ActionCloseIssue, models.ActionClosePullRequest:
|
|
return "issue-closed"
|
|
case models.ActionReopenIssue, models.ActionReopenPullRequest:
|
|
return "issue-reopened"
|
|
case models.ActionMirrorSyncPush, models.ActionMirrorSyncCreate, models.ActionMirrorSyncDelete:
|
|
return "repo-clone"
|
|
default:
|
|
return "invalid type"
|
|
}
|
|
}
|
|
|
|
// ActionContent2Commits converts action content to push commits
|
|
func ActionContent2Commits(act Actioner) *models.PushCommits {
|
|
push := models.NewPushCommits()
|
|
if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
|
|
log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
|
|
}
|
|
return push
|
|
}
|
|
|
|
// DiffTypeToStr returns diff type name
|
|
func DiffTypeToStr(diffType int) string {
|
|
diffTypes := map[int]string{
|
|
1: "add", 2: "modify", 3: "del", 4: "rename",
|
|
}
|
|
return diffTypes[diffType]
|
|
}
|
|
|
|
// DiffLineTypeToStr returns diff line type name
|
|
func DiffLineTypeToStr(diffType int) string {
|
|
switch diffType {
|
|
case 2:
|
|
return "add"
|
|
case 3:
|
|
return "del"
|
|
case 4:
|
|
return "tag"
|
|
}
|
|
return "same"
|
|
}
|
|
|
|
// Language specific rules for translating plural texts
|
|
var trNLangRules = map[string]func(int64) int{
|
|
"en-US": func(cnt int64) int {
|
|
if cnt == 1 {
|
|
return 0
|
|
}
|
|
return 1
|
|
},
|
|
"lv-LV": func(cnt int64) int {
|
|
if cnt%10 == 1 && cnt%100 != 11 {
|
|
return 0
|
|
}
|
|
return 1
|
|
},
|
|
"ru-RU": func(cnt int64) int {
|
|
if cnt%10 == 1 && cnt%100 != 11 {
|
|
return 0
|
|
}
|
|
return 1
|
|
},
|
|
"zh-CN": func(cnt int64) int {
|
|
return 0
|
|
},
|
|
"zh-HK": func(cnt int64) int {
|
|
return 0
|
|
},
|
|
"zh-TW": func(cnt int64) int {
|
|
return 0
|
|
},
|
|
"fr-FR": func(cnt int64) int {
|
|
if cnt > -2 && cnt < 2 {
|
|
return 0
|
|
}
|
|
return 1
|
|
},
|
|
}
|
|
|
|
// TrN returns key to be used for plural text translation
|
|
func TrN(lang string, cnt interface{}, key1, keyN string) string {
|
|
var c int64
|
|
if t, ok := cnt.(int); ok {
|
|
c = int64(t)
|
|
} else if t, ok := cnt.(int16); ok {
|
|
c = int64(t)
|
|
} else if t, ok := cnt.(int32); ok {
|
|
c = int64(t)
|
|
} else if t, ok := cnt.(int64); ok {
|
|
c = t
|
|
} else {
|
|
return keyN
|
|
}
|
|
|
|
ruleFunc, ok := trNLangRules[lang]
|
|
if !ok {
|
|
ruleFunc = trNLangRules["en-US"]
|
|
}
|
|
|
|
if ruleFunc(c) == 0 {
|
|
return key1
|
|
}
|
|
return keyN
|
|
}
|
|
|
|
// MigrationIcon returns a Font Awesome name matching the service an issue/comment was migrated from
|
|
func MigrationIcon(hostname string) string {
|
|
switch hostname {
|
|
case "github.com":
|
|
return "fa-github"
|
|
default:
|
|
return "fa-git-alt"
|
|
}
|
|
}
|