Browse Source

introduce git-shell

tags/v0.9.99
Unknwon 8 years ago
parent
commit
c50a3503e6

+ 1
- 0
.gopmfile View File

github.com/go-xorm/core = commit:3e10003353 github.com/go-xorm/core = commit:3e10003353
github.com/go-xorm/xorm = commit:c643188 github.com/go-xorm/xorm = commit:c643188
github.com/gogits/chardet = commit:2404f77725 github.com/gogits/chardet = commit:2404f77725
github.com/gogits/git-shell =
github.com/gogits/go-gogs-client = commit:7c02c95 github.com/gogits/go-gogs-client = commit:7c02c95
github.com/issue9/identicon = commit:5a61672 github.com/issue9/identicon = commit:5a61672
github.com/klauspost/compress = commit:bbfa9dc github.com/klauspost/compress = commit:bbfa9dc

+ 2
- 1
cmd/web.go View File

m.Get("/?:page", repo.Wiki) m.Get("/?:page", repo.Wiki)


m.Group("", func() { m.Group("", func() {
m.Get("/_new", repo.NewWiki)
m.Combo("/_new").Get(repo.NewWiki).
Post(bindIgnErr(auth.NewWikiForm{}), repo.NewWikiPost)
m.Get("/:page/_edit", repo.EditWiki) m.Get("/:page/_edit", repo.EditWiki)
}, reqSignIn) }, reqSignIn)
}, middleware.RepoRef()) }, middleware.RepoRef())

+ 1
- 6
models/pull.go View File

return fmt.Errorf("GetOwner: %v", err) return fmt.Errorf("GetOwner: %v", err)
} }


headRepoPath, err := pr.HeadRepo.RepoPath()
if err != nil {
return fmt.Errorf("HeadRepo.RepoPath: %v", err)
}

headGitRepo, err := git.OpenRepository(headRepoPath)
headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
if err != nil { if err != nil {
return fmt.Errorf("OpenRepository: %v", err) return fmt.Errorf("OpenRepository: %v", err)
} }

+ 2
- 6
models/release.go View File

return fmt.Errorf("GetRepositoryByID: %v", err) return fmt.Errorf("GetRepositoryByID: %v", err)
} }


repoPath, err := repo.RepoPath()
if err != nil {
return fmt.Errorf("RepoPath: %v", err)
}

_, stderr, err := process.ExecDir(-1, repoPath, fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID),
_, stderr, err := process.ExecDir(-1, repo.RepoPath(),
fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID),
"git", "tag", "-d", rel.TagName) "git", "tag", "-d", rel.TagName)
if err != nil && !strings.Contains(stderr, "not found") { if err != nil && !strings.Contains(stderr, "not found") {
return fmt.Errorf("git tag -d: %v - %s", err, stderr) return fmt.Errorf("git tag -d: %v - %s", err, stderr)

+ 43
- 86
models/repo.go View File

"github.com/Unknwon/cae/zip" "github.com/Unknwon/cae/zip"
"github.com/Unknwon/com" "github.com/Unknwon/com"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
"github.com/mcuadros/go-version"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"


"github.com/gogits/git-shell"

"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/bindata" "github.com/gogits/gogs/modules/bindata"
"github.com/gogits/gogs/modules/git"
oldgit "github.com/gogits/gogs/modules/git"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/process" "github.com/gogits/gogs/modules/process"
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
} }


// Check Git version. // Check Git version.
ver, err := git.GetVersion()
gitVer, err := git.Version()
if err != nil { if err != nil {
log.Fatal(4, "Fail to get Git version: %v", err) log.Fatal(4, "Fail to get Git version: %v", err)
} }


reqVer, err := git.ParseVersion("1.7.1")
if err != nil {
log.Fatal(4, "Fail to parse required Git version: %v", err)
}
if ver.LessThan(reqVer) {
log.Info("Git Version: %s", gitVer)
if version.Compare("1.7.1", gitVer, ">") {
log.Fatal(4, "Gogs requires Git version greater or equal to 1.7.1") log.Fatal(4, "Gogs requires Git version greater or equal to 1.7.1")
} }
log.Info("Git Version: %s", ver.String())


// Git requires setting user.name and user.email in order to commit changes. // Git requires setting user.name and user.email in order to commit changes.
for configKey, defaultValue := range map[string]string{"user.name": "Gogs", "user.email": "gogs@fake.local"} { for configKey, defaultValue := range map[string]string{"user.name": "Gogs", "user.email": "gogs@fake.local"} {
return repo.getOwner(x) return repo.getOwner(x)
} }


