Переглянути джерело

Use go-bindata to embed `public` and `templates` files into binary (#5920)

* fixed private repositories are hidden in the organization's view

* use go-bindata integrate public and templates files to gogs binary

* optimize Dockerfile don't COPY public and templates files

* use kevinburke's go-bindata to generate assets code

* reset develepment as default run mode in configure file

* optimize generated assets code relayout and help function

* fixed code format

* Update conf/app.ini

* assets: add LICENSE headers

* Some housekeeping

* assets/public: simplify code logic

* assets/templates: simplify code logic

* cmd/web: more concise variable names

* Minor changes

* Add custom public and templates support back

Co-authored-by: ᴜɴᴋɴᴡᴏɴ <u@gogs.io>
Michael Li 4 роки тому
батько
коміт
4d83fd4238

+ 0 - 2
Dockerfile

@@ -33,8 +33,6 @@ COPY docker/nsswitch.conf /etc/nsswitch.conf
 
 WORKDIR /app/gogs
 COPY docker ./docker
-COPY templates ./templates
-COPY public ./public
 COPY --from=binarybuilder /go/src/github.com/gogs/gogs/gogs .
 
 RUN ./docker/finalize.sh

+ 0 - 2
Dockerfile.aarch64

@@ -22,8 +22,6 @@ ENV GOGS_CUSTOM /data/gogs
 # Configure LibC Name Service
 COPY docker/nsswitch.conf /etc/nsswitch.conf
 COPY docker /app/gogs/docker
-COPY templates /app/gogs/templates
-COPY public /app/gogs/public
 
 WORKDIR /app/gogs/build
 COPY . .

+ 0 - 2
Dockerfile.aarch64hub

@@ -24,8 +24,6 @@ RUN chmod +x /usr/sbin/gosu \
 
 
 COPY docker /app/gogs/docker
-COPY templates /app/gogs/templates
-COPY public /app/gogs/public
 WORKDIR /app/gogs/build
 COPY . .
 

+ 0 - 2
Dockerfile.rpi

@@ -22,8 +22,6 @@ ENV GOGS_CUSTOM /data/gogs
 # Configure LibC Name Service
 COPY docker/nsswitch.conf /etc/nsswitch.conf
 COPY docker /app/gogs/docker
-COPY templates /app/gogs/templates
-COPY public /app/gogs/public
 
 WORKDIR /app/gogs/build
 COPY . .

+ 0 - 2
Dockerfile.rpihub

@@ -36,8 +36,6 @@ RUN chmod +x /usr/sbin/gosu \
 # Configure LibC Name Service
 COPY docker/nsswitch.conf /etc/nsswitch.conf
 COPY docker /app/gogs/docker
-COPY templates /app/gogs/templates
-COPY public /app/gogs/public
 
 WORKDIR /app/gogs/build
 COPY . .

+ 23 - 9
Makefile

@@ -1,9 +1,12 @@
 LDFLAGS += -X "gogs.io/gogs/internal/setting.BuildTime=$(shell date -u '+%Y-%m-%d %I:%M:%S %Z')"
-LDFLAGS += -X "gogs.io/gogs/internal/setting.BuildGitHash=$(shell git rev-parse HEAD)"
+LDFLAGS += -X "gogs.io/gogs/internal/setting.BuildCommit=$(shell git rev-parse HEAD)"
 
-DATA_FILES := $(shell find conf | sed 's/ /\\ /g')
+CONF_FILES := $(shell find conf | sed 's/ /\\ /g')
+TEMPLATES_FILES := $(shell find templates | sed 's/ /\\ /g')
+PUBLIC_FILES := $(shell find public | sed 's/ /\\ /g')
 LESS_FILES := $(wildcard public/less/gogs.less public/less/_*.less)
-GENERATED  := internal/bindata/bindata.go public/css/gogs.css
+ASSETS_GENERATED := internal/assets/conf/conf_gen.go internal/assets/templates/templates_gen.go internal/assets/public/public_gen.go
+GENERATED := $(ASSETS_GENERATED) public/css/gogs.css
 
 OS := $(shell uname)
 
@@ -15,7 +18,7 @@ RELEASE_GOGS = "release/gogs"
 NOW = $(shell date -u '+%Y%m%d%I%M%S')
 GOVET = go tool vet -composites=false -methods=false -structtags=false
 
-.PHONY: build pack release bindata clean
+.PHONY: build pack release generate clean
 
 .IGNORE: public/css/gogs.css
 
@@ -45,16 +48,27 @@ build-dev-race: $(GENERATED) govet
 pack:
 	rm -rf $(RELEASE_GOGS)
 	mkdir -p $(RELEASE_GOGS)
-	cp -r gogs LICENSE README.md README_ZH.md templates public scripts $(RELEASE_GOGS)
-	rm -rf $(RELEASE_GOGS)/public/config.codekit $(RELEASE_GOGS)/public/less
+	cp -r gogs LICENSE README.md README_ZH.md scripts $(RELEASE_GOGS)
 	cd $(RELEASE_ROOT) && zip -r gogs.$(NOW).zip "gogs"
 
 release: build pack
 
-bindata: internal/bindata/bindata.go
+generate: $(ASSETS_GENERATED)
 
-internal/bindata/bindata.go: $(DATA_FILES)
-	go-bindata -o=$@ -ignore="\\.DS_Store|README.md|TRANSLATORS|auth.d" -pkg=bindata conf/...
+internal/assets/conf/conf_gen.go: $(CONF_FILES)
+	-rm -f $@
+	go generate internal/assets/conf/conf.go
+	gofmt -s -w $@
+
+internal/assets/templates/templates_gen.go: $(TEMPLATES_FILES)
+	-rm -f $@
+	go generate internal/assets/templates/templates.go
+	gofmt -s -w $@
+
+internal/assets/public/public_gen.go: $(PUBLIC_FILES)
+	-rm -f $@
+	go generate internal/assets/public/public.go
+	gofmt -s -w $@
 
 less: public/css/gogs.css
 

+ 1 - 1
conf/README.md

@@ -1,7 +1,7 @@
 After change anything (other than this file) in this directory, a re-run of the following command in the root directory of this repository is required:
 
 ```
-$ make bindata
+$ make generate
 ```
 
 To install the `go-bindata`, please see https://github.com/kevinburke/go-bindata#installation.

+ 3 - 2
conf/app.ini

@@ -61,8 +61,9 @@ KEY_FILE = custom/https/key.pem
 ; Allowed TLS version values: SSL30, TLS10, TLS11, TLS12
 TLS_MIN_VERSION = TLS10
 
-; Upper level of template and static file path
-; default is the path where Gogs is executed
+; Enable to load assets (i.e. "conf", "templates", "public") from disk instead of embedded bindata.
+LOAD_ASSETS_FROM_DISK = false
+; The directory that contains "templates" and "public". By default, it is the working directory.
 STATIC_ROOT_PATH =
 ; Default path for App data
 APP_DATA_PATH = data

+ 0 - 2
go.sum

@@ -80,8 +80,6 @@ github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 h1:yXtpJr/LV6PFu4nTLgfjQ
 github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14/go.mod h1:jPoNZLWDAqA5N3G5amEoiNbhVrmM+ZQEcnQvNQ2KaZk=
 github.com/gogs/git-module v0.8.3 h1:9f8oxSs9OACWrGBYMVnnQNzyTcVN+zzcBM7CXnbmezw=
 github.com/gogs/git-module v0.8.3/go.mod h1:aj4tcm7DxaszJWpZLZIRL6gfPXyguAHiE1PDfAAPrCw=
-github.com/gogs/go-gogs-client v0.0.0-20190710002546-4c3c18947c15 h1:tgEyCCe4+o8A2K/PEi9lF0QMA6XK+Y/j/WN01LnNbbo=
-github.com/gogs/go-gogs-client v0.0.0-20190710002546-4c3c18947c15/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
 github.com/gogs/go-gogs-client v0.0.0-20200128182646-c69cb7680fd4 h1:C7NryI/RQhsIWwC2bHN601P1wJKeuQ6U/UCOYTn3Cic=
 github.com/gogs/go-gogs-client v0.0.0-20200128182646-c69cb7680fd4/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
 github.com/gogs/go-libravatar v0.0.0-20191106065024-33a75213d0a0 h1:K02vod+sn3M1OOkdqi2tPxN2+xESK4qyITVQ3JkGEv4=

+ 13 - 0
internal/assets/assets.go

@@ -0,0 +1,13 @@
+package assets
+
+import (
+	"strings"
+)
+
+// IsErrNotFound returns true if the error is asset not found.
+func IsErrNotFound(err error) bool {
+	if err == nil {
+		return false
+	}
+	return strings.Contains(err.Error(), "not found")
+}

+ 7 - 0
internal/assets/conf/conf.go

@@ -0,0 +1,7 @@
+// Copyright 2020 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 conf
+
+//go:generate go-bindata -nomemcopy -pkg=conf -ignore="\\.DS_Store|README.md|TRANSLATORS|auth.d" -prefix=../../../ -debug=false -o=conf_gen.go ../../../conf/...

Різницю між файлами не показано, бо вона завелика
+ 311 - 0
internal/assets/conf/conf_gen.go


+ 143 - 0
internal/assets/public/public.go

@@ -0,0 +1,143 @@
+// Copyright 2020 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 public
+
+import (
+	"bytes"
+	"net/http"
+	"os"
+	"path/filepath"
+	"time"
+
+	"gogs.io/gogs/internal/assets"
+)
+
+//go:generate go-bindata -nomemcopy -pkg=public -ignore="\\.DS_Store|less" -prefix=../../../public -debug=false -o=public_gen.go ../../../public/...
+
+/*
+	This file is a modified version of https://github.com/go-bindata/go-bindata/pull/18.
+*/
+
+type fileInfo struct {
+	name string
+	size int64
+}
+
+func (d fileInfo) Name() string {
+	return d.name
+}
+
+func (d fileInfo) Size() int64 {
+	return d.size
+}
+
+func (d fileInfo) Mode() os.FileMode {
+	return os.FileMode(0644) | os.ModeDir
+}
+
+func (d fileInfo) ModTime() time.Time {
+	return time.Time{}
+}
+
+// IsDir return file whether a directory
+func (d *fileInfo) IsDir() bool {
+	return true
+}
+
+func (d fileInfo) Sys() interface{} {
+	return nil
+}
+
+// file implements the http.File interface.
+type file struct {
+	name string
+	*bytes.Reader
+
+	children       []os.FileInfo
+	childrenOffset int
+}
+
+func (f *file) Close() error {
+	return nil
+}
+
+// ⚠️ WARNING: This method is not concurrent-safe.
+func (f *file) Readdir(count int) ([]os.FileInfo, error) {
+	if len(f.children) == 0 {
+		return nil, os.ErrNotExist
+	}
+
+	if count <= 0 {
+		return f.children, nil
+	}
+
+	if f.childrenOffset+count > len(f.children) {
+		count = len(f.children) - f.childrenOffset
+	}
+	offset := f.childrenOffset
+	f.childrenOffset += count
+	return f.children[offset : offset+count], nil
+}
+
+func (f *file) Stat() (os.FileInfo, error) {
+	childCount := len(f.children)
+	if childCount != 0 {
+		return &fileInfo{
+			name: f.name,
+			size: int64(childCount),
+		}, nil
+	}
+	return AssetInfo(f.name)
+}
+
+// fileSystem implements the http.FileSystem interface.
+type fileSystem struct{}
+
+func (f *fileSystem) Open(name string) (http.File, error) {
+	if len(name) > 0 && name[0] == '/' {
+		name = name[1:]
+	}
+
+	// Attempt to get it as a file
+	p, err := Asset(name)
+	if err != nil && !assets.IsErrNotFound(err) {
+		return nil, err
+	} else if err == nil {
+		return &file{
+			name:   name,
+			Reader: bytes.NewReader(p),
+		}, nil
+	}
+
+	// Attempt to get it as a directory
+	paths, err := AssetDir(name)
+	if err != nil && !assets.IsErrNotFound(err) {
+		return nil, err
+	}
+
+	infos := make([]os.FileInfo, len(paths))
+	for i, path := range paths {
+		path = filepath.Join(name, path)
+		info, err := AssetInfo(path)
+		if err != nil {
+			if !assets.IsErrNotFound(err) {
+				return nil, err
+			}
+			// Not found as a file, assume it's a directory.
+			infos[i] = &fileInfo{name: path}
+		} else {
+			infos[i] = info
+		}
+	}
+	return &file{
+		name:     name,
+		children: infos,
+	}, nil
+}
+
+// NewFileSystem returns an http.FileSystem instance backed by embedded assets.
+func NewFileSystem() http.FileSystem {
+	return &fileSystem{}
+}

Різницю між файлами не показано, бо вона завелика
+ 1410 - 0
internal/assets/public/public_gen.go


+ 73 - 0
internal/assets/templates/templates.go

@@ -0,0 +1,73 @@
+// Copyright 2020 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"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"path"
+	"strings"
+
+	"gopkg.in/macaron.v1"
+
+	"gogs.io/gogs/internal/osutil"
+)
+
+//go:generate go-bindata -nomemcopy -ignore="\\.DS_Store" -pkg=templates -prefix=../../../templates -debug=false -o=templates_gen.go ../../../templates/...
+
+// fileSystem implements the macaron.TemplateFileSystem interface.
+type fileSystem struct {
+	files []macaron.TemplateFile
+}
+
+func (fs *fileSystem) ListFiles() []macaron.TemplateFile {
+	return fs.files
+}
+
+func (fs *fileSystem) Get(name string) (io.Reader, error) {
+	for i := range fs.files {
+		if fs.files[i].Name()+fs.files[i].Ext() == name {
+			return bytes.NewReader(fs.files[i].Data()), nil
+		}
+	}
+	return nil, fmt.Errorf("file %q not found", name)
+}
+
+// NewTemplateFileSystem returns a macaron.TemplateFileSystem instance for embedded assets.
+// The argument "dir" can be used to serve subset of embedded assets. Template file
+// found under the "customDir" on disk has higher precedence over embedded assets.
+func NewTemplateFileSystem(dir, customDir string) macaron.TemplateFileSystem {
+	if dir != "" && !strings.HasSuffix(dir, "/") {
+		dir += "/"
+	}
+
+	var files []macaron.TemplateFile
+	names := AssetNames()
+	for _, name := range names {
+		if !strings.HasPrefix(name, dir) {
+			continue
+		}
+
+		// Check if corresponding custom file exists
+		var err error
+		var data []byte
+		fpath := path.Join(customDir, name)
+		if osutil.IsFile(fpath) {
+			data, err = ioutil.ReadFile(fpath)
+		} else {
+			data, err = Asset(name)
+		}
+		if err != nil {
+			panic(err)
+		}
+
+		ext := path.Ext(name)
+		name = strings.TrimSuffix(name, ext)
+		files = append(files, macaron.NewTplFile(name, data, ext))
+	}
+	return &fileSystem{files: files}
+}

