// 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 models import ( "fmt" "path" "regexp" "sort" "strconv" "strings" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "github.com/unknwon/com" "xorm.io/builder" "xorm.io/xorm" ) // Issue represents an issue or pull request of repository. type Issue struct { ID int64 `xorm:"pk autoincr"` RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` Repo *Repository `xorm:"-"` Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. PosterID int64 `xorm:"INDEX"` Poster *User `xorm:"-"` OriginalAuthor string OriginalAuthorID int64 `xorm:"index"` Title string `xorm:"name"` Content string `xorm:"TEXT"` RenderedContent string `xorm:"-"` Labels []*Label `xorm:"-"` MilestoneID int64 `xorm:"INDEX"` Milestone *Milestone `xorm:"-"` Priority int AssigneeID int64 `xorm:"-"` Assignee *User `xorm:"-"` IsClosed bool `xorm:"INDEX"` IsRead bool `xorm:"-"` IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. PullRequest *PullRequest `xorm:"-"` NumComments int Ref string DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` ClosedUnix timeutil.TimeStamp `xorm:"INDEX"` Attachments []*Attachment `xorm:"-"` Comments []*Comment `xorm:"-"` Reactions ReactionList `xorm:"-"` TotalTrackedTime int64 `xorm:"-"` Assignees []*User `xorm:"-"` // IsLocked limits commenting abilities to users on an issue // with write access IsLocked bool `xorm:"NOT NULL DEFAULT false"` } var ( issueTasksPat *regexp.Regexp issueTasksDonePat *regexp.Regexp ) const issueTasksRegexpStr = `(^\s*[-*]\s\[[\sx]\]\s.)|(\n\s*[-*]\s\[[\sx]\]\s.)` const issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[x]\]\s.)|(\n\s*[-*]\s\[[x]\]\s.)` const issueMaxDupIndexAttempts = 3 func init() { issueTasksPat = regexp.MustCompile(issueTasksRegexpStr) issueTasksDonePat = regexp.MustCompile(issueTasksDoneRegexpStr) } func (issue *Issue) loadTotalTimes(e Engine) (err error) { opts := FindTrackedTimesOptions{IssueID: issue.ID} issue.TotalTrackedTime, err = opts.ToSession(e).SumInt(&TrackedTime{}, "time") if err != nil { return err } return nil } // IsOverdue checks if the issue is overdue func (issue *Issue) IsOverdue() bool { return timeutil.TimeStampNow() >= issue.DeadlineUnix } // LoadRepo loads issue's repository func (issue *Issue) LoadRepo() error { return issue.loadRepo(x) } func (issue *Issue) loadRepo(e Engine) (err error) { if issue.Repo == nil { issue.Repo, err = getRepositoryByID(e, issue.RepoID) if err != nil { return fmt.Errorf("getRepositoryByID [%d]: %v", issue.RepoID, err) } } return nil } // IsTimetrackerEnabled returns true if the repo enables timetracking func (issue *Issue) IsTimetrackerEnabled() bool { return issue.isTimetrackerEnabled(x) } func (issue *Issue) isTimetrackerEnabled(e Engine) bool { if err := issue.loadRepo(e); err != nil { log.Error(fmt.Sprintf("loadRepo: %v", err)) return false } return issue.Repo.IsTimetrackerEnabled() } // GetPullRequest returns the issue pull request func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) { if !issue.IsPull { return nil, fmt.Errorf("Issue is not a pull request") } pr, err = getPullRequestByIssueID(x, issue.ID) if err != nil { return nil, err } pr.Issue = issue return } func (issue *Issue) loadLabels(e Engine) (err error) { if issue.Labels == nil { issue.Labels, err = getLabelsByIssueID(e, issue.ID) if err != nil { return fmt.Errorf("getLabelsByIssueID [%d]: %v", issue.ID, err) } } return nil } // LoadPoster loads poster func (issue *Issue) LoadPoster() error { return issue.loadPoster(x) } func (issue *Issue) loadPoster(e Engine) (err error) { if issue.Poster == nil { issue.Poster, err = getUserByID(e, issue.PosterID) if err != nil { issue.PosterID = -1 issue.Poster = NewGhostUser() if !IsErrUserNotExist(err) { return fmt.Errorf("getUserByID.(poster) [%d]: %v", issue.PosterID, err) } err = nil return } } return } func (issue *Issue) loadPullRequest(e Engine) (err error) { if issue.IsPull && issue.PullRequest == nil { issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID) if err != nil { if IsErrPullRequestNotExist(err) { return err } return fmt.Errorf("getPullRequestByIssueID [%d]: %v", issue.ID, err) } issue.PullRequest.Issue = issue } return nil } // LoadPullRequest loads pull request info func (issue *Issue) LoadPullRequest() error { return issue.loadPullRequest(x) } func (issue *Issue) loadComments(e Engine) (err error) { return issue.loadCommentsByType(e, CommentTypeUnknown) } // LoadDiscussComments loads discuss comments func (issue *Issue) LoadDiscussComments() error { return issue.loadCommentsByType(x, CommentTypeComment) } func (issue *Issue) loadCommentsByType(e Engine, tp CommentType) (err error) { if issue.Comments != nil { return nil } issue.Comments, err = findComments(e, FindCommentsOptions{ IssueID: issue.ID, Type: tp, }) return err } func (issue *Issue) loadReactions(e Engine) (err error) { if issue.Reactions != nil { return nil } reactions, err := findReactions(e, FindReactionsOptions{ IssueID: issue.ID, }) if err != nil { return err } // Load reaction user data if _, err := ReactionList(reactions).loadUsers(e); err != nil { return err } // Cache comments to map comments := make(map[int64]*Comment) for _, comment := range issue.Comments { comments[comment.ID] = comment } // Add reactions either to issue or comment for _, react := range reactions { if react.CommentID == 0 { issue.Reactions = append(issue.Reactions, react) } else if comment, ok := comments[react.CommentID]; ok { comment.Reactions = append(comment.Reactions, react) } } return nil } func (issue *Issue) loadAttributes(e Engine) (err error) { if err = issue.loadRepo(e); err != nil { return } if err = issue.loadPoster(e); err != nil { return } if err = issue.loadLabels(e); err != nil { return } if issue.Milestone == nil && issue.MilestoneID > 0 { issue.Milestone, err = getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID) if err != nil && !IsErrMilestoneNotExist(err) { return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v", issue.RepoID, issue.MilestoneID, err) } } if err = issue.loadAssignees(e); err != nil { return } if err = issue.loadPullRequest(e); err != nil && !IsErrPullRequestNotExist(err) { // It is possible pull request is not yet created. return err } if issue.Attachments == nil { issue.Attachments, err = getAttachmentsByIssueID(e, issue.ID) if err != nil { return fmt.Errorf("getAttachmentsByIssueID [%d]: %v", issue.ID, err) } } if err = issue.loadComments(e); err != nil { return err } if err = CommentList(issue.Comments).loadAttributes(e); err != nil { return err } if issue.isTimetrackerEnabled(e) { if err = issue.loadTotalTimes(e); err != nil { return err } } return issue.loadReactions(e) } // LoadAttributes loads the attribute of this issue. func (issue *Issue) LoadAttributes() error { return issue.loadAttributes(x) } // GetIsRead load the `IsRead` field of the issue func (issue *Issue) GetIsRead(userID int64) error { issueUser := &IssueUser{IssueID: issue.ID, UID: userID} if has, err := x.Get(issueUser); err != nil { return err } else if !has { issue.IsRead = false return nil } issue.IsRead = issueUser.IsRead return nil } // APIURL returns the absolute APIURL to this issue. func (issue *Issue) APIURL() string { return issue.Repo.APIURL() + "/" + path.Join("issues", fmt.Sprint(issue.Index)) } // HTMLURL returns the absolute URL to this issue. func (issue *Issue) HTMLURL() string { var path string if issue.IsPull { path = "pulls" } else { path = "issues" } return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index) } // DiffURL returns the absolute URL to this diff func (issue *Issue) DiffURL() string { if issue.IsPull { return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index) } return "" } // PatchURL returns the absolute URL to this patch func (issue *Issue) PatchURL() string { if issue.IsPull { return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index) } return "" } // State returns string representation of issue status. func (issue *Issue) State() api.StateType { if issue.IsClosed { return api.StateClosed } return api.StateOpen } // APIFormat assumes some fields assigned with values: // Required - Poster, Labels, // Optional - Milestone, Assignee, PullRequest func (issue *Issue) APIFormat() *api.Issue { return issue.apiFormat(x) } func (issue *Issue) apiFormat(e Engine) *api.Issue { issue.loadLabels(e) apiLabels := make([]*api.Label, len(issue.Labels)) for i := range issue.Labels { apiLabels[i] = issue.Labels[i].APIFormat() } issue.loadPoster(e) issue.loadRepo(e) apiIssue := &api.Issue{ ID: issue.ID, URL: issue.APIURL(), Index: issue.Index, Poster: issue.Poster.APIFormat(), Title: issue.Title, Body: issue.Content, Labels: apiLabels, State: issue.State(), Comments: issue.NumComments, Created: issue.CreatedUnix.AsTime(), Updated: issue.UpdatedUnix.AsTime(), } apiIssue.Repo = &api.RepositoryMeta{ ID: issue.Repo.ID, Name: issue.Repo.Name, FullName: issue.Repo.FullName(), } if issue.ClosedUnix != 0 { apiIssue.Closed = issue.ClosedUnix.AsTimePtr() } if issue.Milestone != nil { apiIssue.Milestone = issue.Milestone.APIFormat() } issue.loadAssignees(e) if len(issue.Assignees) > 0 { for _, assignee := range issue.Assignees { apiIssue.Assignees = append(apiIssue.Assignees, assignee.APIFormat()) } apiIssue.Assignee = issue.Assignees[0].APIFormat() // For compatibility, we're keeping the first assignee as `apiIssue.Assignee` } if issue.IsPull { issue.loadPullRequest(e) apiIssue.PullRequest = &api.PullRequestMeta{ HasMerged: issue.PullRequest.HasMerged, } if issue.PullRequest.HasMerged { apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr() } } if issue.DeadlineUnix != 0 { apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr() } return apiIssue } // HashTag returns unique hash tag for issue. func (issue *Issue) HashTag() string { return "issue-" + com.ToStr(issue.ID) } // IsPoster returns true if given user by ID is the poster. func (issue *Issue) IsPoster(uid int64) bool { return issue.PosterID == uid } func (issue *Issue) hasLabel(e Engine, labelID int64) bool { return hasIssueLabel(e, issue.ID, labelID) } // HasLabel returns true if issue has been labeled by given ID. func (issue *Issue) HasLabel(labelID int64) bool { return issue.hasLabel(x, labelID) } // ReplyReference returns tokenized address to use for email reply headers func (issue *Issue) ReplyReference() string { var path string if issue.IsPull { path = "pulls" } else { path = "issues" } return fmt.Sprintf("%s/%s/%d@%s", issue.Repo.FullName(), path, issue.Index, setting.Domain) } func (issue *Issue) addLabel(e *xorm.Session, label *Label, doer *User) error { return newIssueLabel(e, issue, label, doer) } func (issue *Issue) addLabels(e *xorm.Session, labels []*Label, doer *User) error { return newIssueLabels(e, issue, labels, doer) } func (issue *Issue) getLabels(e Engine) (err error) { if len(issue.Labels) > 0 { return nil } issue.Labels, err = getLabelsByIssueID(e, issue.ID) if err != nil { return fmt.Errorf("getLabelsByIssueID: %v", err) } return nil } func (issue *Issue) removeLabel(e *xorm.Session, doer *User, label *Label) error { return deleteIssueLabel(e, issue, label, doer) } func (issue *Issue) clearLabels(e *xorm.Session, doer *User) (err error) { if err = issue.getLabels(e); err != nil { return fmt.Errorf("getLabels: %v", err) } for i := range issue.Labels { if err = issue.removeLabel(e, doer, issue.Labels[i]); err != nil { return fmt.Errorf("removeLabel: %v", err) } } return nil } // ClearLabels removes all issue labels as the given user. // Triggers appropriate WebHooks, if any. func (issue *Issue) ClearLabels(doer *User) (err error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err } if err := issue.loadRepo(sess); err != nil { return err } else if err = issue.loadPullRequest(sess); err != nil { return err } perm, err := getUserRepoPermission(sess, issue.Repo, doer) if err != nil { return err } if !perm.CanWriteIssuesOrPulls(issue.IsPull) { return ErrLabelNotExist{} } if err = issue.clearLabels(sess, doer); err != nil { return err } if err = sess.Commit(); err != nil { return fmt.Errorf("Commit: %v", err) } return nil } type labelSorter []*Label func (ts labelSorter) Len() int { return len([]*Label(ts)) } func (ts labelSorter) Less(i, j int) bool { return []*Label(ts)[i].ID < []*Label(ts)[j].ID } func (ts labelSorter) Swap(i, j int) { []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i] } // ReplaceLabels removes all current labels and add new labels to the issue. // Triggers appropriate WebHooks, if any. func (issue *Issue) ReplaceLabels(labels []*Label, doer *User) (err error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err } if err = issue.loadLabels(sess); err != nil { return err } sort.Sort(labelSorter(labels)) sort.Sort(labelSorter(issue.Labels)) var toAdd, toRemove []*Label addIndex, removeIndex := 0, 0 for addIndex < len(labels) && removeIndex < len(issue.Labels) { addLabel := labels[addIndex] removeLabel := issue.Labels[removeIndex] if addLabel.ID == removeLabel.ID { addIndex++ removeIndex++ } else if addLabel.ID < removeLabel.ID { toAdd = append(toAdd, addLabel) addIndex++ } else { toRemove = append(toRemove, removeLabel) removeIndex++ } } toAdd = append(toAdd, labels[addIndex:]...) toRemove = append(toRemove, issue.Labels[removeIndex:]...) if len(toAdd) > 0 { if err = issue.addLabels(sess, toAdd, doer); err != nil { return fmt.Errorf("addLabels: %v", err) } } for _, l := range toRemove { if err = issue.removeLabel(sess, doer, l); err != nil { return fmt.Errorf("removeLabel: %v", err) } } return sess.Commit() } // ReadBy sets issue to be read by given user. func (issue *Issue) ReadBy(userID int64) error { if err := UpdateIssueUserByRead(userID, issue.ID); err != nil { return err } return setNotificationStatusReadIfUnread(x, userID, issue.ID) } func updateIssueCols(e Engine, issue *Issue, cols ...string) error { if _, err := e.ID(issue.ID).Cols(cols...).Update(issue); err != nil { return err } return nil } func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (err error) { // Reload the issue currentIssue, err := getIssueByID(e, issue.ID) if err != nil { return err } // Nothing should be performed if current status is same as target status if currentIssue.IsClosed == isClosed { return nil } // Check for open dependencies if isClosed && issue.Repo.isDependenciesEnabled(e) { // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies noDeps, err := issueNoDependenciesLeft(e, issue) if err != nil { return err } if !noDeps { return ErrDependenciesLeft{issue.ID} } } issue.IsClosed = isClosed if isClosed { issue.ClosedUnix = timeutil.TimeStampNow() } else { issue.ClosedUnix = 0 } if err = updateIssueCols(e, issue, "is_closed", "closed_unix"); err != nil { return err } // Update issue count of labels if err = issue.getLabels(e); err != nil { return err } for idx := range issue.Labels { if err = updateLabel(e, issue.Labels[idx]); err != nil { return err } } // Update issue count of milestone if err := update
// 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 routers

