diff --git a/README.md b/README.md index e88a247705..1d3aaf3e8a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language ![Demo](http://gowalker.org/public/gogs_demo.gif) -##### Current version: 0.1.9 Alpha +##### Current version: 0.2.0 Alpha #### Due to testing purpose, data of [try.gogits.org](http://try.gogits.org) has been reset in March 29, 2014 and will reset multiple times after. Please do NOT put your important data on the site. diff --git a/README_ZH.md b/README_ZH.md index 8e187c7364..4590e36b1d 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -5,7 +5,7 @@ Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。 ![Demo](http://gowalker.org/public/gogs_demo.gif) -##### 当前版本:0.1.9 Alpha +##### 当前版本:0.2.0 Alpha ## 开发目的 diff --git a/gogs.go b/gogs.go index f1372f0cfc..2ef35ca4da 100644 --- a/gogs.go +++ b/gogs.go @@ -19,7 +19,7 @@ import ( // Test that go1.2 tag above is included in builds. main.go refers to this definition. const go12tag = true -const APP_VER = "0.1.9.0329 Alpha" +const APP_VER = "0.2.0.0329 Alpha" func init() { base.AppVer = APP_VER diff --git a/models/issue.go b/models/issue.go index 9fd1b905a5..f14030df5e 100644 --- a/models/issue.go +++ b/models/issue.go @@ -170,9 +170,17 @@ type Milestone struct { Created time.Time `xorm:"created"` } +// Issue types. +const ( + IT_PLAIN = iota // Pure comment. + IT_REOPEN // Issue reopen status change prompt. + IT_CLOSE // Issue close status change prompt. +) + // Comment represents a comment in commit and issue page. type Comment struct { Id int64 + Type int PosterId int64 Poster *User `xorm:"-"` IssueId int64 @@ -183,21 +191,37 @@ type Comment struct { } // CreateComment creates comment of issue or commit. -func CreateComment(userId, issueId, commitId, line int64, content string) error { +func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error { sess := orm.NewSession() defer sess.Close() sess.Begin() - if _, err := orm.Insert(&Comment{PosterId: userId, IssueId: issueId, + if _, err := orm.Insert(&Comment{PosterId: userId, Type: cmtType, IssueId: issueId, CommitId: commitId, Line: line, Content: content}); err != nil { sess.Rollback() return err } - rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" - if _, err := sess.Exec(rawSql, issueId); err != nil { - sess.Rollback() - return err + // Check comment type. + switch cmtType { + case IT_PLAIN: + rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" + if _, err := sess.Exec(rawSql, issueId); err != nil { + sess.Rollback() + return err + } + case IT_REOPEN: + rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?" + if _, err := sess.Exec(rawSql, repoId); err != nil { + sess.Rollback() + return err + } + case IT_CLOSE: + rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?" + if _, err := sess.Exec(rawSql, repoId); err != nil { + sess.Rollback() + return err + } } return sess.Commit() } diff --git a/models/models.go b/models/models.go index bafa1747e8..825730c5a8 100644 --- a/models/models.go +++ b/models/models.go @@ -34,8 +34,7 @@ func LoadModelsConfig() { DbCfg.Path = base.Cfg.MustValue("database", "PATH", "data/gogs.db") } -func SetEngine() { - var err error +func SetEngine() (err error) { switch DbCfg.Type { case "mysql": orm, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8", @@ -47,12 +46,10 @@ func SetEngine() { os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm) orm, err = xorm.NewEngine("sqlite3", DbCfg.Path) default: - fmt.Printf("Unknown database type: %s\n", DbCfg.Type) - os.Exit(2) + return fmt.Errorf("Unknown database type: %s\n", DbCfg.Type) } if err != nil { - fmt.Printf("models.init(fail to conntect database): %v\n", err) - os.Exit(2) + return fmt.Errorf("models.init(fail to conntect database): %v\n", err) } // WARNNING: for serv command, MUST remove the output to os.stdout, @@ -62,20 +59,21 @@ func SetEngine() { //orm.ShowErr = true f, err := os.Create("xorm.log") if err != nil { - fmt.Printf("models.init(fail to create xorm.log): %v\n", err) - os.Exit(2) + return fmt.Errorf("models.init(fail to create xorm.log): %v\n", err) } orm.Logger = f orm.ShowSQL = true + return nil } -func NewEngine() { - SetEngine() - if err := orm.Sync(new(User), new(PublicKey), new(Repository), new(Watch), +func NewEngine() (err error) { + if err = SetEngine(); err != nil { + return err + } else if err = orm.Sync(new(User), new(PublicKey), new(Repository), new(Watch), new(Action), new(Access), new(Issue), new(Comment)); err != nil { - fmt.Printf("sync database struct error: %v\n", err) - os.Exit(2) + return fmt.Errorf("sync database struct error: %v\n", err) } + return nil } type Statistic struct { diff --git a/models/repo.go b/models/repo.go index a848694da6..0b2bbe4862 100644 --- a/models/repo.go +++ b/models/repo.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "strings" "time" "unicode/utf8" @@ -59,15 +58,6 @@ func NewRepoContext() { os.Exit(2) } } - - // Initialize illegal patterns. - for i := range illegalPatterns[1:] { - pattern := "" - for j := range illegalPatterns[i+1] { - pattern += "[" + string(illegalPatterns[i+1][j]-32) + string(illegalPatterns[i+1][j]) + "]" - } - illegalPatterns[i+1] = pattern - } } // Repository represents a git repository. @@ -105,15 +95,20 @@ func IsRepositoryExist(user *User, repoName string) (bool, error) { } var ( - // Define as all lower case!! - illegalPatterns = []string{"[.][Gg][Ii][Tt]", "raw", "user", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin"} + illegalEquals = []string{"raw", "install", "api", "avatar", "user", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin"} + illegalSuffixs = []string{".git"} ) // IsLegalName returns false if name contains illegal characters. func IsLegalName(repoName string) bool { - for _, pattern := range illegalPatterns { - has, _ := regexp.MatchString(pattern, repoName) - if has { + repoName = strings.ToLower(repoName) + for _, char := range illegalEquals { + if repoName == char { + return false + } + } + for _, char := range illegalSuffixs { + if strings.HasSuffix(repoName, char) { return false } } diff --git a/modules/auth/auth.go b/modules/auth/auth.go index ac03a8f126..361f55b2d3 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -172,6 +172,7 @@ type InstallForm struct { DatabasePath string `form:"database_path"` RepoRootPath string `form:"repo_path"` RunUser string `form:"run_user"` + Domain string `form:"domain"` AppUrl string `form:"app_url"` AdminName string `form:"admin_name" binding:"Required"` AdminPasswd string `form:"admin_pwd" binding:"Required;MinSize(6);MaxSize(30)"` diff --git a/modules/base/conf.go b/modules/base/conf.go index fd77cfd3ce..f696d083f7 100644 --- a/modules/base/conf.go +++ b/modules/base/conf.go @@ -272,18 +272,19 @@ func NewConfigContext() { Domain = Cfg.MustValue("server", "DOMAIN") SecretKey = Cfg.MustValue("security", "SECRET_KEY") + InstallLock = Cfg.MustBool("security", "INSTALL_LOCK", false) + RunUser = Cfg.MustValue("", "RUN_USER") curUser := os.Getenv("USERNAME") if len(curUser) == 0 { curUser = os.Getenv("USER") } - if RunUser != curUser { + // Does not check run user when the install lock is off. + if InstallLock && RunUser != curUser { fmt.Printf("Expect user(%s) but current user is: %s\n", RunUser, curUser) os.Exit(2) } - InstallLock = Cfg.MustBool("security", "INSTALL_LOCK", false) - LogInRememberDays = Cfg.MustInt("security", "LOGIN_REMEMBER_DAYS") CookieUserName = Cfg.MustValue("security", "COOKIE_USERNAME") CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME") diff --git a/public/css/gogs.css b/public/css/gogs.css index d6c117c846..965c90962c 100755 --- a/public/css/gogs.css +++ b/public/css/gogs.css @@ -1197,13 +1197,13 @@ html, body { line-height: 42px; } -#issue .issue-closed{ - border-bottom: 3px solid #CCC; +#issue .issue-closed, #issue .issue-opened { + border-bottom: 2px solid #CCC; margin-bottom: 24px; padding-bottom: 24px; } -#issue .issue-closed .btn-danger,#issue .issue-opened .btn-success{ +#issue .issue-closed .label-danger,#issue .issue-opened .label-success{ margin: 0 .8em; } diff --git a/routers/install.go b/routers/install.go index b5c3a9364b..407705b73a 100644 --- a/routers/install.go +++ b/routers/install.go @@ -6,13 +6,46 @@ package routers import ( "errors" + "os" + "strings" + + "github.com/Unknwon/goconfig" + "github.com/codegangsta/martini" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/mailer" "github.com/gogits/gogs/modules/middleware" ) +// Check run mode(Default of martini is Dev). +func checkRunMode() { + switch base.Cfg.MustValue("", "RUN_MODE") { + case "prod": + martini.Env = martini.Prod + case "test": + martini.Env = martini.Test + } + log.Info("Run Mode: %s", strings.Title(martini.Env)) +} + +// GlobalInit is for global configuration reload-able. +func GlobalInit() { + base.NewConfigContext() + mailer.NewMailerContext() + models.LoadModelsConfig() + models.LoadRepoConfig() + models.NewRepoContext() + if err := models.NewEngine(); err != nil && base.InstallLock { + log.Error("%v", err) + os.Exit(2) + } + base.NewServices() + checkRunMode() +} + func Install(ctx *middleware.Context, form auth.InstallForm) { if base.InstallLock { ctx.Handle(404, "install.Install", errors.New("Installation is prohibited")) @@ -20,14 +53,114 @@ func Install(ctx *middleware.Context, form auth.InstallForm) { } ctx.Data["Title"] = "Install" - ctx.Data["DbCfg"] = models.DbCfg - ctx.Data["RepoRootPath"] = base.RepoRootPath - ctx.Data["RunUser"] = base.RunUser - ctx.Data["AppUrl"] = base.AppUrl ctx.Data["PageIsInstall"] = true + // Get and assign value to install form. + if len(form.Host) == 0 { + form.Host = models.DbCfg.Host + } + if len(form.User) == 0 { + form.User = models.DbCfg.User + } + if len(form.Passwd) == 0 { + form.Passwd = models.DbCfg.Pwd + } + if len(form.DatabaseName) == 0 { + form.DatabaseName = models.DbCfg.Name + } + if len(form.DatabasePath) == 0 { + form.DatabasePath = models.DbCfg.Path + } + + if len(form.RepoRootPath) == 0 { + form.RepoRootPath = base.RepoRootPath + } + if len(form.RunUser) == 0 { + form.RunUser = base.RunUser + } + if len(form.Domain) == 0 { + form.Domain = base.Domain + } + if len(form.AppUrl) == 0 { + form.AppUrl = base.AppUrl + } + + auth.AssignForm(form, ctx.Data) + if ctx.Req.Method == "GET" { ctx.HTML(200, "install") return } + + if ctx.HasError() { + ctx.HTML(200, "install") + return + } + + // Pass basic check, now test configuration. + // Test database setting. + dbTypes := map[string]string{"mysql": "mysql", "pgsql": "postgres", "sqlite": "sqlite3"} + models.DbCfg.Type = dbTypes[form.Database] + models.DbCfg.Host = form.Host + models.DbCfg.User = form.User + models.DbCfg.Pwd = form.Passwd + models.DbCfg.Name = form.DatabaseName + models.DbCfg.SslMode = form.SslMode + models.DbCfg.Path = form.DatabasePath + + if err := models.NewEngine(); err != nil { + ctx.RenderWithErr("Database setting is not correct: "+err.Error(), "install", &form) + return + } + + // Test repository root path. + if err := os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil { + ctx.RenderWithErr("Repository root path is invalid: "+err.Error(), "install", &form) + return + } + + // Create admin account. + if _, err := models.RegisterUser(&models.User{Name: form.AdminName, Email: form.AdminEmail, Passwd: form.AdminPasswd, + IsAdmin: true, IsActive: true}); err != nil { + if err != models.ErrUserAlreadyExist { + ctx.RenderWithErr("Admin account setting is invalid: "+err.Error(), "install", &form) + return + } + } + + // Save settings. + base.Cfg.SetValue("database", "DB_TYPE", models.DbCfg.Type) + base.Cfg.SetValue("database", "HOST", models.DbCfg.Host) + base.Cfg.SetValue("database", "NAME", models.DbCfg.Name) + base.Cfg.SetValue("database", "USER", models.DbCfg.User) + base.Cfg.SetValue("database", "PASSWD", models.DbCfg.Pwd) + base.Cfg.SetValue("database", "SSL_MODE", models.DbCfg.SslMode) + base.Cfg.SetValue("database", "PATH", models.DbCfg.Path) + + base.Cfg.SetValue("repository", "ROOT", form.RepoRootPath) + base.Cfg.SetValue("", "RUN_USER", form.RunUser) + base.Cfg.SetValue("server", "DOMAIN", form.Domain) + base.Cfg.SetValue("server", "ROOT_URL", form.AppUrl) + + if len(form.Host) > 0 { + base.Cfg.SetValue("mailer", "ENABLED", "true") + base.Cfg.SetValue("mailer", "HOST", form.SmtpHost) + base.Cfg.SetValue("mailer", "USER", form.SmtpEmail) + base.Cfg.SetValue("mailer", "PASSWD", form.SmtpPasswd) + + base.Cfg.SetValue("service", "REGISTER_EMAIL_CONFIRM", base.ToStr(form.RegisterConfirm == "on")) + base.Cfg.SetValue("service", "ENABLE_NOTIFY_MAIL", base.ToStr(form.MailNotify == "on")) + } + + base.Cfg.SetValue("security", "INSTALL_LOCK", "true") + + if err := goconfig.SaveConfigFile(base.Cfg, "custom/conf/app.ini"); err != nil { + ctx.RenderWithErr("Fail to save configuration: "+err.Error(), "install", &form) + return + } + + GlobalInit() + + log.Info("First-time run install finished!") + ctx.Redirect("/user/login") } diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 337bd4bf1c..3506e90163 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -7,6 +7,7 @@ package repo import ( "fmt" "net/url" + "strings" "github.com/codegangsta/martini" @@ -175,7 +176,7 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { ctx.Data["Title"] = issue.Name ctx.Data["Issue"] = issue ctx.Data["Comments"] = comments - ctx.Data["IsIssueOwner"] = issue.PosterId == ctx.User.Id + ctx.Data["IsIssueOwner"] = ctx.Repo.IsOwner || issue.PosterId == ctx.User.Id ctx.Data["IsRepoToolbarIssues"] = true ctx.Data["IsRepoToolbarIssuesList"] = false ctx.HTML(200, "issue/view") @@ -225,24 +226,17 @@ func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat } func Comment(ctx *middleware.Context, params martini.Params) { - fmt.Println(ctx.Query("change_status")) if !ctx.Repo.IsValid { ctx.Handle(404, "issue.Comment(invalid repo):", nil) } - index, err := base.StrTo(ctx.Query("issueIndex")).Int() + index, err := base.StrTo(ctx.Query("issueIndex")).Int64() if err != nil { - ctx.Handle(404, "issue.Comment", err) + ctx.Handle(404, "issue.Comment(get index)", err) return } - content := ctx.Query("content") - if len(content) == 0 { - ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", ctx.User.Name, ctx.Repo.Repository.Name, index)) - return - } - - issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, int64(index)) + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index) if err != nil { if err == models.ErrIssueNotExist { ctx.Handle(404, "issue.Comment", err) @@ -252,16 +246,46 @@ func Comment(ctx *middleware.Context, params martini.Params) { return } - switch params["action"] { - case "new": - if err = models.CreateComment(ctx.User.Id, issue.Id, 0, 0, content); err != nil { - ctx.Handle(500, "issue.Comment(create comment)", err) + // Check if issue owner changes the status of issue. + var newStatus string + if ctx.Repo.IsOwner || issue.PosterId == ctx.User.Id { + newStatus = ctx.Query("change_status") + } + if len(newStatus) > 0 { + if (strings.Contains(newStatus, "Reopen") && issue.IsClosed) || + (strings.Contains(newStatus, "Close") && !issue.IsClosed) { + issue.IsClosed = !issue.IsClosed + if err = models.UpdateIssue(issue); err != nil { + ctx.Handle(200, "issue.Comment(update issue status)", err) + return + } + + cmtType := models.IT_CLOSE + if !issue.IsClosed { + cmtType = models.IT_REOPEN + } + + if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, ""); err != nil { + ctx.Handle(200, "issue.Comment(create status change comment)", err) + return + } + log.Trace("%s Issue(%d) status changed: %v", ctx.Req.RequestURI, issue.Id, !issue.IsClosed) + } + } + + content := ctx.Query("content") + if len(content) > 0 { + switch params["action"] { + case "new": + if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content); err != nil { + ctx.Handle(500, "issue.Comment(create comment)", err) + return + } + log.Trace("%s Comment created: %d", ctx.Req.RequestURI, issue.Id) + default: + ctx.Handle(404, "issue.Comment", err) return } - log.Trace("%s Comment created: %d", ctx.Req.RequestURI, issue.Id) - default: - ctx.Handle(404, "issue.Comment", err) - return } ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", ctx.User.Name, ctx.Repo.Repository.Name, index)) diff --git a/routers/user/user.go b/routers/user/user.go index 114169e606..aeaf91c97d 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -120,7 +120,7 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { return } - if hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) { + if ctx.HasError() { ctx.HTML(200, "user/signin") return } @@ -308,17 +308,19 @@ func Issues(ctx *middleware.Context) { showRepos := make([]models.Repository, 0, len(repos)) - var closedIssueCount, createdByCount int + isShowClosed := ctx.Query("state") == "closed" + var closedIssueCount, createdByCount, allIssueCount int // Get all issues. allIssues := make([]models.Issue, 0, 5*len(repos)) for i, repo := range repos { - issues, err := models.GetIssues(0, repo.Id, posterId, 0, page, false, false, "", "") + issues, err := models.GetIssues(0, repo.Id, posterId, 0, page, isShowClosed, false, "", "") if err != nil { ctx.Handle(200, "user.Issues(get issues)", err) return } + allIssueCount += repo.NumIssues closedIssueCount += repo.NumClosedIssues // Set repository information to issues. @@ -330,12 +332,10 @@ func Issues(ctx *middleware.Context) { repos[i].NumOpenIssues = repo.NumIssues - repo.NumClosedIssues if repos[i].NumOpenIssues > 0 { showRepos = append(showRepos, repos[i]) - } } showIssues := make([]models.Issue, 0, len(allIssues)) - isShowClosed := ctx.Query("state") == "closed" ctx.Data["IsShowClosed"] = isShowClosed // Get posters and filter issues. @@ -361,9 +361,9 @@ func Issues(ctx *middleware.Context) { ctx.Data["Repos"] = showRepos ctx.Data["Issues"] = showIssues - ctx.Data["AllIssueCount"] = len(allIssues) + ctx.Data["AllIssueCount"] = allIssueCount ctx.Data["ClosedIssueCount"] = closedIssueCount - ctx.Data["OpenIssueCount"] = len(allIssues) - closedIssueCount + ctx.Data["OpenIssueCount"] = allIssueCount - closedIssueCount ctx.Data["CreatedByCount"] = createdByCount ctx.HTML(200, "issue/user") } diff --git a/templates/install.tmpl b/templates/install.tmpl index a456ac5f2c..20bd502da2 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -3,9 +3,8 @@