func (repo *Repository) mustOwner(e Engine) *User {
if err := repo.getOwner(e); err != nil {
return &User{
Name: "error",
FullName: err.Error(),
}
}

return repo.Owner
}

// MustOwner always returns a valid *User object to avoid
// conceptually impossible error handling.
// It creates a fake object that contains error deftail
// when error occurs.
func (repo *Repository) MustOwner() *User {
return repo.mustOwner(x)
}

// GetAssignees returns all users that have write access of repository. // GetAssignees returns all users that have write access of repository.
func (repo *Repository) GetAssignees() (_ []*User, err error) { func (repo *Repository) GetAssignees() (_ []*User, err error) {
if err = repo.GetOwner(); err != nil { if err = repo.GetOwner(); err != nil {
return err return err
} }


func (repo *Repository) repoPath(e Engine) (string, error) {
if err := repo.getOwner(e); err != nil {
return "", err
}
return RepoPath(repo.Owner.Name, repo.Name), nil
func (repo *Repository) repoPath(e Engine) string {
return RepoPath(repo.mustOwner(e).Name, repo.Name)
} }


func (repo *Repository) RepoPath() (string, error) {
func (repo *Repository) RepoPath() string {
return repo.repoPath(x) return repo.repoPath(x)
} }


func (repo *Repository) WikiPath() (string, error) {
if err := repo.GetOwner(); err != nil {
return "", err
}

return WikiPath(repo.Owner.Name, repo.Name), nil
}

func (repo *Repository) RepoLink() (string, error) {
if err := repo.GetOwner(); err != nil {
return "", err
}
return setting.AppSubUrl + "/" + repo.Owner.Name + "/" + repo.Name, nil
func (repo *Repository) RepoLink() string {
return setting.AppSubUrl + "/" + repo.MustOwner().Name + "/" + repo.Name
} }


func (repo *Repository) HasAccess(u *User) bool { func (repo *Repository) HasAccess(u *User) bool {


// UpdateLocalCopy makes sure the local copy of repository is up-to-date. // UpdateLocalCopy makes sure the local copy of repository is up-to-date.
func (repo *Repository) UpdateLocalCopy() error { func (repo *Repository) UpdateLocalCopy() error {
repoPath, err := repo.RepoPath()
if err != nil {
return err
}

repoPath := repo.RepoPath()
localPath := repo.LocalCopyPath() localPath := repo.LocalCopyPath()
if !com.IsExist(localPath) { if !com.IsExist(localPath) {
_, stderr, err := process.Exec( _, stderr, err := process.Exec(


var ( var (
reservedNames = []string{"debug", "raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new"} reservedNames = []string{"debug", "raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new"}
reservedPatterns = []string{"*.git", "*.keys"}
reservedPatterns = []string{"*.git", "*.keys", "*.wiki"}
) )


// IsUsableName checks if name is reserved or pattern of name is not allowed. // IsUsableName checks if name is reserved or pattern of name is not allowed.
} }


// Check if repository has master branch, if so set it to default branch. // Check if repository has master branch, if so set it to default branch.
gitRepo, err := git.OpenRepository(repoPath)
gitRepo, err := oldgit.OpenRepository(repoPath)
if err != nil { if err != nil {
return repo, fmt.Errorf("open git repository: %v", err) return repo, fmt.Errorf("open git repository: %v", err)
} }
} }


// initRepoCommit temporarily changes with work directory. // initRepoCommit temporarily changes with work directory.
func initRepoCommit(tmpPath string, sig *git.Signature) (err error) {
func initRepoCommit(tmpPath string, sig *oldgit.Signature) (err error) {
var stderr string var stderr string
if _, stderr, err = process.ExecDir(-1, if _, stderr, err = process.ExecDir(-1,
tmpPath, fmt.Sprintf("initRepoCommit (git add): %s", tmpPath), tmpPath, fmt.Sprintf("initRepoCommit (git add): %s", tmpPath),
return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git") return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git")
} }


// WikiPath returns wiki data path by given user and repository name.
func WikiPath(userName, repoName string) string {
return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".wiki.git")
}

// TransferOwnership transfers all corresponding setting from old user to new one. // TransferOwnership transfers all corresponding setting from old user to new one.
func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { func TransferOwnership(u *User, newOwnerName string, repo *Repository) error {
newOwner, err := GetUserByName(newOwnerName) newOwner, err := GetUserByName(newOwnerName)
} }


// Remove repository files. // Remove repository files.
repoPath, err := repo.repoPath(sess)
if err != nil {
return fmt.Errorf("RepoPath: %v", err)
}
repoPath := repo.repoPath(sess)
if err = os.RemoveAll(repoPath); err != nil { if err = os.RemoveAll(repoPath); err != nil {
desc := fmt.Sprintf("delete repository files[%s]: %v", repoPath, err) desc := fmt.Sprintf("delete repository files[%s]: %v", repoPath, err)
log.Warn(desc) log.Warn(desc)
return x.Where("id > 0").Iterate(new(Repository), return x.Where("id > 0").Iterate(new(Repository),
func(idx int, bean interface{}) error { func(idx int, bean interface{}) error {
repo := bean.(*Repository) repo := bean.(*Repository)
repoPath, err := repo.RepoPath()
if err != nil {
if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepositoryArchives.RepoPath [%d]: %v", repo.ID, err)); err2 != nil {
log.Error(4, "CreateRepositoryNotice: %v", err2)
}
return nil
}
return os.RemoveAll(filepath.Join(repoPath, "archives"))
return os.RemoveAll(filepath.Join(repo.RepoPath(), "archives"))
}) })
} }


if err := x.Where("id > 0").Iterate(new(Repository), if err := x.Where("id > 0").Iterate(new(Repository),
func(idx int, bean interface{}) error { func(idx int, bean interface{}) error {
repo := bean.(*Repository) repo := bean.(*Repository)
repoPath, err := repo.RepoPath()
if err != nil {
if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepositoryArchives.RepoPath [%d]: %v", repo.ID, err)); err2 != nil {
log.Error(4, "CreateRepositoryNotice: %v", err2)
}
return nil
}

if !com.IsDir(repoPath) {
if !com.IsDir(repo.RepoPath()) {
repos = append(repos, repo) repos = append(repos, repo)
} }
return nil return nil
return x.Where("id > 0").Iterate(new(Repository), return x.Where("id > 0").Iterate(new(Repository),
func(idx int, bean interface{}) error { func(idx int, bean interface{}) error {
repo := bean.(*Repository) repo := bean.(*Repository)
repoPath, err := repo.RepoPath()
if err != nil {
if err2 := CreateRepositoryNotice(fmt.Sprintf("RewriteRepositoryUpdateHook[%d]: %v", repo.ID, err)); err2 != nil {
log.Error(4, "CreateRepositoryNotice: %v", err2)
}
return nil
}
return createUpdateHook(repoPath)
return createUpdateHook(repo.RepoPath())
}) })
} }


return nil return nil
} }


repoPath, err := m.Repo.RepoPath()
if err != nil {
return fmt.Errorf("Repo.RepoPath: %v", err)
}

repoPath := m.Repo.RepoPath()
if _, stderr, err := process.ExecDir(10*time.Minute, if _, stderr, err := process.ExecDir(10*time.Minute,
repoPath, fmt.Sprintf("MirrorUpdate: %s", repoPath), repoPath, fmt.Sprintf("MirrorUpdate: %s", repoPath),
"git", "remote", "update", "--prune"); err != nil { "git", "remote", "update", "--prune"); err != nil {
if err := x.Where("id>0").Iterate(new(Repository), if err := x.Where("id>0").Iterate(new(Repository),
func(idx int, bean interface{}) error { func(idx int, bean interface{}) error {
repo := bean.(*Repository) repo := bean.(*Repository)
repoPath, err := repo.RepoPath()
if err != nil {
return fmt.Errorf("RepoPath: %v", err)
}

_, _, err = process.ExecDir(-1, repoPath, "Repository health check", "git", args...)
repoPath := repo.RepoPath()
_, _, err := process.ExecDir(-1, repoPath, "Repository health check", "git", args...)
if err != nil { if err != nil {
desc := fmt.Sprintf("Fail to health check repository(%s)", repoPath) desc := fmt.Sprintf("Fail to health check repository(%s)", repoPath)
log.Warn(desc) log.Warn(desc)
return nil, err return nil, err
} }


oldRepoPath, err := oldRepo.RepoPath()
if err != nil {
return nil, fmt.Errorf("get old repository path: %v", err)
}

repoPath := RepoPath(u.Name, repo.Name) repoPath := RepoPath(u.Name, repo.Name)
_, stderr, err := process.ExecTimeout(10*time.Minute, _, stderr, err := process.ExecTimeout(10*time.Minute,
fmt.Sprintf("ForkRepository(git clone): %s/%s", u.Name, repo.Name), fmt.Sprintf("ForkRepository(git clone): %s/%s", u.Name, repo.Name),
"git", "clone", "--bare", oldRepoPath, repoPath)
"git", "clone", "--bare", oldRepo.RepoPath(), repoPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("git clone: %v", stderr) return nil, fmt.Errorf("git clone: %v", stderr)
} }

+ 56
- 0
models/wiki.go View File

// Copyright 2015 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 models

import (
"fmt"
"path/filepath"
"strings"

"github.com/Unknwon/com"

"github.com/gogits/git-shell"
)

// ToWikiPageName formats a string to corresponding wiki URL name.
func ToWikiPageName(name string) string {
return strings.Replace(name, " ", "-", -1)
}

// WikiPath returns wiki data path by given user and repository name.
func WikiPath(userName, repoName string) string {
return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".wiki.git")
}

func (repo *Repository) WikiPath() string {
return WikiPath(repo.MustOwner().Name, repo.Name)
}

// HasWiki returns true if repository has wiki.
func (repo *Repository) HasWiki() bool {
return com.IsDir(repo.WikiPath())
}

// InitWiki initializes a wiki for repository,
// it does nothing when repository already has wiki.
func (repo *Repository) InitWiki() error {
if repo.HasWiki() {
return nil
}

if err := git.InitRepository(repo.WikiPath(), true); err != nil {
return fmt.Errorf("InitRepository: %v", err)
}
return nil
}

// AddWikiPage adds new page to repository wiki.
func (repo *Repository) AddWikiPage(title, content, message string) (err error) {
if err = repo.InitWiki(); err != nil {
return fmt.Errorf("InitWiki: %v", err)
}

return nil
}

+ 18
- 0
modules/auth/repo_form.go View File

func (f *EditReleaseForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { func (f *EditReleaseForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale) return validate(errs, ctx.Data, f, ctx.Locale)
} }

// __ __.__ __ .__
// / \ / \__| | _|__|
// \ \/\/ / | |/ / |
// \ /| | <| |
// \__/\ / |__|__|_ \__|
// \/ \/

type NewWikiForm struct {
Title string `binding:"Required"`
Content string
Message string
}

// FIXME: use code generation to generate this method.
func (f *NewWikiForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}

+ 1
- 5
modules/middleware/repo.go View File

return return
} }
ctx.Repo.GitRepo = gitRepo ctx.Repo.GitRepo = gitRepo
ctx.Repo.RepoLink, err = repo.RepoLink()
if err != nil {
ctx.Handle(500, "RepoLink", err)
return
}
ctx.Repo.RepoLink = repo.RepoLink()
ctx.Data["RepoLink"] = ctx.Repo.RepoLink ctx.Data["RepoLink"] = ctx.Repo.RepoLink
ctx.Data["RepoRelPath"] = ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name ctx.Data["RepoRelPath"] = ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name



+ 2
- 1
modules/process/manager.go View File

func Kill(pid int64) error { func Kill(pid int64) error {
for i, proc := range Processes { for i, proc := range Processes {
if proc.Pid == pid { if proc.Pid == pid {
if proc.Cmd.Process != nil && proc.Cmd.ProcessState != nil && !proc.Cmd.ProcessState.Exited() {
if proc.Cmd != nil && proc.Cmd.Process != nil &&
proc.Cmd.ProcessState != nil && !proc.Cmd.ProcessState.Exited() {
if err := proc.Cmd.Process.Kill(); err != nil { if err := proc.Cmd.Process.Kill(); err != nil {
return fmt.Errorf("fail to kill process(%d/%s): %v", proc.Pid, proc.Description, err) return fmt.Errorf("fail to kill process(%d/%s): %v", proc.Pid, proc.Description, err)
} }

+ 3
- 0
routers/install.go View File

"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"


"github.com/gogits/git-shell"

"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
"github.com/gogits/gogs/models/cron" "github.com/gogits/gogs/models/cron"
"github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/auth"
macaron.Env = macaron.PROD macaron.Env = macaron.PROD
macaron.ColorLog = false macaron.ColorLog = false
setting.ProdMode = true setting.ProdMode = true
git.Debug = false
} }
log.Info("Run Mode: %s", strings.Title(macaron.Env)) log.Info("Run Mode: %s", strings.Title(macaron.Env))
} }

+ 1
- 1
routers/repo/http.go View File

func authRequired(ctx *middleware.Context) { func authRequired(ctx *middleware.Context) {
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"") ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
ctx.Data["ErrorMsg"] = "no basic auth and digit auth" ctx.Data["ErrorMsg"] = "no basic auth and digit auth"
ctx.HTML(401, base.TplName("status/401"))
ctx.Error(401)
} }


func HTTP(ctx *middleware.Context) { func HTTP(ctx *middleware.Context) {

+ 2
- 12
routers/repo/pull.go View File

} }


if pull.HeadRepo != nil { if pull.HeadRepo != nil {
headRepoPath, err := pull.HeadRepo.RepoPath()
if err != nil {
ctx.Handle(500, "HeadRepo.RepoPath", err)
return nil
}

headGitRepo, err = git.OpenRepository(headRepoPath)
headGitRepo, err = git.OpenRepository(pull.HeadRepo.RepoPath())
if err != nil { if err != nil {
ctx.Handle(500, "OpenRepository", err) ctx.Handle(500, "OpenRepository", err)
return nil return nil
) )


// Get diff information. // Get diff information.
ctx.Data["CommitRepoLink"], err = headRepo.RepoLink()
if err != nil {
ctx.Handle(500, "RepoLink", err)
return false
}
ctx.Data["CommitRepoLink"] = headRepo.RepoLink()


headCommitID, err := headGitRepo.GetCommitIdOfBranch(headBranch) headCommitID, err := headGitRepo.GetCommitIdOfBranch(headBranch)
if err != nil { if err != nil {

+ 21
- 6
routers/repo/wiki.go View File

package repo package repo


import ( import (
"github.com/Unknwon/com"

"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/auth"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/middleware"
) )
ctx.Data["Title"] = ctx.Tr("repo.wiki") ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.Data["PageIsWiki"] = true ctx.Data["PageIsWiki"] = true


wikiPath := models.WikiPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
if !com.IsDir(wikiPath) {
if !ctx.Repo.Repository.HasWiki() {
ctx.HTML(200, WIKI_START) ctx.HTML(200, WIKI_START)
return return
} }
ctx.Data["PageIsWiki"] = true ctx.Data["PageIsWiki"] = true
ctx.Data["RequireSimpleMDE"] = true ctx.Data["RequireSimpleMDE"] = true


wikiPath := models.WikiPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
if !com.IsDir(wikiPath) {
if !ctx.Repo.Repository.HasWiki() {
ctx.Data["title"] = "Home" ctx.Data["title"] = "Home"
} }


ctx.HTML(200, WIKI_NEW) ctx.HTML(200, WIKI_NEW)
} }


func NewWikiPost(ctx *middleware.Context, form auth.NewWikiForm) {
ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
ctx.Data["PageIsWiki"] = true
ctx.Data["RequireSimpleMDE"] = true

if ctx.HasError() {
ctx.HTML(200, WIKI_NEW)
return
}

if err := ctx.Repo.Repository.AddWikiPage(form.Title, form.Content, form.Message); err != nil {
ctx.Handle(500, "AddWikiPage", err)
return
}

ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + models.ToWikiPageName(form.Title))
}

func EditWiki(ctx *middleware.Context) { func EditWiki(ctx *middleware.Context) {
ctx.PlainText(200, []byte(ctx.Params(":page"))) ctx.PlainText(200, []byte(ctx.Params(":page")))
} }

+ 2
- 1
templates/repo/wiki/new.tmpl View File

{{.i18n.Tr "repo.wiki.new_page"}} {{.i18n.Tr "repo.wiki.new_page"}}
</div> </div>
<form class="ui form" action="{{.Link}}" method="post"> <form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field"> <div class="field">
<input name="title" value="{{.title}}" autofocus>
<input name="title" value="{{.title}}" autofocus required>
</div> </div>
<div class="field"> <div class="field">
<textarea id="edit-area" name="content" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "repo.wiki.welcome"}}</textarea> <textarea id="edit-area" name="content" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "repo.wiki.welcome"}}</textarea>

+ 0
- 5
templates/status/401.tmpl View File

{{template "base/head" .}}
<div class="ui container center">
401 Unauthorized
</div>
{{template "base/footer" .}}

+ 0
- 5
templates/status/403.tmpl View File

{{template "base/head" .}}
<div class="ui container center">
403 Forbidden
</div>
{{template "base/footer" .}}

Loading…
Cancel
Save