import (
	"errors"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"

	"github.com/Unknwon/com"
	"github.com/go-xorm/xorm"
	"gopkg.in/ini.v1"
	"gopkg.in/macaron.v1"

	"github.com/gogits/git-module"

	"github.com/gogits/gogs/models"
	"github.com/gogits/gogs/modules/auth"
	"github.com/gogits/gogs/modules/base"
	"github.com/gogits/gogs/modules/cron"
	"github.com/gogits/gogs/modules/log"
	"github.com/gogits/gogs/modules/mailer"
	"github.com/gogits/gogs/modules/markdown"
	"github.com/gogits/gogs/modules/middleware"
	"github.com/gogits/gogs/modules/setting"
	"github.com/gogits/gogs/modules/ssh"
	"github.com/gogits/gogs/modules/template/highlight"
	"github.com/gogits/gogs/modules/user"
)

const (
	INSTALL base.TplName = "install"
)

func checkRunMode() {
	switch setting.Cfg.Section("").Key("RUN_MODE").String() {
	case "prod":
		macaron.Env = macaron.PROD
		macaron.ColorLog = false
		setting.ProdMode = true
	default:
		git.Debug = true
	}
	log.Info("Run Mode: %s", strings.Title(macaron.Env))
}

func NewServices() {
	setting.NewServices()
	mailer.NewContext()
}