Різницю між файлами не показано, бо вона завелика
+ 219 - 0
internal/assets/templates/templates_gen.go


Різницю між файлами не показано, бо вона завелика
+ 0 - 305
internal/bindata/bindata.go


+ 0 - 3
internal/cmd/hook.go

@@ -11,7 +11,6 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
-	"path"
 	"path/filepath"
 	"strings"
 
@@ -201,8 +200,6 @@ func runHookPostReceive(c *cli.Context) error {
 	// so we need to setup additional services for email notifications.
 	setting.NewPostReceiveHookServices()
 	mailer.NewContext()
-	mailer.InitMailRender(path.Join(setting.StaticRootPath, "templates/mail"),
-		path.Join(setting.CustomPath, "templates/mail"), template.NewFuncMap())
 
 	isWiki := strings.Contains(os.Getenv(db.ENV_REPO_CUSTOM_HOOKS_PATH), ".wiki.git/")
 

+ 27 - 31
internal/cmd/web.go

@@ -8,7 +8,6 @@ import (
 	"crypto/tls"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/http"
 	"net/http/fcgi"
@@ -24,18 +23,18 @@ import (
 	"github.com/go-macaron/i18n"
 	"github.com/go-macaron/session"
 	"github.com/go-macaron/toolbox"
-	"github.com/mcuadros/go-version"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"github.com/unknwon/com"
 	"github.com/urfave/cli"
 	log "gopkg.in/clog.v1"
 	"gopkg.in/macaron.v1"
 
-	"gogs.io/gogs/internal/bindata"
+	"gogs.io/gogs/internal/assets/conf"
+	"gogs.io/gogs/internal/assets/public"
+	"gogs.io/gogs/internal/assets/templates"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/form"
-	"gogs.io/gogs/internal/mailer"
 	"gogs.io/gogs/internal/route"
 	"gogs.io/gogs/internal/route/admin"
 	apiv1 "gogs.io/gogs/internal/route/api/v1"
@@ -59,23 +58,6 @@ and it takes care of all the other things for you`,
 	},
 }
 
-// checkVersion checks if binary matches the version of templates files.
-func checkVersion() {
-	// Templates.
-	data, err := ioutil.ReadFile(setting.StaticRootPath + "/templates/.VERSION")
-	if err != nil {
-		log.Fatal(2, "Fail to read 'templates/.VERSION': %v", err)
-	}
-	tplVer := strings.TrimSpace(string(data))
-	if tplVer != setting.AppVer {
-		if version.Compare(tplVer, setting.AppVer, ">") {
-			log.Fatal(2, "Binary version is lower than template file version, did you forget to recompile Gogs?")
-		} else {
-			log.Fatal(2, "Binary version is higher than template file version, did you forget to update template files?")
-		}
-	}
-}
-
 // newMacaron initializes Macaron instance.
 func newMacaron() *macaron.Macaron {
 	m := macaron.New()
@@ -89,12 +71,26 @@ func newMacaron() *macaron.Macaron {
 	if setting.Protocol == setting.SCHEME_FCGI {
 		m.SetURLPrefix(setting.AppSubURL)
 	}
+
+	// Register custom middleware first to make it possible to override files under "public".
+	m.Use(macaron.Static(
+		path.Join(setting.CustomPath, "public"),
+		macaron.StaticOptions{
+			SkipLogging: setting.DisableRouterLog,
+		},
+	))
+	var publicFs http.FileSystem
+	if !setting.LoadAssetsFromDisk {
+		publicFs = public.NewFileSystem()
+	}
 	m.Use(macaron.Static(
 		path.Join(setting.StaticRootPath, "public"),
 		macaron.StaticOptions{
 			SkipLogging: setting.DisableRouterLog,
+			FileSystem:  publicFs,
 		},
 	))
+
 	m.Use(macaron.Static(
 		setting.AvatarUploadPath,
 		macaron.StaticOptions{
@@ -110,23 +106,24 @@ func newMacaron() *macaron.Macaron {
 		},
 	))
 
-	funcMap := template.NewFuncMap()
-	m.Use(macaron.Renderer(macaron.RenderOptions{
+	renderOpt := macaron.RenderOptions{
 		Directory:         path.Join(setting.StaticRootPath, "templates"),
 		AppendDirectories: []string{path.Join(setting.CustomPath, "templates")},
-		Funcs:             funcMap,
+		Funcs:             template.FuncMap(),
 		IndentJSON:        macaron.Env != macaron.PROD,
-	}))
-	mailer.InitMailRender(path.Join(setting.StaticRootPath, "templates/mail"),
-		path.Join(setting.CustomPath, "templates/mail"), funcMap)
+	}
+	if !setting.LoadAssetsFromDisk {
+		renderOpt.TemplateFileSystem = templates.NewTemplateFileSystem("", renderOpt.AppendDirectories[0])
+	}
+	m.Use(macaron.Renderer(renderOpt))
 
-	localeNames, err := bindata.AssetDir("conf/locale")
+	localeNames, err := conf.AssetDir("conf/locale")
 	if err != nil {
 		log.Fatal(4, "Fail to list locale files: %v", err)
 	}
 	localFiles := make(map[string][]byte)
 	for _, name := range localeNames {
-		localFiles[name] = bindata.MustAsset("conf/locale/" + name)
+		localFiles[name] = conf.MustAsset("conf/locale/" + name)
 	}
 	m.Use(i18n.I18n(i18n.Options{
 		SubURL:          setting.AppSubURL,
@@ -170,7 +167,6 @@ func runWeb(c *cli.Context) error {
 		setting.CustomConf = c.String("config")
 	}
 	route.GlobalInit()
-	checkVersion()
 
 	m := newMacaron()
 
@@ -697,7 +693,7 @@ func runWeb(c *cli.Context) error {
 	} else {
 		listenAddr = fmt.Sprintf("%s:%s", setting.HTTPAddr, setting.HTTPPort)
 	}
-	log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL)
+	log.Info("Listen on %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL)
 
 	var err error
 	switch setting.Protocol {

+ 3 - 2
internal/db/migrations/v15.go

@@ -12,9 +12,10 @@ import (
 	"strings"
 
 	"github.com/unknwon/com"
-	"xorm.io/xorm"
 	log "gopkg.in/clog.v1"
+	"xorm.io/xorm"
 
+	"gogs.io/gogs/internal/osutil"
 	"gogs.io/gogs/internal/setting"
 )
 
@@ -80,7 +81,7 @@ func generateAndMigrateGitHooks(x *xorm.Engine) (err error) {
 				// Gogs didn't allow user to set custom update hook thus no migration for it.
 				// In case user runs this migration multiple times, and custom hook exists,
 				// we assume it's been migrated already.
-				if hookName != "update" && com.IsFile(oldHookPath) && !com.IsExist(customHookDir) {
+				if hookName != "update" && osutil.IsFile(oldHookPath) && !com.IsExist(customHookDir) {
 					os.MkdirAll(customHookDir, os.ModePerm)
 					if err = os.Rename(oldHookPath, newHookPath); err != nil {
 						return fmt.Errorf("move hook file to custom directory '%s' -> '%s': %v", oldHookPath, newHookPath, err)

+ 2 - 1
internal/db/pull.go

@@ -19,6 +19,7 @@ import (
 	api "github.com/gogs/go-gogs-client"
 
 	"gogs.io/gogs/internal/db/errors"
+	"gogs.io/gogs/internal/osutil"
 	"gogs.io/gogs/internal/process"
 	"gogs.io/gogs/internal/setting"
 	"gogs.io/gogs/internal/sync"
@@ -406,7 +407,7 @@ func (pr *PullRequest) testPatch() (err error) {
 	}
 
 	// Fast fail if patch does not exist, this assumes data is cruppted.
-	if !com.IsFile(patchPath) {
+	if !osutil.IsFile(patchPath) {
 		log.Trace("PullRequest[%d].testPatch: ignored cruppted data", pr.ID)
 		return nil
 	}

+ 5 - 4
internal/db/repo.go

@@ -30,10 +30,11 @@ import (
 	"github.com/gogs/git-module"
 	api "github.com/gogs/go-gogs-client"
 
+	"gogs.io/gogs/internal/assets/conf"
 	"gogs.io/gogs/internal/avatar"
-	"gogs.io/gogs/internal/bindata"
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/markup"
+	"gogs.io/gogs/internal/osutil"
 	"gogs.io/gogs/internal/process"
 	"gogs.io/gogs/internal/setting"
 	"gogs.io/gogs/internal/sync"
@@ -56,7 +57,7 @@ func LoadRepoConfig() {
 	types := []string{"gitignore", "license", "readme", "label"}
 	typeFiles := make([][]string, 4)
 	for i, t := range types {
-		files, err := bindata.AssetDir("conf/" + t)
+		files, err := conf.AssetDir("conf/" + t)
 		if err != nil {
 			log.Fatal(4, "Fail to get %s files: %v", t, err)
 		}
@@ -929,10 +930,10 @@ func getRepoInitFile(tp, name string) ([]byte, error) {
 
 	// Use custom file when available.
 	customPath := path.Join(setting.CustomPath, relPath)
-	if com.IsFile(customPath) {
+	if osutil.IsFile(customPath) {
 		return ioutil.ReadFile(customPath)
 	}
-	return bindata.Asset(relPath)
+	return conf.Asset(relPath)
 }
 
 func prepareRepoCommit(repo *Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {

+ 4 - 3
internal/db/repo_editor.go

@@ -22,6 +22,7 @@ import (
 	"github.com/gogs/git-module"
 
 	"gogs.io/gogs/internal/db/errors"
+	"gogs.io/gogs/internal/osutil"
 	"gogs.io/gogs/internal/process"
 	"gogs.io/gogs/internal/setting"
 	"gogs.io/gogs/internal/tool"
@@ -165,7 +166,7 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
 
 	// Ignore move step if it's a new file under a directory.
 	// Otherwise, move the file when name changed.
-	if com.IsFile(oldFilePath) && opts.OldTreeName != opts.NewTreeName {
+	if osutil.IsFile(oldFilePath) && opts.OldTreeName != opts.NewTreeName {
 		if err = git.MoveFile(localPath, opts.OldTreeName, opts.NewTreeName); err != nil {
 			return fmt.Errorf("git mv %q %q: %v", opts.OldTreeName, opts.NewTreeName, err)
 		}
@@ -402,7 +403,7 @@ func DeleteUploads(uploads ...*Upload) (err error) {
 
 	for _, upload := range uploads {
 		localPath := upload.LocalPath()
-		if !com.IsFile(localPath) {
+		if !osutil.IsFile(localPath) {
 			continue
 		}
 
@@ -480,7 +481,7 @@ func (repo *Repository) UploadRepoFiles(doer *User, opts UploadRepoFileOptions)
 	// Copy uploaded files into repository
 	for _, upload := range uploads {
 		tmpPath := upload.LocalPath()
-		if !com.IsFile(tmpPath) {
+		if !osutil.IsFile(tmpPath) {
 			continue
 		}
 

+ 49 - 26
internal/mailer/mail.go

@@ -7,11 +7,15 @@ package mailer
 import (
 	"fmt"
 	"html/template"
+	"path"
+	"sync"
+	"time"
 
 	log "gopkg.in/clog.v1"
 	"gopkg.in/gomail.v2"
 	"gopkg.in/macaron.v1"
 
+	"gogs.io/gogs/internal/assets/templates"
 	"gogs.io/gogs/internal/markup"
 	"gogs.io/gogs/internal/setting"
 )
@@ -28,30 +32,49 @@ const (
 	MAIL_NOTIFY_COLLABORATOR = "notify/collaborator"
 )
 
-type MailRender interface {
-	HTMLString(string, interface{}, ...macaron.HTMLOptions) (string, error)
-}
-
-var mailRender MailRender
-
-func InitMailRender(dir, appendDir string, funcMap []template.FuncMap) {
-	opt := &macaron.RenderOptions{
-		Directory:         dir,
-		AppendDirectories: []string{appendDir},
-		Funcs:             funcMap,
-		Extensions:        []string{".tmpl", ".html"},
-	}
-	ts := macaron.NewTemplateSet()
-	ts.Set(macaron.DEFAULT_TPL_SET_NAME, opt)
+var (
+	tplRender     *macaron.TplRender
+	tplRenderOnce sync.Once
+)
 
-	mailRender = &macaron.TplRender{
-		TemplateSet: ts,
-		Opt:         opt,
-	}
+// render renders a mail template with given data.
+func render(tpl string, data map[string]interface{}) (string, error) {
+	tplRenderOnce.Do(func() {
+		opt := &macaron.RenderOptions{
+			Directory:         path.Join(setting.StaticRootPath, "templates/mail"),
+			AppendDirectories: []string{path.Join(setting.CustomPath, "templates/mail")},
+			Funcs: []template.FuncMap{map[string]interface{}{
+				"AppName": func() string {
+					return setting.AppName
+				},
+				"AppURL": func() string {
+					return setting.AppURL
+				},
+				"Year": func() int {
+					return time.Now().Year()
+				},
+				"Str2HTML": func(raw string) template.HTML {
+					return template.HTML(markup.Sanitize(raw))
+				},
+			}},
+		}
+		if !setting.LoadAssetsFromDisk {
+			opt.TemplateFileSystem = templates.NewTemplateFileSystem("mail", opt.AppendDirectories[0])
+		}
+
+		ts := macaron.NewTemplateSet()
+		ts.Set(macaron.DEFAULT_TPL_SET_NAME, opt)
+		tplRender = &macaron.TplRender{
+			TemplateSet: ts,
+			Opt:         opt,
+		}
+	})
+
+	return tplRender.HTMLString(tpl, data)
 }
 
 func SendTestMail(email string) error {
-	return gomail.Send(&Sender{}, NewMessage([]string{email}, "Gogs Test Email!", "Gogs Test Email!").Message)
+	return gomail.Send(&Sender{}, NewMessage([]string{email}, "Gogs Test Email", "Hello 👋, greeting from Gogs!").Message)
 }
 
 /*
@@ -85,9 +108,9 @@ func SendUserMail(c *macaron.Context, u User, tpl, code, subject, info string) {
 		"ResetPwdCodeLives": setting.Service.ResetPwdCodeLives / 60,
 		"Code":              code,
 	}
-	body, err := mailRender.HTMLString(string(tpl), data)
+	body, err := render(tpl, data)
 	if err != nil {
-		log.Error(2, "HTMLString: %v", err)
+		log.Error(2, "render: %v", err)
 		return
 	}
 
@@ -113,7 +136,7 @@ func SendActivateEmailMail(c *macaron.Context, u User, email string) {
 		"Code":            u.GenerateEmailActivateCode(email),
 		"Email":           email,
 	}
-	body, err := mailRender.HTMLString(string(MAIL_AUTH_ACTIVATE_EMAIL), data)
+	body, err := render(MAIL_AUTH_ACTIVATE_EMAIL, data)
 	if err != nil {
 		log.Error(3, "HTMLString: %v", err)
 		return
@@ -130,7 +153,7 @@ func SendRegisterNotifyMail(c *macaron.Context, u User) {
 	data := map[string]interface{}{
 		"Username": u.DisplayName(),
 	}
-	body, err := mailRender.HTMLString(string(MAIL_AUTH_REGISTER_NOTIFY), data)
+	body, err := render(MAIL_AUTH_REGISTER_NOTIFY, data)
 	if err != nil {
 		log.Error(3, "HTMLString: %v", err)
 		return
@@ -151,7 +174,7 @@ func SendCollaboratorMail(u, doer User, repo Repository) {
 		"RepoName": repo.FullName(),
 		"Link":     repo.HTMLURL(),
 	}
-	body, err := mailRender.HTMLString(string(MAIL_NOTIFY_COLLABORATOR), data)
+	body, err := render(MAIL_NOTIFY_COLLABORATOR, data)
 	if err != nil {
 		log.Error(3, "HTMLString: %v", err)
 		return
@@ -176,7 +199,7 @@ func composeIssueMessage(issue Issue, repo Repository, doer User, tplName string
 	body := string(markup.Markdown([]byte(issue.Content()), repo.HTMLURL(), repo.ComposeMetas()))
 	data := composeTplData(subject, body, issue.HTMLURL())
 	data["Doer"] = doer
-	content, err := mailRender.HTMLString(tplName, data)
+	content, err := render(tplName, data)
 	if err != nil {
 		log.Error(3, "HTMLString (%s): %v", tplName, err)
 	}

+ 18 - 0
internal/osutil/osutil.go

@@ -0,0 +1,18 @@
+// Copyright 2020 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 osutil
+
+import (
+	"os"
+)
+
+// IsFile returns true if given path exists as a file (i.e. not a directory).
+func IsFile(path string) bool {
+	f, e := os.Stat(path)
+	if e != nil {
+		return false
+	}
+	return !f.IsDir()
+}

+ 6 - 3
internal/route/install.go

@@ -43,7 +43,7 @@ func checkRunMode() {
 	} else {
 		git.Debug = true
 	}
-	log.Info("Run Mode: %s", strings.Title(macaron.Env))
+	log.Info("Run mode: %s", strings.Title(macaron.Env))
 }
 
 func NewServices() {
@@ -78,10 +78,13 @@ func GlobalInit() {
 		db.InitTestPullRequests()
 	}
 	if db.EnableSQLite3 {
-		log.Info("SQLite3 Supported")
+		log.Info("SQLite3 is supported")
 	}
 	if setting.SupportMiniWinService {
-		log.Info("Builtin Windows Service Supported")
+		log.Info("Builtin Windows Service is supported")
+	}
+	if setting.LoadAssetsFromDisk {
+		log.Info("Assets are loaded from disk")
 	}
 	checkRunMode()
 

+ 12 - 10
internal/setting/setting.go

@@ -16,18 +16,18 @@ import (
 	"strings"
 	"time"
 
-	"github.com/unknwon/com"
 	_ "github.com/go-macaron/cache/memcache"
 	_ "github.com/go-macaron/cache/redis"
 	"github.com/go-macaron/session"
 	_ "github.com/go-macaron/session/redis"
 	"github.com/mcuadros/go-version"
+	"github.com/unknwon/com"
 	log "gopkg.in/clog.v1"
 	"gopkg.in/ini.v1"
 
 	"github.com/gogs/go-libravatar"
 
-	"gogs.io/gogs/internal/bindata"
+	"gogs.io/gogs/internal/assets/conf"
 	"gogs.io/gogs/internal/process"
 	"gogs.io/gogs/internal/user"
 )
@@ -50,8 +50,8 @@ const (
 
 var (
 	// Build information should only be set by -ldflags.
-	BuildTime    string
-	BuildGitHash string
+	BuildTime   string
+	BuildCommit string
 
 	// App settings
 	AppVer         string
@@ -74,6 +74,7 @@ var (
 	CertFile             string
 	KeyFile              string
 	TLSMinVersion        string
+	LoadAssetsFromDisk   bool
 	StaticRootPath       string
 	EnableGzip           bool
 	LandingPageURL       LandingPage
@@ -416,7 +417,7 @@ func NewContext() {
 
 	Cfg, err = ini.LoadSources(ini.LoadOptions{
 		IgnoreInlineComment: true,
-	}, bindata.MustAsset("conf/app.ini"))
+	}, conf.MustAsset("conf/app.ini"))
 	if err != nil {
 		log.Fatal(2, "Fail to parse 'conf/app.ini': %v", err)
 	}
@@ -489,6 +490,7 @@ func NewContext() {
 	LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(string(Protocol) + "://localhost:" + HTTPPort + "/")
 	OfflineMode = sec.Key("OFFLINE_MODE").MustBool()
 	DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool()
+	LoadAssetsFromDisk = sec.Key("LOAD_ASSETS_FROM_DISK").MustBool()
 	StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(workDir)
 	AppDataPath = sec.Key("APP_DATA_PATH").MustString("data")
 	EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
@@ -729,8 +731,8 @@ func newService() {
 
 func newLogService() {
 	if len(BuildTime) > 0 {
-		log.Trace("Build Time: %s", BuildTime)
-		log.Trace("Build Git Hash: %s", BuildGitHash)
+		log.Trace("Build time: %s", BuildTime)
+		log.Trace("Build commit: %s", BuildCommit)
 	}
 
 	// Because we always create a console logger as primary logger before all settings are loaded,
@@ -809,7 +811,7 @@ func newLogService() {
 		}
 
 		log.New(log.MODE(mode), LogConfigs[i])
-		log.Trace("Log Mode: %s (%s)", strings.Title(mode), strings.Title(name))
+		log.Trace("Log mode: %s (%s)", strings.Title(mode), strings.Title(name))
 	}
 
 	// Make sure everyone gets version info printed.
@@ -830,7 +832,7 @@ func newCacheService() {
 		log.Fatal(2, "Unknown cache adapter: %s", CacheAdapter)
 	}
 
-	log.Info("Cache Service Enabled")
+	log.Info("Cache service is enabled")
 }
 
 func newSessionService() {
@@ -844,7 +846,7 @@ func newSessionService() {
 	SessionConfig.Maxlifetime = Cfg.Section("session").Key("SESSION_LIFE_TIME").MustInt64(86400)
 	CSRFCookieName = Cfg.Section("session").Key("CSRF_COOKIE_NAME").MustString("_csrf")
 
-	log.Info("Session Service Enabled")
+	log.Info("Session service is enabled")
 }
 
 // Mailer represents mail service.

+ 105 - 96
internal/template/template.go

@@ -12,6 +12,7 @@ import (
 	"path/filepath"
 	"runtime"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/editorconfig/editorconfig-core-go/v2"
@@ -27,103 +28,111 @@ import (
 	"gogs.io/gogs/internal/tool"
 )
 
-// TODO: only initialize map once and save to a local variable to reduce copies.
-func NewFuncMap() []template.FuncMap {
-	return []template.FuncMap{map[string]interface{}{
-		"GoVer": func() string {
-			return strings.Title(runtime.Version())
-		},
-		"Year": func() int {
-			return time.Now().Year()
-		},
-		"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
-		},
-		"AppDomain": func() string {
-			return setting.Domain
-		},
-		"DisableGravatar": func() bool {
-			return setting.DisableGravatar
-		},
-		"ShowFooterTemplateLoadTime": func() bool {
-			return setting.ShowFooterTemplateLoadTime
-		},
-		"LoadTimes": func(startTime time.Time) string {
-			return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
-		},
-		"AvatarLink":       tool.AvatarLink,
-		"AppendAvatarSize": tool.AppendAvatarSize,
-		"Safe":             Safe,
-		"Sanitize":         bluemonday.UGCPolicy().Sanitize,
-		"Str2HTML":         Str2HTML,
-		"NewLine2br":       NewLine2br,
-		"TimeSince":        tool.TimeSince,
-		"RawTimeSince":     tool.RawTimeSince,
-		"FileSize":         tool.FileSize,
-		"Subtract":         tool.Subtract,
-		"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")
-		},
-		"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]
-		},
-		"Join":                  strings.Join,
-		"EllipsisString":        tool.EllipsisString,
-		"DiffTypeToStr":         DiffTypeToStr,
-		"DiffLineTypeToStr":     DiffLineTypeToStr,
-		"Sha1":                  Sha1,
-		"ShortSHA1":             tool.ShortSHA1,
-		"MD5":                   tool.MD5,
-		"ActionContent2Commits": ActionContent2Commits,
-		"EscapePound":           EscapePound,
-		"RenderCommitMessage":   RenderCommitMessage,
-		"ThemeColorMetaTag": func() string {
-			return setting.UI.ThemeColorMetaTag
-		},
-		"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, err := ec.GetDefinitionForFilename(filename)
-				if err == nil && def.TabWidth > 0 {
-					return fmt.Sprintf("tab-size-%d", def.TabWidth)
+var (
+	funcMap     []template.FuncMap
+	funcMapOnce sync.Once
+)
+
+// FuncMap returns a list of user-defined template functions.
+func FuncMap() []template.FuncMap {
+	funcMapOnce.Do(func() {
+		funcMap = []template.FuncMap{map[string]interface{}{
+			"GoVer": func() string {
+				return strings.Title(runtime.Version())
+			},
+			"Year": func() int {
+				return time.Now().Year()
+			},
+			"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
+			},
+			"AppDomain": func() string {
+				return setting.Domain
+			},
+			"DisableGravatar": func() bool {
+				return setting.DisableGravatar
+			},
+			"ShowFooterTemplateLoadTime": func() bool {
+				return setting.ShowFooterTemplateLoadTime
+			},
+			"LoadTimes": func(startTime time.Time) string {
+				return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
+			},
+			"AvatarLink":       tool.AvatarLink,
+			"AppendAvatarSize": tool.AppendAvatarSize,
+			"Safe":             Safe,
+			"Sanitize":         bluemonday.UGCPolicy().Sanitize,
+			"Str2HTML":         Str2HTML,
+			"NewLine2br":       NewLine2br,
+			"TimeSince":        tool.TimeSince,
+			"RawTimeSince":     tool.RawTimeSince,
+			"FileSize":         tool.FileSize,
+			"Subtract":         tool.Subtract,
+			"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")
+			},
+			"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]
+			},
+			"Join":                  strings.Join,
+			"EllipsisString":        tool.EllipsisString,
+			"DiffTypeToStr":         DiffTypeToStr,
+			"DiffLineTypeToStr":     DiffLineTypeToStr,
+			"Sha1":                  Sha1,
+			"ShortSHA1":             tool.ShortSHA1,
+			"MD5":                   tool.MD5,
+			"ActionContent2Commits": ActionContent2Commits,
+			"EscapePound":           EscapePound,
+			"RenderCommitMessage":   RenderCommitMessage,
+			"ThemeColorMetaTag": func() string {
+				return setting.UI.ThemeColorMetaTag
+			},
+			"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, err := ec.GetDefinitionForFilename(filename)
+					if err == nil && def.TabWidth > 0 {
+						return fmt.Sprintf("tab-size-%d", def.TabWidth)
+					}
 				}
-			}
-			return "tab-size-8"
-		},
-	}}
+				return "tab-size-8"
+			},
+		}}
+	})
+	return funcMap
 }
 
 func Safe(raw string) template.HTML {

+ 0 - 1
public/js/gogs.js

@@ -40,7 +40,6 @@ function initEditPreviewTab($form) {
             var $this = $(this);
             $.post($this.data('url'), {
                 "_csrf": csrf,
-                "mode": "gfm",
                 "context": $this.data('context'),
                 "text": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
             },

+ 0 - 1
templates/.VERSION

@@ -1 +0,0 @@
-0.11.97.1209

Деякі файли не було показано, через те що забагато файлів було змінено