// GlobalInit is for global configuration reload-able.
func GlobalInit() {
	setting.NewContext()
	highlight.NewContext()
	log.Trace("Custom path: %s", setting.CustomPath)
	log.Trace("Log path: %s", setting.LogRootPath)
	models.LoadConfigs()
	NewServices()

	if setting.InstallLock {
		models.LoadRepoConfig()
		models.NewRepoContext()

		if err := models.NewEngine(); err != nil {
			log.Fatal(4, "Fail to initialize ORM engine: %v", err)
		}

		models.HasEngine = true
		cron.NewContext()
		models.InitDeliverHooks()
		models.InitTestPullRequests()
		log.NewGitLogger(path.Join(setting.LogRootPath, "http.log"))
	}
	if models.EnableSQLite3 {
		log.Info("SQLite3 Supported")
	}
	if models.EnableTidb {
		log.Info("TiDB Supported")
	}
	if setting.SupportMiniWinService {
		log.Info("Builtin Windows Service Supported")
	}
	checkRunMode()

	if setting.SSH.StartBuiltinServer {
		ssh.Listen(setting.SSH.ListenPort)
		log.Info("SSH server started on :%v", setting.SSH.ListenPort)
	}

	// Build Sanitizer
	markdown.BuildSanitizer()
}

func InstallInit(ctx *middleware.Context) {
	if setting.InstallLock {
		ctx.Handle(404, "Install", errors.New("Installation is prohibited"))
		return
	}

	ctx.Data["Title"] = ctx.Tr("install.install")
	ctx.Data["PageIsInstall"] = true

	dbOpts := []string{"MySQL", "PostgreSQL"}
	if models.EnableSQLite3 {
		dbOpts = append(dbOpts, "SQLite3")
	}
	if models.EnableTidb {
		dbOpts = append(dbOpts, "TiDB")
	}
	ctx.Data["DbOptions"] = dbOpts
}

func Install(ctx *middleware.Context) {
	form := auth.InstallForm{}

	// Database settings
	form.DbHost = models.DbCfg.Host
	form.DbUser = models.DbCfg.User
	form.DbName = models.DbCfg.Name
	form.DbPath = models.DbCfg.Path

	ctx.Data["CurDbOption"] = "MySQL"
	switch models.DbCfg.Type {
	case "postgres":
		ctx.Data["CurDbOption"] = "PostgreSQL"
	case "sqlite3":
		if models.EnableSQLite3 {
			ctx.Data["CurDbOption"] = "SQLite3"
		}
	case "tidb":
		if models.EnableTidb {
			ctx.Data["CurDbOption"] = "TiDB"
		}
	}

	// Application general settings
	form.AppName = setting.AppName
	form.RepoRootPath = setting.RepoRootPath

	// Note(unknwon): it's hard for Windows users change a running user,
	// 	so just use current one if config says default.
	if setting.IsWindows && setting.RunUser == "git" {
		form.RunUser = user.CurrentUsername()
	} else {
		form.RunUser = setting.RunUser
	}

	form.Domain = setting.Domain
	form.SSHPort = setting.SSH.Port
	form.HTTPPort = setting.HttpPort
	form.AppUrl = setting.AppUrl
	form.LogRootPath = setting.LogRootPath

	// E-mail service settings
	if setting.MailService != nil {
		form.SMTPHost = setting.MailService.Host
		form.SMTPFrom = setting.MailService.From
		form.SMTPEmail = setting.MailService.User
	}
	form.RegisterConfirm = setting.Service.RegisterEmailConfirm
	form.MailNotify = setting.Service.EnableNotifyMail

	// Server and other services settings
	form.OfflineMode = setting.OfflineMode
	form.DisableGravatar = setting.DisableGravatar
	form.DisableRegistration = setting.Service.DisableRegistration
	form.EnableCaptcha = setting.Service.EnableCaptcha
	form.RequireSignInView = setting.Service.RequireSignInView

	auth.AssignForm(form, ctx.Data)
	ctx.HTML(200, INSTALL)
}

func InstallPost(ctx *middleware.Context, form auth.InstallForm) {
	ctx.Data["CurDbOption"] = form.DbType

	if ctx.HasError() {
		if ctx.HasValue("Err_SMTPEmail") {
			ctx.Data["Err_SMTP"] = true
		}
		if ctx.HasValue("Err_AdminName") ||
			ctx.HasValue("Err_AdminPasswd") ||
			ctx.HasValue("Err_AdminEmail") {
			ctx.Data["Err_Admin"] = true
		}

		ctx.HTML(200, INSTALL)
		return
	}

	if _, err := exec.LookPath("git"); err != nil {
		ctx.RenderWithErr(ctx.Tr("install.test_git_failed", err), INSTALL, &form)
		return
	}

	// Pass basic check, now test configuration.
	// Test database setting.
	dbTypes := map[string]string{"MySQL": "mysql", "PostgreSQL": "postgres", "SQLite3": "sqlite3", "TiDB": "tidb"}
	models.DbCfg.Type = dbTypes[form.DbType]
	models.DbCfg.Host = form.DbHost
	models.DbCfg.User = form.DbUser
	models.DbCfg.Passwd = form.DbPasswd
	models.DbCfg.Name = form.DbName
	models.DbCfg.SSLMode = form.SSLMode
	models.DbCfg.Path = form.DbPath

	if (models.DbCfg.Type == "sqlite3" || models.DbCfg.Type == "tidb") &&
		len(models.DbCfg.Path) == 0 {
		ctx.Data["Err_DbPath"] = true
		ctx.RenderWithErr(ctx.Tr("install.err_empty_db_path"), INSTALL, &form)
		return
	} else if models.DbCfg.Type == "tidb" &&
		strings.ContainsAny(path.Base(models.DbCfg.Path), ".-") {
		ctx.Data["Err_DbPath"] = true
		ctx.RenderWithErr(ctx.Tr("install.err_invalid_tidb_name"), INSTALL, &form)
		return
	}

	// Set test engine.
	var x *xorm.Engine
	if err := models.NewTestEngine(x); err != nil {
		if strings.Contains(err.Error(), `Unknown database type: sqlite3`) {
			ctx.Data["Err_DbType"] = true
			ctx.RenderWithErr(ctx.Tr("install.sqlite3_not_available", "http://gogs.io/docs/installation/install_from_binary.html"), INSTALL, &form)
		} else {
			ctx.Data["Err_DbSetting"] = true
			ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), INSTALL, &form)
		}
		return
	}

	// Test repository root path.
	form.RepoRootPath = strings.Replace(form.RepoRootPath, "\\", "/", -1)
	if err := os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil {
		ctx.Data["Err_RepoRootPath"] = true
		ctx.RenderWithErr(ctx.Tr("install.invalid_repo_path", err), INSTALL, &form)
		return
	}

	// Test log root path.
	form.LogRootPath = strings.Replace(form.LogRootPath, "\\", "/", -1)
	if err := os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil {
		ctx.Data["Err_LogRootPath"] = true
		ctx.RenderWithErr(ctx.Tr("install.invalid_log_root_path", err), INSTALL, &form)
		return
	}

	// Check run user.
	curUser := user.CurrentUsername()
	if form.RunUser != curUser {
		ctx.Data["Err_RunUser"] = true
		ctx.RenderWithErr(ctx.Tr("install.run_user_not_match", form.RunUser, curUser), INSTALL, &form)
		return
	}

	// Check logic loophole between disable self-registration and no admin account.
	if form.DisableRegistration && len(form.AdminName) == 0 {
		ctx.Data["Err_Services"] = true
		ctx.Data["Err_Admin"] = true
		ctx.RenderWithErr(ctx.Tr("install.no_admin_and_disable_registration"), INSTALL, form)
		return
	}

	// Check admin password.
	if len(form.AdminName) > 0 && len(form.AdminPasswd) == 0 {
		ctx.Data["Err_Admin"] = true
		ctx.Data["Err_AdminPasswd"] = true
		ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), INSTALL, form)
		return
	}
	if form.AdminPasswd != form.AdminConfirmPasswd {
		ctx.Data["Err_Admin"] = true
		ctx.Data["Err_AdminPasswd"] = true
		ctx.RenderWithErr(ctx.Tr("form.password_not_match"), INSTALL, form)
		return
	}

	if form.AppUrl[len(form.AppUrl)-1] != '/' {
		form.AppUrl += "/"
	}

	// Save settings.
	cfg := ini.Empty()
	if com.IsFile(setting.CustomConf) {
		// Keeps custom settings if there is already something.
		if err := cfg.Append(setting.CustomConf); err != nil {
			log.Error(4, "Fail to load custom conf '%s': %v", setting.CustomConf, err)
		}
	}
	cfg.Section("database").Key("DB_TYPE").SetValue(models.DbCfg.Type)
	cfg.Section("database").Key("HOST").SetValue(models.DbCfg.Host)
	cfg.Section("database").Key("NAME").SetValue(models.DbCfg.Name)
	cfg.Section("database").Key("USER").SetValue(models.DbCfg.User)
	cfg.Section("database").Key("PASSWD").SetValue(models.DbCfg.Passwd)
	cfg.Section("database").Key("SSL_MODE").SetValue(models.DbCfg.SSLMode)
	cfg.Section("database").Key("PATH").SetValue(models.DbCfg.Path)

	cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
	cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
	cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
	cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
	cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
	cfg.Section("server").Key("ROOT_URL").SetValue(form.AppUrl)

	if form.SSHPort == 0 {
		cfg.Section("server").Key("DISABLE_SSH").SetValue("true")
	} else {
		cfg.Section("server").Key("DISABLE_SSH").SetValue("false")
		cfg.Section("server").Key("SSH_PORT").SetValue(com.ToStr(form.SSHPort))
	}

	if len(strings.TrimSpace(form.SMTPHost)) > 0 {
		cfg.Section("mailer").Key("ENABLED").SetValue("true")
		cfg.Section("mailer").Key("HOST").SetValue(form.SMTPHost)
		cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom)
		cfg.Section("mailer").Key("USER").SetValue(form.SMTPEmail)
		cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd)
	} else {
		cfg.Section("mailer").Key("ENABLED").SetValue("false")
	}
	cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(com.ToStr(form.RegisterConfirm))
	cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(com.ToStr(form.MailNotify))

	cfg.Section("server").Key("OFFLINE_MODE").SetValue(com.ToStr(form.OfflineMode))
	cfg.Section("picture").Key("DISABLE_GRAVATAR").SetValue(com.ToStr(form.DisableGravatar))
	cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(com.ToStr(form.DisableRegistration))
	cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(com.ToStr(form.EnableCaptcha))
	cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(com.ToStr(form.RequireSignInView))

	cfg.Section("").Key("RUN_MODE").SetValue("prod")

	cfg.Section("session").Key("PROVIDER").SetValue("file")

	cfg.Section("log").Key("MODE").SetValue("file")
	cfg.Section("log").Key("LEVEL").SetValue("Info")
	cfg.Section("log").Key("ROOT_PATH").SetValue(form.LogRootPath)

	cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
	cfg.Section("security").Key("SECRET_KEY").SetValue(base.GetRandomString(15))

	os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm)
	if err := cfg.SaveTo(setting.CustomConf); err != nil {
		ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), INSTALL, &form)
		return
	}

	GlobalInit()

	// Create admin account
	if len(form.AdminName) > 0 {
		u := &models.User{
			Name:     form.AdminName,
			Email:    form.AdminEmail,
			Passwd:   form.AdminPasswd,
			IsAdmin:  true,
			IsActive: true,
		}
		if err := models.CreateUser(u); err != nil {
			if !models.IsErrUserAlreadyExist(err) {
				setting.InstallLock = false
				ctx.Data["Err_AdminName"] = true
				ctx.Data["Err_AdminEmail"] = true
				ctx.RenderWithErr(ctx.Tr("install.invalid_admin_setting", err), INSTALL, &form)
				return
			}
			log.Info("Admin account already exist")
			u, _ = models.GetUserByName(u.Name)
		}

		// Auto-login for admin
		ctx.Session.Set("uid", u.Id)
		ctx.Session.Set("uname", u.Name)
	}

	log.Info("First-time run install finished!")
	ctx.Flash.Success(ctx.Tr("install.install_success"))
	ctx.Redirect(form.AppUrl + "user/login")
}