diff --git a/.gopmfile b/.gopmfile index 5cfcc20c05..b8aa02372a 100644 --- a/.gopmfile +++ b/.gopmfile @@ -19,8 +19,6 @@ github.com/gogits/session = `commit:7ab78d4` github.com/juju2013/goldap = `commit:f4a7f67` github.com/lib/pq = `commit:529edd9` github.com/nfnt/resize = `commit:8aee0d9` -github.com/qiniu/log = `commit:891d1cb` -github.com/robfig/cron = `commit:b024fc5` [res] include = templates|public diff --git a/README.md b/README.md index 2de797afd2..af33b0e8eb 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ 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.4.0 Alpha +##### Current version: 0.4.5 Alpha ### NOTICES -- Due to testing purpose, data of [try.gogits.org](http://try.gogits.org) has been reset in **April 14, 2014** and will reset multiple times after. Please do **NOT** put your important data on the site. +- Due to testing purpose, data of [try.gogits.org](http://try.gogits.org) has been reset in **June 21, 2014** and will reset multiple times after. Please do **NOT** put your important data on the site. - Demo site [try.gogits.org](http://try.gogits.org) is running under `dev` branch. #### Other language version @@ -33,7 +33,7 @@ More importantly, Gogs only needs one binary to setup your own project hosting o - Activity timeline - SSH/HTTP(S) protocol support -- SMTP/LDAP authentication support +- SMTP/LDAP/reverse proxy authentication support - Register/delete/rename account - Create/migrate/mirror/delete/watch/rename/transfer public/private repository - Repository viewer/release/issue tracker/webhooks @@ -75,9 +75,6 @@ There are 5 ways to install Gogs: The [core team](http://gogs.io/team) of this project. See [contributors page](https://github.com/gogits/gogs/graphs/contributors) for full list of contributors. -[![Clone in Koding](http://learn.koding.com/btn/clone_d.png)][koding] -[koding]: https://koding.com/Teamwork?import=https://github.com/gogits/gogs/archive/master.zip&c=git1 - ## License This project is under the MIT License. See the [LICENSE](https://github.com/gogits/gogs/blob/master/LICENSE) file for the full license text. diff --git a/README_ZH.md b/README_ZH.md index f3be4418d1..26746ccbe8 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.4.0 Alpha +##### 当前版本:0.4.5 Alpha ## 开发目的 @@ -24,7 +24,7 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依 - 活动时间线 - 支持 SSH/HTTP(S) 协议 -- 支持 SMTP/LDAP 用户认证 +- 支持 SMTP/LDAP/反向代理 用户认证 - 注册/删除/重命名用户 - 创建/迁移/镜像/删除/关注/重命名/转移 公开/私有 仓库 - 仓库 浏览器/发布/缺陷管理/Web 钩子 @@ -66,9 +66,6 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依 本项目的 [开发团队](http://gogs.io/team)。您可以通过查看 [贡献者页面](https://github.com/gogits/gogs/graphs/contributors) 获取完整的贡献者列表。 -[![Clone in Koding](http://learn.koding.com/btn/clone_d.png)][koding] -[koding]: https://koding.com/Teamwork?import=https://github.com/gogits/gogs/archive/master.zip&c=git1 - ## 授权许可 本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/gogits/gogs/blob/master/LICENSE) 文件中。 \ No newline at end of file diff --git a/cmd/fix.go b/cmd/fix.go index 3c2278ba90..95ab3ae66f 100644 --- a/cmd/fix.go +++ b/cmd/fix.go @@ -5,10 +5,16 @@ package cmd import ( + "bufio" "fmt" + "io" + "io/ioutil" "os" + "path" + "strings" "github.com/codegangsta/cli" + "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/setting" ) @@ -16,28 +22,152 @@ import ( var CmdFix = cli.Command{ Name: "fix", Usage: "This command for upgrade from old version", - Description: `Fix provide upgrade from old version`, Action: runFix, + Subcommands: fixCommands, Flags: []cli.Flag{}, } -func runFix(k *cli.Context) { - workDir, _ := setting.WorkDir() - newLogger(workDir) - - setting.NewConfigContext() - models.LoadModelsConfig() - - if models.UseSQLite3 { - os.Chdir(workDir) - } - - models.SetEngine() - - err := models.Fix() - if err != nil { - fmt.Println(err) - } else { - fmt.Println("Fix successfully!") - } +func runFix(ctx *cli.Context) { +} + +var fixCommands = []cli.Command{ + { + Name: "location", + Usage: "Change Gogs app location", + Description: `Command location fixes location change of Gogs + +gogs fix location +`, + Action: runFixLocation, + }, +} + +// rewriteAuthorizedKeys replaces old Gogs path to the new one. +func rewriteAuthorizedKeys(sshPath, oldPath, newPath string) error { + fr, err := os.Open(sshPath) + if err != nil { + return err + } + defer fr.Close() + + tmpPath := sshPath + ".tmp" + fw, err := os.Create(tmpPath) + if err != nil { + return err + } + defer fw.Close() + + oldPath = "command=\"" + oldPath + " serv" + newPath = "command=\"" + newPath + " serv" + buf := bufio.NewReader(fr) + for { + line, errRead := buf.ReadString('\n') + line = strings.TrimSpace(line) + + if errRead != nil { + if errRead != io.EOF { + return errRead + } + + // Reached end of file, if nothing to read then break, + // otherwise handle the last line. + if len(line) == 0 { + break + } + } + + // Still finding the line, copy the line that currently read. + if _, err = fw.WriteString(strings.Replace(line, oldPath, newPath, 1) + "\n"); err != nil { + return err + } + + if errRead == io.EOF { + break + } + } + + if err = os.Remove(sshPath); err != nil { + return err + } + return os.Rename(tmpPath, sshPath) +} + +func rewriteUpdateHook(path, appPath string) error { + rp := strings.NewReplacer("\\", "/", " ", "\\ ") + if err := ioutil.WriteFile(path, []byte(fmt.Sprintf(models.TPL_UPDATE_HOOK, + setting.ScriptType, rp.Replace(appPath))), os.ModePerm); err != nil { + return err + } + return nil +} + +func walkDir(rootPath, recPath, appPath string, depth int) error { + depth++ + if depth > 3 { + return nil + } else if depth == 3 { + if err := rewriteUpdateHook(path.Join(rootPath, "hooks/update"), appPath); err != nil { + return err + } + } + + dir, err := os.Open(rootPath) + if err != nil { + return err + } + defer dir.Close() + + fis, err := dir.Readdir(0) + if err != nil { + return err + } + + for _, fi := range fis { + if strings.Contains(fi.Name(), ".DS_Store") { + continue + } + + relPath := path.Join(recPath, fi.Name()) + curPath := path.Join(rootPath, fi.Name()) + if fi.IsDir() { + if err = walkDir(curPath, relPath, appPath, depth); err != nil { + return err + } + } + } + return nil +} + +func runFixLocation(ctx *cli.Context) { + if len(ctx.Args()) != 1 { + fmt.Println("Incorrect arguments number, expect 1") + os.Exit(2) + } + + execPath, _ := setting.ExecPath() + + oldPath := ctx.Args().First() + fmt.Printf("Old location: %s\n", oldPath) + fmt.Println("This command should be executed in the new Gogs path") + fmt.Printf("Do you want to change Gogs app path from old location to:\n") + fmt.Printf("-> %s?\n", execPath) + fmt.Print("Press to continue, use to exit.") + fmt.Scanln() + + // Fix in authorized_keys file. + sshPath := path.Join(models.SshPath, "authorized_keys") + fmt.Printf("Fixing pathes in file: %s\n", sshPath) + if err := rewriteAuthorizedKeys(sshPath, oldPath, execPath); err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Fix position in gogs-repositories. + setting.NewConfigContext() + fmt.Printf("Fixing pathes in repositories: %s\n", setting.RepoRootPath) + if err := walkDir(setting.RepoRootPath, "", execPath, 0); err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println("Fix position finished!") } diff --git a/cmd/serve.go b/cmd/serve.go index 302f8568e1..2a76da7937 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -13,9 +13,9 @@ import ( "strings" "github.com/codegangsta/cli" - qlog "github.com/qiniu/log" "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" ) @@ -27,27 +27,13 @@ var CmdServ = cli.Command{ Flags: []cli.Flag{}, } -func newLogger(logPath string) { - os.MkdirAll(path.Dir(logPath), os.ModePerm) - - f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.ModePerm) - if err != nil { - qlog.Fatal(err) - } - - qlog.SetOutput(f) - //qlog.SetOutputLevel(qlog.Ldebug) - qlog.Info("Start logging serv...") -} - func setup(logPath string) { - workDir, _ := setting.WorkDir() - newLogger(path.Join(workDir, logPath)) - setting.NewConfigContext() + log.NewGitLogger(path.Join(setting.LogRootPath, logPath)) models.LoadModelsConfig() if models.UseSQLite3 { + workDir, _ := setting.WorkDir() os.Chdir(workDir) } @@ -70,45 +56,45 @@ func parseCmd(cmd string) (string, string) { } var ( - COMMANDS_READONLY = map[string]int{ - "git-upload-pack": models.AU_WRITABLE, - "git upload-pack": models.AU_WRITABLE, - "git-upload-archive": models.AU_WRITABLE, + COMMANDS_READONLY = map[string]models.AccessType{ + "git-upload-pack": models.WRITABLE, + "git upload-pack": models.WRITABLE, + "git-upload-archive": models.WRITABLE, } - COMMANDS_WRITE = map[string]int{ - "git-receive-pack": models.AU_READABLE, - "git receive-pack": models.AU_READABLE, + COMMANDS_WRITE = map[string]models.AccessType{ + "git-receive-pack": models.READABLE, + "git receive-pack": models.READABLE, } ) -func In(b string, sl map[string]int) bool { +func In(b string, sl map[string]models.AccessType) bool { _, e := sl[b] return e } func runServ(k *cli.Context) { - setup(path.Join(setting.LogRootPath, "serv.log")) + setup("serv.log") keys := strings.Split(os.Args[2], "-") if len(keys) != 2 { println("Gogs: auth file format error") - qlog.Fatal("Invalid auth file format: %s", os.Args[2]) + log.GitLogger.Fatal("Invalid auth file format: %s", os.Args[2]) } keyId, err := strconv.ParseInt(keys[1], 10, 64) if err != nil { println("Gogs: auth file format error") - qlog.Fatalf("Invalid auth file format: %v", err) + log.GitLogger.Fatal("Invalid auth file format: %v", err) } user, err := models.GetUserByKeyId(keyId) if err != nil { if err == models.ErrUserNotKeyOwner { println("Gogs: you are not the owner of SSH key") - qlog.Fatalf("Invalid owner of SSH key: %d", keyId) + log.GitLogger.Fatal("Invalid owner of SSH key: %d", keyId) } println("Gogs: internal error:", err) - qlog.Fatalf("Fail to get user by key ID(%d): %v", keyId, err) + log.GitLogger.Fatal("Fail to get user by key ID(%d): %v", keyId, err) } cmd := os.Getenv("SSH_ORIGINAL_COMMAND") @@ -122,7 +108,7 @@ func runServ(k *cli.Context) { rr := strings.SplitN(repoPath, "/", 2) if len(rr) != 2 { println("Gogs: unavailable repository", args) - qlog.Fatalf("Unavailable repository: %v", args) + log.GitLogger.Fatal("Unavailable repository: %v", args) } repoUserName := rr[0] repoName := strings.TrimSuffix(rr[1], ".git") @@ -134,45 +120,45 @@ func runServ(k *cli.Context) { if err != nil { if err == models.ErrUserNotExist { println("Gogs: given repository owner are not registered") - qlog.Fatalf("Unregistered owner: %s", repoUserName) + log.GitLogger.Fatal("Unregistered owner: %s", repoUserName) } println("Gogs: internal error:", err) - qlog.Fatalf("Fail to get repository owner(%s): %v", repoUserName, err) + log.GitLogger.Fatal("Fail to get repository owner(%s): %v", repoUserName, err) } // Access check. switch { case isWrite: - has, err := models.HasAccess(user.Name, path.Join(repoUserName, repoName), models.AU_WRITABLE) + has, err := models.HasAccess(user.Name, path.Join(repoUserName, repoName), models.WRITABLE) if err != nil { println("Gogs: internal error:", err) - qlog.Fatal("Fail to check write access:", err) + log.GitLogger.Fatal("Fail to check write access:", err) } else if !has { println("You have no right to write this repository") - qlog.Fatalf("User %s has no right to write repository %s", user.Name, repoPath) + log.GitLogger.Fatal("User %s has no right to write repository %s", user.Name, repoPath) } case isRead: repo, err := models.GetRepositoryByName(repoUser.Id, repoName) if err != nil { if err == models.ErrRepoNotExist { println("Gogs: given repository does not exist") - qlog.Fatalf("Repository does not exist: %s/%s", repoUser.Name, repoName) + log.GitLogger.Fatal("Repository does not exist: %s/%s", repoUser.Name, repoName) } println("Gogs: internal error:", err) - qlog.Fatalf("Fail to get repository: %v", err) + log.GitLogger.Fatal("Fail to get repository: %v", err) } if !repo.IsPrivate { break } - has, err := models.HasAccess(user.Name, path.Join(repoUserName, repoName), models.AU_READABLE) + has, err := models.HasAccess(user.Name, path.Join(repoUserName, repoName), models.READABLE) if err != nil { println("Gogs: internal error:", err) - qlog.Fatal("Fail to check read access:", err) + log.GitLogger.Fatal("Fail to check read access:", err) } else if !has { println("You have no right to access this repository") - qlog.Fatalf("User %s has no right to read repository %s", user.Name, repoPath) + log.GitLogger.Fatal("User %s has no right to read repository %s", user.Name, repoPath) } default: println("Unknown command") @@ -186,18 +172,9 @@ func runServ(k *cli.Context) { gitcmd.Stdout = os.Stdout gitcmd.Stdin = os.Stdin gitcmd.Stderr = os.Stderr - - if err = gitcmd.Run(); err != nil { + err = gitcmd.Run() + if err != nil { println("Gogs: internal error:", err) - qlog.Fatalf("Fail to execute git command: %v", err) + log.GitLogger.Fatal("Fail to execute git command: %v", err) } - - //refName := os.Getenv("refName") - //oldCommitId := os.Getenv("oldCommitId") - //newCommitId := os.Getenv("newCommitId") - - //qlog.Error("get envs:", refName, oldCommitId, newCommitId) - - // update - //models.Update(refName, oldCommitId, newCommitId, repoUserName, repoName, user.Id) } diff --git a/cmd/update.go b/cmd/update.go index b8686a3e6e..c030b6cfb2 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -6,14 +6,12 @@ package cmd import ( "os" - "path" "strconv" "github.com/codegangsta/cli" - qlog "github.com/qiniu/log" "github.com/gogits/gogs/models" - "github.com/gogits/gogs/modules/setting" + "github.com/gogits/gogs/modules/log" ) var CmdUpdate = cli.Command{ @@ -24,35 +22,27 @@ var CmdUpdate = cli.Command{ Flags: []cli.Flag{}, } -func updateEnv(refName, oldCommitId, newCommitId string) { - os.Setenv("refName", refName) - os.Setenv("oldCommitId", oldCommitId) - os.Setenv("newCommitId", newCommitId) - qlog.Info("set envs:", refName, oldCommitId, newCommitId) -} - func runUpdate(c *cli.Context) { cmd := os.Getenv("SSH_ORIGINAL_COMMAND") if cmd == "" { return } - setup(path.Join(setting.LogRootPath, "update.log")) + setup("update.log") args := c.Args() if len(args) != 3 { - qlog.Fatal("received less 3 parameters") + log.GitLogger.Fatal("received less 3 parameters") } else if args[0] == "" { - qlog.Fatal("refName is empty, shouldn't use") + log.GitLogger.Fatal("refName is empty, shouldn't use") } - //updateEnv(args[0], args[1], args[2]) - userName := os.Getenv("userName") userId, _ := strconv.ParseInt(os.Getenv("userId"), 10, 64) - //repoId := os.Getenv("repoId") repoUserName := os.Getenv("repoUserName") repoName := os.Getenv("repoName") - models.Update(args[0], args[1], args[2], userName, repoUserName, repoName, userId) + if err := models.Update(args[0], args[1], args[2], userName, repoUserName, repoName, userId); err != nil { + log.GitLogger.Fatal(err.Error()) + } } diff --git a/cmd/web.go b/cmd/web.go index dc4daca085..7387d445e4 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -27,6 +27,7 @@ import ( "github.com/gogits/gogs/routers/admin" "github.com/gogits/gogs/routers/api/v1" "github.com/gogits/gogs/routers/dev" + "github.com/gogits/gogs/routers/org" "github.com/gogits/gogs/routers/repo" "github.com/gogits/gogs/routers/user" ) @@ -89,11 +90,13 @@ func runWeb(*cli.Context) { m.Get("/", ignSignIn, routers.Home) m.Get("/install", bindIgnErr(auth.InstallForm{}), routers.Install) m.Post("/install", bindIgnErr(auth.InstallForm{}), routers.InstallPost) - m.Get("/issues", reqSignIn, user.Issues) - m.Get("/pulls", reqSignIn, user.Pulls) - m.Get("/stars", reqSignIn, user.Stars) + m.Group("", func(r martini.Router) { + r.Get("/issues", user.Issues) + r.Get("/pulls", user.Pulls) + r.Get("/stars", user.Stars) + }, reqSignIn) - m.Group("/api", func(r martini.Router) { + m.Group("/api", func(_ martini.Router) { m.Group("/v1", func(r martini.Router) { // Miscellaneous. r.Post("/markdown", bindIgnErr(apiv1.MarkdownForm{}), v1.Markdown) @@ -159,8 +162,9 @@ func runWeb(*cli.Context) { m.Group("/admin", func(r martini.Router) { r.Get("/users", admin.Users) r.Get("/repos", admin.Repositories) - r.Get("/config", admin.Config) r.Get("/auths", admin.Auths) + r.Get("/config", admin.Config) + r.Get("/monitor", admin.Monitor) }, adminReq) m.Group("/admin/users", func(r martini.Router) { r.Get("/new", admin.NewUser) @@ -184,6 +188,22 @@ func runWeb(*cli.Context) { reqOwner := middleware.RequireOwner() + m.Group("/org", func(r martini.Router) { + r.Get("/create", org.New) + r.Post("/create", bindIgnErr(auth.CreateOrgForm{}), org.NewPost) + r.Get("/:org", org.Organization) + r.Get("/:org/dashboard", org.Dashboard) + r.Get("/:org/members", org.Members) + + r.Get("/:org/teams/:team/edit", org.EditTeam) + r.Get("/:org/teams/new", org.NewTeam) + r.Get("/:org/teams", org.Teams) + + r.Get("/:org/settings", org.Settings) + r.Post("/:org/settings", bindIgnErr(auth.OrgSettingForm{}), org.SettingsPost) + r.Post("/:org/settings/delete", org.DeletePost) + }, reqSignIn) + m.Group("/:username/:reponame", func(r martini.Router) { r.Get("/settings", repo.Setting) r.Post("/settings", bindIgnErr(auth.RepoSettingForm{}), repo.SettingPost) @@ -221,11 +241,13 @@ func runWeb(*cli.Context) { }) r.Post("/comment/:action", repo.Comment) - r.Get("/releases/new", repo.ReleasesNew) + r.Get("/releases/new", repo.NewRelease) + r.Get("/releases/edit/:tagname", repo.EditRelease) }, reqSignIn, middleware.RepoAssignment(true)) m.Group("/:username/:reponame", func(r martini.Router) { - r.Post("/releases/new", bindIgnErr(auth.NewReleaseForm{}), repo.ReleasesNewPost) + r.Post("/releases/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost) + r.Post("/releases/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost) }, reqSignIn, middleware.RepoAssignment(true, true)) m.Group("/:username/:reponame", func(r martini.Router) { diff --git a/conf/app.ini b/conf/app.ini index 1fc88757d8..296509f721 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -51,6 +51,8 @@ SECRET_KEY = !#@FDEWREWR&*( LOGIN_REMEMBER_DAYS = 7 COOKIE_USERNAME = gogs_awesome COOKIE_REMEMBER_NAME = gogs_incredible +; Reverse proxy authentication header name of user name +REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER [service] ACTIVE_CODE_LIVE_MINUTES = 180 @@ -65,6 +67,14 @@ REQUIRE_SIGNIN_VIEW = false ENABLE_CACHE_AVATAR = false ; Mail notification ENABLE_NOTIFY_MAIL = false +; More detail: https://github.com/gogits/gogs/issues/165 +ENABLE_REVERSE_PROXY_AUTHENTICATION = false + +[webhook] +; Cron task interval in minutes +TASK_INTERVAL = 1 +; Deliver timeout in seconds +DELIVER_TIMEOUT = 5 [mailer] ENABLED = false @@ -227,5 +237,7 @@ RECEIVERS = ; For "database" mode only [log.database] LEVEL = +; Either "mysql" or "postgres" DRIVER = +; Based on xorm, e.g.: root:root@localhost/gogs?charset=utf8 CONN = diff --git a/dockerfiles/build.sh b/dockerfiles/build.sh index 83f7e9a566..b658db4ecd 100755 --- a/dockerfiles/build.sh +++ b/dockerfiles/build.sh @@ -10,6 +10,12 @@ HOST_PORT="YOUR_HOST_PORT" # The port on host, which will be redirected t # apt source, you can select 'nchc'(mirror in Taiwan) or 'aliyun'(best for mainlance China users) according to your network, if you could connect to the official unbunt mirror in a fast speed, just leave it to "". APT_SOURCE="" +DOCKER_BIN=$(which docker.io || which docker) +if [ -z "$DOCKER_BIN" ] ; then + echo "Please install docker. You can install docker by running \"wget -qO- https://get.docker.io/ | sh\"." + exit 1 +fi + # Replace the database root password in database image Dockerfile. sed -i "s/THE_DB_PASSWORD/$DB_PASSWORD/g" images/$DB_TYPE/Dockerfile # Replace the database root password in gogits image deploy.sh file. @@ -36,22 +42,22 @@ if [ $MEM_TYPE != "" ] sed -i "${GOGS_BUILD_LINE}s/$/ -tags $MEM_TYPE/" images/gogits/Dockerfile cd images/$MEM_TYPE - docker build -t gogits/$MEM_TYPE . - docker run -d --name $MEM_RUN_NAME gogits/$MEM_TYPE + $DOCKER_BIN build -t gogits/$MEM_TYPE . + $DOCKER_BIN run -d --name $MEM_RUN_NAME gogits/$MEM_TYPE MEM_LINK=" --link $MEM_RUN_NAME:mem " cd ../../ fi # Build the database image cd images/$DB_TYPE -docker build -t gogits/$DB_TYPE . +$DOCKER_BIN build -t gogits/$DB_TYPE . # ## Build the gogits image cd ../gogits -docker build -t gogits/gogs . +$DOCKER_BIN build -t gogits/gogs . #sed -i "s#RUN go get -u -tags $MEM_TYPE github.com/gogits/gogs#RUN go get -u github.com/gogits/gogs#g" Dockerfile @@ -60,9 +66,9 @@ sed -i "s/ -tags $MEM_TYPE//" Dockerfile # ## Run MySQL image with name -docker run -d --name $DB_RUN_NAME gogits/$DB_TYPE +$DOCKER_BIN run -d --name $DB_RUN_NAME gogits/$DB_TYPE # ## Run gogits image and link it to the database image echo "Now we have the $DB_TYPE image(running) and gogs image, use the follow command to start gogs service:" -echo -e "\033[33m docker run -i -t --link $DB_RUN_NAME:db $MEM_LINK -p $HOST_PORT:3000 gogits/gogs \033[0m" +echo -e "\033[33m $DOCKER_BIN run -i -t --link $DB_RUN_NAME:db $MEM_LINK -p $HOST_PORT:3000 gogits/gogs \033[0m" diff --git a/dockerfiles/run.sh b/dockerfiles/run.sh index 7721ab41d4..cef2ebb81f 100755 --- a/dockerfiles/run.sh +++ b/dockerfiles/run.sh @@ -5,9 +5,15 @@ typeset -u MYSQL_ALIAS MYSQL_ALIAS="db" HOST_PORT="3000" +DOCKER_BIN=$(which docker.io || which docker) +if [ -z "$DOCKER_BIN" ] ; then + echo "Please install docker. You can install docker by running \"wget -qO- https://get.docker.io/ | sh\"." + exit 1 +fi + ## Run MySQL image with name -docker run -d --name $MYSQL_RUN_NAME gogs/mysql +$DOCKER_BIN run -d --name $MYSQL_RUN_NAME gogs/mysql # ## Run gogits image and link it to the MySQL image -docker run --link $MYSQL_RUN_NAME:$MYSQL_ALIAS -p $HOST_PORT:3000 gogs/gogits +$DOCKER_BIN run --link $MYSQL_RUN_NAME:$MYSQL_ALIAS -p $HOST_PORT:3000 gogs/gogits diff --git a/gogs.go b/gogs.go index 963cd5878e..3ad059bcc1 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.4.1.0601 Alpha" +const APP_VER = "0.4.5.0628 Alpha" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) @@ -31,10 +31,10 @@ func main() { app.Version = APP_VER app.Commands = []cli.Command{ cmd.CmdWeb, - // cmd.CmdFix, - cmd.CmdDump, cmd.CmdServ, cmd.CmdUpdate, + cmd.CmdFix, + cmd.CmdDump, } app.Flags = append(app.Flags, []cli.Flag{}...) app.Run(os.Args) diff --git a/models/access.go b/models/access.go index 4a202dc6bc..5238daba32 100644 --- a/models/access.go +++ b/models/access.go @@ -11,26 +11,27 @@ import ( "github.com/go-xorm/xorm" ) -// Access types. +type AccessType int + const ( - AU_READABLE = iota + 1 - AU_WRITABLE + READABLE AccessType = iota + 1 + WRITABLE ) // Access represents the accessibility of user to repository. type Access struct { Id int64 - UserName string `xorm:"unique(s)"` - RepoName string `xorm:"unique(s)"` // / - Mode int `xorm:"unique(s)"` - Created time.Time `xorm:"created"` + UserName string `xorm:"unique(s)"` + RepoName string `xorm:"unique(s)"` // / + Mode AccessType `xorm:"unique(s)"` + Created time.Time `xorm:"created"` } // AddAccess adds new access record. func AddAccess(access *Access) error { access.UserName = strings.ToLower(access.UserName) access.RepoName = strings.ToLower(access.RepoName) - _, err := orm.Insert(access) + _, err := x.Insert(access) return err } @@ -38,13 +39,13 @@ func AddAccess(access *Access) error { func UpdateAccess(access *Access) error { access.UserName = strings.ToLower(access.UserName) access.RepoName = strings.ToLower(access.RepoName) - _, err := orm.Id(access.Id).Update(access) + _, err := x.Id(access.Id).Update(access) return err } // DeleteAccess deletes access record. func DeleteAccess(access *Access) error { - _, err := orm.Delete(access) + _, err := x.Delete(access) return err } @@ -59,7 +60,7 @@ func UpdateAccessWithSession(sess *xorm.Session, access *Access) error { // HasAccess returns true if someone can read or write to given repository. // The repoName should be in format /. -func HasAccess(uname, repoName string, mode int) (bool, error) { +func HasAccess(uname, repoName string, mode AccessType) (bool, error) { if len(repoName) == 0 { return false, nil } @@ -67,7 +68,7 @@ func HasAccess(uname, repoName string, mode int) (bool, error) { UserName: strings.ToLower(uname), RepoName: strings.ToLower(repoName), } - has, err := orm.Get(access) + has, err := x.Get(access) if err != nil { return false, err } else if !has { diff --git a/models/action.go b/models/action.go index 9fc9d89b9f..55557da2ff 100644 --- a/models/action.go +++ b/models/action.go @@ -12,10 +12,8 @@ import ( "time" "github.com/gogits/git" - qlog "github.com/qiniu/log" "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/hooks" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" ) @@ -116,7 +114,7 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, return errors.New("action.CommitRepoAction(NotifyWatchers): " + err.Error()) } - qlog.Info("action.CommitRepoAction(end): %d/%s", repoUserId, repoName) + //qlog.Info("action.CommitRepoAction(end): %d/%s", repoUserId, repoName) // New push event hook. if err := repo.GetOwner(); err != nil { @@ -131,35 +129,35 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, } repoLink := fmt.Sprintf("%s%s/%s", setting.AppUrl, repoUserName, repoName) - commits := make([]*hooks.PayloadCommit, len(commit.Commits)) + commits := make([]*PayloadCommit, len(commit.Commits)) for i, cmt := range commit.Commits { - commits[i] = &hooks.PayloadCommit{ + commits[i] = &PayloadCommit{ Id: cmt.Sha1, Message: cmt.Message, Url: fmt.Sprintf("%s/commit/%s", repoLink, cmt.Sha1), - Author: &hooks.PayloadAuthor{ + Author: &PayloadAuthor{ Name: cmt.AuthorName, Email: cmt.AuthorEmail, }, } } - p := &hooks.Payload{ + p := &Payload{ Ref: refFullName, Commits: commits, - Repo: &hooks.PayloadRepo{ + Repo: &PayloadRepo{ Id: repo.Id, Name: repo.LowerName, Url: repoLink, Description: repo.Description, Website: repo.Website, Watchers: repo.NumWatches, - Owner: &hooks.PayloadAuthor{ + Owner: &PayloadAuthor{ Name: repoUserName, Email: actEmail, }, Private: repo.IsPrivate, }, - Pusher: &hooks.PayloadAuthor{ + Pusher: &PayloadAuthor{ Name: repo.Owner.LowerName, Email: repo.Owner.Email, }, @@ -172,20 +170,27 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, } p.Secret = w.Secret - hooks.AddHookTask(&hooks.HookTask{hooks.HTT_WEBHOOK, w.Url, p, w.ContentType, w.IsSsl}) + CreateHookTask(&HookTask{ + Type: WEBHOOK, + Url: w.Url, + Payload: p, + ContentType: w.ContentType, + IsSsl: w.IsSsl, + }) } return nil } // NewRepoAction adds new action for creating repository. -func NewRepoAction(user *User, repo *Repository) (err error) { - if err = NotifyWatchers(&Action{ActUserId: user.Id, ActUserName: user.Name, ActEmail: user.Email, - OpType: OP_CREATE_REPO, RepoId: repo.Id, RepoName: repo.Name, IsPrivate: repo.IsPrivate}); err != nil { - log.Error("action.NewRepoAction(notify watchers): %d/%s", user.Id, repo.Name) +func NewRepoAction(u *User, repo *Repository) (err error) { + if err = NotifyWatchers(&Action{ActUserId: u.Id, ActUserName: u.Name, ActEmail: u.Email, + OpType: OP_CREATE_REPO, RepoId: repo.Id, RepoUserName: repo.Owner.Name, RepoName: repo.Name, + IsPrivate: repo.IsPrivate}); err != nil { + log.Error("action.NewRepoAction(notify watchers): %d/%s", u.Id, repo.Name) return err } - log.Trace("action.NewRepoAction: %s/%s", user.LowerName, repo.LowerName) + log.Trace("action.NewRepoAction: %s/%s", u.LowerName, repo.LowerName) return err } @@ -205,7 +210,7 @@ func TransferRepoAction(user, newUser *User, repo *Repository) (err error) { // GetFeeds returns action list of given user in given context. func GetFeeds(userid, offset int64, isProfile bool) ([]*Action, error) { actions := make([]*Action, 0, 20) - sess := orm.Limit(20, int(offset)).Desc("id").Where("user_id=?", userid) + sess := x.Limit(20, int(offset)).Desc("id").Where("user_id=?", userid) if isProfile { sess.Where("is_private=?", false).And("act_user_id=?", userid) } else { diff --git a/models/fix.go b/models/fix.go deleted file mode 100644 index 9fc141bd26..0000000000 --- a/models/fix.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -func Fix() error { - _, err := orm.Exec("alter table repository drop column num_releases") - return err -} diff --git a/models/git_diff.go b/models/git_diff.go index 5b5a46a120..ed114b7504 100644 --- a/models/git_diff.go +++ b/models/git_diff.go @@ -6,6 +6,7 @@ package models import ( "bufio" + "fmt" "io" "os" "os/exec" @@ -15,6 +16,7 @@ import ( "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/process" ) // Diff line types. @@ -67,7 +69,7 @@ func (diff *Diff) NumFiles() int { const DIFF_HEAD = "diff --git " -func ParsePatch(cmd *exec.Cmd, reader io.Reader) (*Diff, error) { +func ParsePatch(pid int64, cmd *exec.Cmd, reader io.Reader) (*Diff, error) { scanner := bufio.NewScanner(reader) var ( curFile *DiffFile @@ -169,11 +171,8 @@ func ParsePatch(cmd *exec.Cmd, reader io.Reader) (*Diff, error) { } // In case process became zombie. - if !cmd.ProcessState.Exited() { - log.Debug("git_diff.ParsePatch: process doesn't exit and now will be killed") - if err := cmd.Process.Kill(); err != nil { - log.Error("git_diff.ParsePatch: fail to kill zombie process: %v", err) - } + if err := process.Kill(pid); err != nil { + log.Error("git_diff.ParsePatch(Kill): %v", err) } return diff, nil } @@ -207,5 +206,5 @@ func GetDiff(repoPath, commitid string) (*Diff, error) { wr.Close() }() defer rd.Close() - return ParsePatch(cmd, rd) + return ParsePatch(process.Add(fmt.Sprintf("GetDiff(%s)", repoPath), cmd), cmd, rd) } diff --git a/models/issue.go b/models/issue.go index 18057985f0..6d67a72bc4 100644 --- a/models/issue.go +++ b/models/issue.go @@ -92,7 +92,7 @@ func (i *Issue) GetAssignee() (err error) { // CreateIssue creates new issue for repository. func NewIssue(issue *Issue) (err error) { - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err @@ -114,7 +114,7 @@ func NewIssue(issue *Issue) (err error) { // GetIssueByIndex returns issue by given index in repository. func GetIssueByIndex(rid, index int64) (*Issue, error) { issue := &Issue{RepoId: rid, Index: index} - has, err := orm.Get(issue) + has, err := x.Get(issue) if err != nil { return nil, err } else if !has { @@ -126,7 +126,7 @@ func GetIssueByIndex(rid, index int64) (*Issue, error) { // GetIssueById returns an issue by ID. func GetIssueById(id int64) (*Issue, error) { issue := &Issue{Id: id} - has, err := orm.Get(issue) + has, err := x.Get(issue) if err != nil { return nil, err } else if !has { @@ -137,7 +137,7 @@ func GetIssueById(id int64) (*Issue, error) { // GetIssues returns a list of issues by given conditions. func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labelIds, sortType string) ([]Issue, error) { - sess := orm.Limit(20, (page-1)*20) + sess := x.Limit(20, (page-1)*20) if rid > 0 { sess.Where("repo_id=?", rid).And("is_closed=?", isClosed) @@ -193,13 +193,13 @@ const ( // GetIssuesByLabel returns a list of issues by given label and repository. func GetIssuesByLabel(repoId int64, label string) ([]*Issue, error) { issues := make([]*Issue, 0, 10) - err := orm.Where("repo_id=?", repoId).And("label_ids like '%$" + label + "|%'").Find(&issues) + err := x.Where("repo_id=?", repoId).And("label_ids like '%$" + label + "|%'").Find(&issues) return issues, err } // GetIssueCountByPoster returns number of issues of repository by poster. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 { - count, _ := orm.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue)) + count, _ := x.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue)) return count } @@ -213,9 +213,9 @@ func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 { // IssueUser represents an issue-user relation. type IssueUser struct { Id int64 - Uid int64 // User ID. + Uid int64 `xorm:"INDEX"` // User ID. IssueId int64 - RepoId int64 + RepoId int64 `xorm:"INDEX"` MilestoneId int64 IsRead bool IsAssigned bool @@ -241,7 +241,7 @@ func NewIssueUserPairs(rid, iid, oid, pid, aid int64, repoName string) (err erro isNeedAddPoster = false } iu.IsAssigned = iu.Uid == aid - if _, err = orm.Insert(iu); err != nil { + if _, err = x.Insert(iu); err != nil { return err } } @@ -249,7 +249,7 @@ func NewIssueUserPairs(rid, iid, oid, pid, aid int64, repoName string) (err erro iu.Uid = pid iu.IsPoster = true iu.IsAssigned = iu.Uid == aid - if _, err = orm.Insert(iu); err != nil { + if _, err = x.Insert(iu); err != nil { return err } } @@ -270,7 +270,7 @@ func PairsContains(ius []*IssueUser, issueId int64) int { // GetIssueUserPairs returns issue-user pairs by given repository and user. func GetIssueUserPairs(rid, uid int64, isClosed bool) ([]*IssueUser, error) { ius := make([]*IssueUser, 0, 10) - err := orm.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoId: rid, Uid: uid}) + err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoId: rid, Uid: uid}) return ius, err } @@ -285,7 +285,7 @@ func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*Issue cond := strings.TrimSuffix(buf.String(), " OR ") ius := make([]*IssueUser, 0, 10) - sess := orm.Limit(20, (page-1)*20).Where("is_closed=?", isClosed) + sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed) if len(cond) > 0 { sess.And(cond) } @@ -296,7 +296,7 @@ func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*Issue // GetIssueUserPairsByMode returns issue-user pairs by given repository and user. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) { ius := make([]*IssueUser, 0, 10) - sess := orm.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed) + sess := x.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed) if rid > 0 { sess.And("repo_id=?", rid) } @@ -335,7 +335,7 @@ func GetIssueStats(rid, uid int64, isShowClosed bool, filterMode int) *IssueStat issue := new(Issue) tmpSess := &xorm.Session{} - sess := orm.Where("repo_id=?", rid) + sess := x.Where("repo_id=?", rid) *tmpSess = *sess stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue) *tmpSess = *sess @@ -347,7 +347,7 @@ func GetIssueStats(rid, uid int64, isShowClosed bool, filterMode int) *IssueStat } if filterMode != FM_MENTION { - sess = orm.Where("repo_id=?", rid) + sess = x.Where("repo_id=?", rid) switch filterMode { case FM_ASSIGN: sess.And("assignee_id=?", uid) @@ -361,16 +361,16 @@ func GetIssueStats(rid, uid int64, isShowClosed bool, filterMode int) *IssueStat *tmpSess = *sess stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue) } else { - sess := orm.Where("repo_id=?", rid).And("uid=?", uid).And("is_mentioned=?", true) + sess := x.Where("repo_id=?", rid).And("uid=?", uid).And("is_mentioned=?", true) *tmpSess = *sess stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(new(IssueUser)) *tmpSess = *sess stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(new(IssueUser)) } nofilter: - stats.AssignCount, _ = orm.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("assignee_id=?", uid).Count(issue) - stats.CreateCount, _ = orm.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("poster_id=?", uid).Count(issue) - stats.MentionCount, _ = orm.Where("repo_id=?", rid).And("uid=?", uid).And("is_closed=?", isShowClosed).And("is_mentioned=?", true).Count(new(IssueUser)) + stats.AssignCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("assignee_id=?", uid).Count(issue) + stats.CreateCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("poster_id=?", uid).Count(issue) + stats.MentionCount, _ = x.Where("repo_id=?", rid).And("uid=?", uid).And("is_closed=?", isShowClosed).And("is_mentioned=?", true).Count(new(IssueUser)) return stats } @@ -378,28 +378,28 @@ nofilter: func GetUserIssueStats(uid int64, filterMode int) *IssueStats { stats := &IssueStats{} issue := new(Issue) - stats.AssignCount, _ = orm.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue) - stats.CreateCount, _ = orm.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue) + stats.AssignCount, _ = x.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue) + stats.CreateCount, _ = x.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue) return stats } // UpdateIssue updates information of issue. func UpdateIssue(issue *Issue) error { - _, err := orm.Id(issue.Id).AllCols().Update(issue) + _, err := x.Id(issue.Id).AllCols().Update(issue) return err } // UpdateIssueUserByStatus updates issue-user pairs by issue status. func UpdateIssueUserPairsByStatus(iid int64, isClosed bool) error { rawSql := "UPDATE `issue_user` SET is_closed = ? WHERE issue_id = ?" - _, err := orm.Exec(rawSql, isClosed, iid) + _, err := x.Exec(rawSql, isClosed, iid) return err } // UpdateIssueUserPairByAssignee updates issue-user pair for assigning. func UpdateIssueUserPairByAssignee(aid, iid int64) error { rawSql := "UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?" - if _, err := orm.Exec(rawSql, false, iid); err != nil { + if _, err := x.Exec(rawSql, false, iid); err != nil { return err } @@ -408,14 +408,14 @@ func UpdateIssueUserPairByAssignee(aid, iid int64) error { return nil } rawSql = "UPDATE `issue_user` SET is_assigned = true WHERE uid = ? AND issue_id = ?" - _, err := orm.Exec(rawSql, aid, iid) + _, err := x.Exec(rawSql, aid, iid) return err } // UpdateIssueUserPairByRead updates issue-user pair for reading. func UpdateIssueUserPairByRead(uid, iid int64) error { rawSql := "UPDATE `issue_user` SET is_read = ? WHERE uid = ? AND issue_id = ?" - _, err := orm.Exec(rawSql, true, uid, iid) + _, err := x.Exec(rawSql, true, uid, iid) return err } @@ -423,16 +423,16 @@ func UpdateIssueUserPairByRead(uid, iid int64) error { func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { for _, uid := range uids { iu := &IssueUser{Uid: uid, IssueId: iid} - has, err := orm.Get(iu) + has, err := x.Get(iu) if err != nil { return err } iu.IsMentioned = true if has { - _, err = orm.Id(iu.Id).AllCols().Update(iu) + _, err = x.Id(iu.Id).AllCols().Update(iu) } else { - _, err = orm.Insert(iu) + _, err = x.Insert(iu) } if err != nil { return err @@ -467,7 +467,7 @@ func (m *Label) CalOpenIssues() { // NewLabel creates new label of repository. func NewLabel(l *Label) error { - _, err := orm.Insert(l) + _, err := x.Insert(l) return err } @@ -478,7 +478,7 @@ func GetLabelById(id int64) (*Label, error) { } l := &Label{Id: id} - has, err := orm.Get(l) + has, err := x.Get(l) if err != nil { return nil, err } else if !has { @@ -490,13 +490,13 @@ func GetLabelById(id int64) (*Label, error) { // GetLabels returns a list of labels of given repository ID. func GetLabels(repoId int64) ([]*Label, error) { labels := make([]*Label, 0, 10) - err := orm.Where("repo_id=?", repoId).Find(&labels) + err := x.Where("repo_id=?", repoId).Find(&labels) return labels, err } // UpdateLabel updates label information. func UpdateLabel(l *Label) error { - _, err := orm.Id(l.Id).Update(l) + _, err := x.Id(l.Id).Update(l) return err } @@ -516,7 +516,7 @@ func DeleteLabel(repoId int64, strId string) error { return err } - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err @@ -569,7 +569,7 @@ func (m *Milestone) CalOpenIssues() { // NewMilestone creates new milestone of repository. func NewMilestone(m *Milestone) (err error) { - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err @@ -591,7 +591,7 @@ func NewMilestone(m *Milestone) (err error) { // GetMilestoneById returns the milestone by given ID. func GetMilestoneById(id int64) (*Milestone, error) { m := &Milestone{Id: id} - has, err := orm.Get(m) + has, err := x.Get(m) if err != nil { return nil, err } else if !has { @@ -603,7 +603,7 @@ func GetMilestoneById(id int64) (*Milestone, error) { // GetMilestoneByIndex returns the milestone of given repository and index. func GetMilestoneByIndex(repoId, idx int64) (*Milestone, error) { m := &Milestone{RepoId: repoId, Index: idx} - has, err := orm.Get(m) + has, err := x.Get(m) if err != nil { return nil, err } else if !has { @@ -615,13 +615,13 @@ func GetMilestoneByIndex(repoId, idx int64) (*Milestone, error) { // GetMilestones returns a list of milestones of given repository and status. func GetMilestones(repoId int64, isClosed bool) ([]*Milestone, error) { miles := make([]*Milestone, 0, 10) - err := orm.Where("repo_id=?", repoId).And("is_closed=?", isClosed).Find(&miles) + err := x.Where("repo_id=?", repoId).And("is_closed=?", isClosed).Find(&miles) return miles, err } // UpdateMilestone updates information of given milestone. func UpdateMilestone(m *Milestone) error { - _, err := orm.Id(m.Id).Update(m) + _, err := x.Id(m.Id).Update(m) return err } @@ -632,7 +632,7 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { return err } - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err @@ -658,7 +658,7 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { // ChangeMilestoneAssign changes assignment of milestone for issue. func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err @@ -717,7 +717,7 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { // DeleteMilestone deletes a milestone. func DeleteMilestone(m *Milestone) (err error) { - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err @@ -771,13 +771,13 @@ type Comment struct { IssueId int64 CommitId int64 Line int64 - Content string + Content string `xorm:"TEXT"` Created time.Time `xorm:"CREATED"` } // CreateComment creates comment of issue or commit. func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error { - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err @@ -816,6 +816,6 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, c // GetIssueComments returns list of comment by given issue id. func GetIssueComments(issueId int64) ([]Comment, error) { comments := make([]Comment, 0, 10) - err := orm.Asc("created").Find(&comments, &Comment{IssueId: issueId}) + err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId}) return comments, err } diff --git a/models/login.go b/models/login.go index 3efef2f78f..e99b61e779 100644 --- a/models/login.go +++ b/models/login.go @@ -1,4 +1,4 @@ -// Copyright github.com/juju2013. All rights reserved. +// Copyright 2014 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -20,12 +20,13 @@ import ( "github.com/gogits/gogs/modules/log" ) -// Login types. +type LoginType int + const ( - LT_NOTYPE = iota - LT_PLAIN - LT_LDAP - LT_SMTP + NOTYPE LoginType = iota + PLAIN + LDAP + SMTP ) var ( @@ -34,9 +35,9 @@ var ( ErrAuthenticationUserUsed = errors.New("Authentication has been used by some users") ) -var LoginTypes = map[int]string{ - LT_LDAP: "LDAP", - LT_SMTP: "SMTP", +var LoginTypes = map[LoginType]string{ + LDAP: "LDAP", + SMTP: "SMTP", } // Ensure structs implmented interface. @@ -49,7 +50,6 @@ type LDAPConfig struct { ldap.Ldapsource } -// implement func (cfg *LDAPConfig) FromDB(bs []byte) error { return json.Unmarshal(bs, &cfg.Ldapsource) } @@ -65,7 +65,6 @@ type SMTPConfig struct { TLS bool } -// implement func (cfg *SMTPConfig) FromDB(bs []byte) error { return json.Unmarshal(bs, cfg) } @@ -76,13 +75,13 @@ func (cfg *SMTPConfig) ToDB() ([]byte, error) { type LoginSource struct { Id int64 - Type int - Name string `xorm:"unique"` - IsActived bool `xorm:"not null default false"` + Type LoginType + Name string `xorm:"UNIQUE"` + IsActived bool `xorm:"NOT NULL DEFAULT false"` Cfg core.Conversion `xorm:"TEXT"` - Created time.Time `xorm:"created"` - Updated time.Time `xorm:"updated"` - AllowAutoRegister bool `xorm:"not null default false"` + AllowAutoRegister bool `xorm:"NOT NULL DEFAULT false"` + Created time.Time `xorm:"CREATED"` + Updated time.Time `xorm:"UPDATED"` } func (source *LoginSource) TypeString() string { @@ -97,61 +96,59 @@ func (source *LoginSource) SMTP() *SMTPConfig { return source.Cfg.(*SMTPConfig) } -// for xorm callback func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { if colName == "type" { ty := (*val).(int64) - switch ty { - case LT_LDAP: + switch LoginType(ty) { + case LDAP: source.Cfg = new(LDAPConfig) - case LT_SMTP: + case SMTP: source.Cfg = new(SMTPConfig) } } } +func CreateSource(source *LoginSource) error { + _, err := x.Insert(source) + return err +} + func GetAuths() ([]*LoginSource, error) { - var auths = make([]*LoginSource, 0) - err := orm.Find(&auths) + var auths = make([]*LoginSource, 0, 5) + err := x.Find(&auths) return auths, err } func GetLoginSourceById(id int64) (*LoginSource, error) { source := new(LoginSource) - has, err := orm.Id(id).Get(source) + has, err := x.Id(id).Get(source) if err != nil { return nil, err - } - if !has { + } else if !has { return nil, ErrAuthenticationNotExist } return source, nil } -func AddSource(source *LoginSource) error { - _, err := orm.Insert(source) - return err -} - func UpdateSource(source *LoginSource) error { - _, err := orm.Id(source.Id).AllCols().Update(source) + _, err := x.Id(source.Id).AllCols().Update(source) return err } func DelLoginSource(source *LoginSource) error { - cnt, err := orm.Count(&User{LoginSource: source.Id}) + cnt, err := x.Count(&User{LoginSource: source.Id}) if err != nil { return err } if cnt > 0 { return ErrAuthenticationUserUsed } - _, err = orm.Id(source.Id).Delete(&LoginSource{}) + _, err = x.Id(source.Id).Delete(&LoginSource{}) return err } -// login a user -func LoginUser(uname, passwd string) (*User, error) { +// UserSignIn validates user name and password. +func UserSignIn(uname, passwd string) (*User, error) { var u *User if strings.Contains(uname, "@") { u = &User{Email: uname} @@ -159,19 +156,19 @@ func LoginUser(uname, passwd string) (*User, error) { u = &User{LowerName: strings.ToLower(uname)} } - has, err := orm.Get(u) + has, err := x.Get(u) if err != nil { return nil, err } - if u.LoginType == LT_NOTYPE { + if u.LoginType == NOTYPE { if has { - u.LoginType = LT_PLAIN + u.LoginType = PLAIN } } // for plain login, user must have existed. - if u.LoginType == LT_PLAIN { + if u.LoginType == PLAIN { if !has { return nil, ErrUserNotExist } @@ -185,28 +182,26 @@ func LoginUser(uname, passwd string) (*User, error) { } else { if !has { var sources []LoginSource - if err = orm.UseBool().Find(&sources, + if err = x.UseBool().Find(&sources, &LoginSource{IsActived: true, AllowAutoRegister: true}); err != nil { return nil, err } for _, source := range sources { - if source.Type == LT_LDAP { + if source.Type == LDAP { u, err := LoginUserLdapSource(nil, uname, passwd, source.Id, source.Cfg.(*LDAPConfig), true) if err == nil { return u, nil - } else { - log.Warn("Fail to login(%s) by LDAP(%s): %v", uname, source.Name, err) } - } else if source.Type == LT_SMTP { + log.Warn("Fail to login(%s) by LDAP(%s): %v", uname, source.Name, err) + } else if source.Type == SMTP { u, err := LoginUserSMTPSource(nil, uname, passwd, source.Id, source.Cfg.(*SMTPConfig), true) if err == nil { return u, nil - } else { - log.Warn("Fail to login(%s) by SMTP(%s): %v", uname, source.Name, err) } + log.Warn("Fail to login(%s) by SMTP(%s): %v", uname, source.Name, err) } } @@ -214,7 +209,7 @@ func LoginUser(uname, passwd string) (*User, error) { } var source LoginSource - hasSource, err := orm.Id(u.LoginSource).Get(&source) + hasSource, err := x.Id(u.LoginSource).Get(&source) if err != nil { return nil, err } else if !hasSource { @@ -224,10 +219,10 @@ func LoginUser(uname, passwd string) (*User, error) { } switch u.LoginType { - case LT_LDAP: + case LDAP: return LoginUserLdapSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*LDAPConfig), false) - case LT_SMTP: + case SMTP: return LoginUserSMTPSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*SMTPConfig), false) } @@ -252,7 +247,7 @@ func LoginUserLdapSource(user *User, name, passwd string, sourceId int64, cfg *L user = &User{ LowerName: strings.ToLower(name), Name: strings.ToLower(name), - LoginType: LT_LDAP, + LoginType: LDAP, LoginSource: sourceId, LoginName: name, IsActive: true, @@ -260,7 +255,7 @@ func LoginUserLdapSource(user *User, name, passwd string, sourceId int64, cfg *L Email: mail, } - return RegisterUser(user) + return CreateUser(user) } type loginAuth struct { @@ -320,9 +315,8 @@ func SmtpAuth(host string, port int, a smtp.Auth, useTls bool) error { return err } return nil - } else { - return ErrUnsupportedLoginType } + return ErrUnsupportedLoginType } // Query if name/passwd can login against the LDAP direcotry pool @@ -358,13 +352,12 @@ func LoginUserSMTPSource(user *User, name, passwd string, sourceId int64, cfg *S user = &User{ LowerName: strings.ToLower(loginName), Name: strings.ToLower(loginName), - LoginType: LT_SMTP, + LoginType: SMTP, LoginSource: sourceId, LoginName: name, IsActive: true, Passwd: passwd, Email: name, } - - return RegisterUser(user) + return CreateUser(user) } diff --git a/models/models.go b/models/models.go index fa65ef30f6..4e65c00bcb 100644 --- a/models/models.go +++ b/models/models.go @@ -18,7 +18,7 @@ import ( ) var ( - orm *xorm.Engine + x *xorm.Engine tables []interface{} HasEngine bool @@ -35,7 +35,7 @@ func init() { tables = append(tables, new(User), new(PublicKey), new(Repository), new(Watch), new(Action), new(Access), new(Issue), new(Comment), new(Oauth2), new(Follow), new(Mirror), new(Release), new(LoginSource), new(Webhook), new(IssueUser), - new(Milestone), new(Label)) + new(Milestone), new(Label), new(HookTask), new(Team), new(OrgUser), new(TeamUser)) } func LoadModelsConfig() { @@ -46,7 +46,9 @@ func LoadModelsConfig() { DbCfg.Host = setting.Cfg.MustValue("database", "HOST") DbCfg.Name = setting.Cfg.MustValue("database", "NAME") DbCfg.User = setting.Cfg.MustValue("database", "USER") - DbCfg.Pwd = setting.Cfg.MustValue("database", "PASSWD") + if len(DbCfg.Pwd) == 0 { + DbCfg.Pwd = setting.Cfg.MustValue("database", "PASSWD") + } DbCfg.SslMode = setting.Cfg.MustValue("database", "SSL_MODE") DbCfg.Path = setting.Cfg.MustValue("database", "PATH", "data/gogs.db") } @@ -67,7 +69,6 @@ func NewTestEngine(x *xorm.Engine) (err error) { } cnnstr := fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s", DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode) - //fmt.Println(cnnstr) x, err = xorm.NewEngine("postgres", cnnstr) case "sqlite3": if !EnableSQLite3 { @@ -87,7 +88,7 @@ func NewTestEngine(x *xorm.Engine) (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", + x, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8", DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name)) case "postgres": var host, port = "127.0.0.1", "5432" @@ -98,11 +99,11 @@ func SetEngine() (err error) { if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 { port = fields[1] } - orm, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s", + x, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s", DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)) case "sqlite3": os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm) - orm, err = xorm.NewEngine("sqlite3", DbCfg.Path) + x, err = xorm.NewEngine("sqlite3", DbCfg.Path) default: return fmt.Errorf("Unknown database type: %s", DbCfg.Type) } @@ -119,11 +120,11 @@ func SetEngine() (err error) { if err != nil { return fmt.Errorf("models.init(fail to create xorm.log): %v", err) } - orm.Logger = xorm.NewSimpleLogger(f) + x.Logger = xorm.NewSimpleLogger(f) - orm.ShowSQL = true - orm.ShowDebug = true - orm.ShowErr = true + x.ShowSQL = true + x.ShowDebug = true + x.ShowErr = true return nil } @@ -131,7 +132,7 @@ func NewEngine() (err error) { if err = SetEngine(); err != nil { return err } - if err = orm.Sync(tables...); err != nil { + if err = x.Sync2(tables...); err != nil { return fmt.Errorf("sync database struct error: %v\n", err) } return nil @@ -146,24 +147,24 @@ type Statistic struct { } func GetStatistic() (stats Statistic) { - stats.Counter.User, _ = orm.Count(new(User)) - stats.Counter.PublicKey, _ = orm.Count(new(PublicKey)) - stats.Counter.Repo, _ = orm.Count(new(Repository)) - stats.Counter.Watch, _ = orm.Count(new(Watch)) - stats.Counter.Action, _ = orm.Count(new(Action)) - stats.Counter.Access, _ = orm.Count(new(Access)) - stats.Counter.Issue, _ = orm.Count(new(Issue)) - stats.Counter.Comment, _ = orm.Count(new(Comment)) - stats.Counter.Mirror, _ = orm.Count(new(Mirror)) - stats.Counter.Oauth, _ = orm.Count(new(Oauth2)) - stats.Counter.Release, _ = orm.Count(new(Release)) - stats.Counter.LoginSource, _ = orm.Count(new(LoginSource)) - stats.Counter.Webhook, _ = orm.Count(new(Webhook)) - stats.Counter.Milestone, _ = orm.Count(new(Milestone)) + stats.Counter.User, _ = x.Count(new(User)) + stats.Counter.PublicKey, _ = x.Count(new(PublicKey)) + stats.Counter.Repo, _ = x.Count(new(Repository)) + stats.Counter.Watch, _ = x.Count(new(Watch)) + stats.Counter.Action, _ = x.Count(new(Action)) + stats.Counter.Access, _ = x.Count(new(Access)) + stats.Counter.Issue, _ = x.Count(new(Issue)) + stats.Counter.Comment, _ = x.Count(new(Comment)) + stats.Counter.Mirror, _ = x.Count(new(Mirror)) + stats.Counter.Oauth, _ = x.Count(new(Oauth2)) + stats.Counter.Release, _ = x.Count(new(Release)) + stats.Counter.LoginSource, _ = x.Count(new(LoginSource)) + stats.Counter.Webhook, _ = x.Count(new(Webhook)) + stats.Counter.Milestone, _ = x.Count(new(Milestone)) return } // DumpDatabase dumps all data from database to file system. func DumpDatabase(filePath string) error { - return orm.DumpAllToFile(filePath) + return x.DumpAllToFile(filePath) } diff --git a/models/oauth2.go b/models/oauth2.go index 61044d6882..4b024a26e4 100644 --- a/models/oauth2.go +++ b/models/oauth2.go @@ -8,16 +8,16 @@ import ( "errors" ) -// OT: Oauth2 Type +type OauthType int + const ( - OT_GITHUB = iota + 1 - OT_GOOGLE - OT_TWITTER - OT_QQ - OT_WEIBO - OT_BITBUCKET - OT_OSCHINA - OT_FACEBOOK + GITHUB OauthType = iota + 1 + GOOGLE + TWITTER + QQ + WEIBO + BITBUCKET + FACEBOOK ) var ( @@ -35,18 +35,18 @@ type Oauth2 struct { } func BindUserOauth2(userId, oauthId int64) error { - _, err := orm.Id(oauthId).Update(&Oauth2{Uid: userId}) + _, err := x.Id(oauthId).Update(&Oauth2{Uid: userId}) return err } func AddOauth2(oa *Oauth2) error { - _, err := orm.Insert(oa) + _, err := x.Insert(oa) return err } func GetOauth2(identity string) (oa *Oauth2, err error) { oa = &Oauth2{Identity: identity} - isExist, err := orm.Get(oa) + isExist, err := x.Get(oa) if err != nil { return } else if !isExist { @@ -60,7 +60,7 @@ func GetOauth2(identity string) (oa *Oauth2, err error) { func GetOauth2ById(id int64) (oa *Oauth2, err error) { oa = new(Oauth2) - has, err := orm.Id(id).Get(oa) + has, err := x.Id(id).Get(oa) if err != nil { return nil, err } else if !has { @@ -71,18 +71,18 @@ func GetOauth2ById(id int64) (oa *Oauth2, err error) { // GetOauthByUserId returns list of oauthes that are releated to given user. func GetOauthByUserId(uid int64) (oas []*Oauth2, err error) { - err = orm.Find(&oas, Oauth2{Uid: uid}) + err = x.Find(&oas, Oauth2{Uid: uid}) return oas, err } // DeleteOauth2ById deletes a oauth2 by ID. func DeleteOauth2ById(id int64) error { - _, err := orm.Delete(&Oauth2{Id: id}) + _, err := x.Delete(&Oauth2{Id: id}) return err } // CleanUnbindOauth deletes all unbind OAuthes. func CleanUnbindOauth() error { - _, err := orm.Delete(&Oauth2{Uid: -1}) + _, err := x.Delete(&Oauth2{Uid: -1}) return err } diff --git a/models/org.go b/models/org.go new file mode 100644 index 0000000000..025759b001 --- /dev/null +++ b/models/org.go @@ -0,0 +1,236 @@ +// 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 ( + "strings" + + "github.com/gogits/gogs/modules/base" +) + +// GetOwnerTeam returns owner team of organization. +func (org *User) GetOwnerTeam() (*Team, error) { + t := &Team{ + OrgId: org.Id, + Name: OWNER_TEAM, + } + _, err := x.Get(t) + return t, err +} + +// CreateOrganization creates record of a new organization. +func CreateOrganization(org, owner *User) (*User, error) { + if !IsLegalName(org.Name) { + return nil, ErrUserNameIllegal + } + + isExist, err := IsUserExist(org.Name) + if err != nil { + return nil, err + } else if isExist { + return nil, ErrUserAlreadyExist + } + + isExist, err = IsEmailUsed(org.Email) + if err != nil { + return nil, err + } else if isExist { + return nil, ErrEmailAlreadyUsed + } + + org.LowerName = strings.ToLower(org.Name) + org.FullName = org.Name + org.Avatar = base.EncodeMd5(org.Email) + org.AvatarEmail = org.Email + // No password for organization. + org.NumTeams = 1 + org.NumMembers = 1 + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return nil, err + } + + if _, err = sess.Insert(org); err != nil { + sess.Rollback() + return nil, err + } + + // Create default owner team. + t := &Team{ + OrgId: org.Id, + Name: OWNER_TEAM, + Authorize: ORG_ADMIN, + NumMembers: 1, + } + if _, err = sess.Insert(t); err != nil { + sess.Rollback() + return nil, err + } + + // Add initial creator to organization and owner team. + ou := &OrgUser{ + Uid: owner.Id, + OrgId: org.Id, + IsOwner: true, + NumTeam: 1, + } + if _, err = sess.Insert(ou); err != nil { + sess.Rollback() + return nil, err + } + + tu := &TeamUser{ + Uid: owner.Id, + OrgId: org.Id, + TeamId: t.Id, + } + if _, err = sess.Insert(tu); err != nil { + sess.Rollback() + return nil, err + } + + return org, sess.Commit() +} + +// TODO: need some kind of mechanism to record failure. +// DeleteOrganization completely and permanently deletes everything of organization. +func DeleteOrganization(org *User) (err error) { + if err := DeleteUser(org); err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Delete(&Team{OrgId: org.Id}); err != nil { + sess.Rollback() + return err + } + if _, err = sess.Delete(&OrgUser{OrgId: org.Id}); err != nil { + sess.Rollback() + return err + } + if _, err = sess.Delete(&TeamUser{OrgId: org.Id}); err != nil { + sess.Rollback() + return err + } + return sess.Commit() +} + +type AuthorizeType int + +const ( + ORG_READABLE AuthorizeType = iota + 1 + ORG_WRITABLE + ORG_ADMIN +) + +const OWNER_TEAM = "Owner" + +// Team represents a organization team. +type Team struct { + Id int64 + OrgId int64 `xorm:"INDEX"` + Name string + Description string + Authorize AuthorizeType + RepoIds string `xorm:"TEXT"` + NumMembers int + NumRepos int +} + +// NewTeam creates a record of new team. +func NewTeam(t *Team) error { + _, err := x.Insert(t) + return err +} + +func UpdateTeam(t *Team) error { + if len(t.Description) > 255 { + t.Description = t.Description[:255] + } + + _, err := x.Id(t.Id).AllCols().Update(t) + return err +} + +// ________ ____ ___ +// \_____ \_______ ____ | | \______ ___________ +// / | \_ __ \/ ___\| | / ___// __ \_ __ \ +// / | \ | \/ /_/ > | /\___ \\ ___/| | \/ +// \_______ /__| \___ /|______//____ >\___ >__| +// \/ /_____/ \/ \/ + +// OrgUser represents an organization-user relation. +type OrgUser struct { + Id int64 + Uid int64 `xorm:"INDEX"` + OrgId int64 `xorm:"INDEX"` + IsPublic bool + IsOwner bool + NumTeam int +} + +// GetOrgUsersByUserId returns all organization-user relations by user ID. +func GetOrgUsersByUserId(uid int64) ([]*OrgUser, error) { + ous := make([]*OrgUser, 0, 10) + err := x.Where("uid=?", uid).Find(&ous) + return ous, err +} + +// GetOrgUsersByOrgId returns all organization-user relations by organization ID. +func GetOrgUsersByOrgId(orgId int64) ([]*OrgUser, error) { + ous := make([]*OrgUser, 0, 10) + err := x.Where("org_id=?", orgId).Find(&ous) + return ous, err +} + +func GetOrganizationCount(u *User) (int64, error) { + return x.Where("uid=?", u.Id).Count(new(OrgUser)) +} + +// IsOrganizationOwner returns true if given user ID is in the owner team. +func IsOrganizationOwner(orgId, uid int64) bool { + has, _ := x.Where("is_owner=?", true).Get(&OrgUser{Uid: uid, OrgId: orgId}) + return has +} + +// ___________ ____ ___ +// \__ ___/___ _____ _____ | | \______ ___________ +// | |_/ __ \\__ \ / \| | / ___// __ \_ __ \ +// | |\ ___/ / __ \| Y Y \ | /\___ \\ ___/| | \/ +// |____| \___ >____ /__|_| /______//____ >\___ >__| +// \/ \/ \/ \/ \/ + +// TeamUser represents an team-user relation. +type TeamUser struct { + Id int64 + Uid int64 + OrgId int64 `xorm:"INDEX"` + TeamId int64 +} + +// GetTeamMembers returns all members in given team of organization. +func GetTeamMembers(orgId, teamId int64) ([]*User, error) { + tus := make([]*TeamUser, 0, 10) + err := x.Where("org_id=?", orgId).And("team_id=?", teamId).Find(&tus) + if err != nil { + return nil, err + } + + us := make([]*User, len(tus)) + for i, tu := range tus { + us[i], err = GetUserById(tu.Uid) + if err != nil { + return nil, err + } + } + return us, nil +} diff --git a/models/publickey.go b/models/publickey.go index 556db96491..603ff36438 100644 --- a/models/publickey.go +++ b/models/publickey.go @@ -19,9 +19,9 @@ import ( "time" "github.com/Unknwon/com" - qlog "github.com/qiniu/log" "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/process" ) const ( @@ -37,7 +37,7 @@ var ( var sshOpLocker = sync.Mutex{} var ( - sshPath string // SSH directory. + SshPath string // SSH directory. appPath string // Execution(binary) path. ) @@ -54,7 +54,7 @@ func exePath() (string, error) { func homeDir() string { home, err := com.HomeDir() if err != nil { - qlog.Fatalln(err) + log.Fatal("Fail to get home directory: %v", err) } return home } @@ -63,13 +63,13 @@ func init() { var err error if appPath, err = exePath(); err != nil { - qlog.Fatalf("publickey.init(fail to get app path): %v\n", err) + log.Fatal("publickey.init(fail to get app path): %v\n", err) } // Determine and create .ssh path. - sshPath = filepath.Join(homeDir(), ".ssh") - if err = os.MkdirAll(sshPath, os.ModePerm); err != nil { - qlog.Fatalf("publickey.init(fail to create sshPath(%s)): %v\n", sshPath, err) + SshPath = filepath.Join(homeDir(), ".ssh") + if err = os.MkdirAll(SshPath, os.ModePerm); err != nil { + log.Fatal("publickey.init(fail to create SshPath(%s)): %v\n", SshPath, err) } } @@ -94,7 +94,7 @@ func saveAuthorizedKeyFile(key *PublicKey) error { sshOpLocker.Lock() defer sshOpLocker.Unlock() - fpath := filepath.Join(sshPath, "authorized_keys") + fpath := filepath.Join(SshPath, "authorized_keys") f, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return err @@ -107,7 +107,7 @@ func saveAuthorizedKeyFile(key *PublicKey) error { // AddPublicKey adds new public key to database and authorized_keys file. func AddPublicKey(key *PublicKey) (err error) { - has, err := orm.Get(key) + has, err := x.Get(key) if err != nil { return err } else if has { @@ -121,7 +121,7 @@ func AddPublicKey(key *PublicKey) (err error) { if err = ioutil.WriteFile(tmpPath, []byte(key.Content), os.ModePerm); err != nil { return err } - stdout, stderr, err := com.ExecCmd("ssh-keygen", "-l", "-f", tmpPath) + stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-l", "-f", tmpPath) if err != nil { return errors.New("ssh-keygen -l -f: " + stderr) } else if len(stdout) < 2 { @@ -130,11 +130,11 @@ func AddPublicKey(key *PublicKey) (err error) { key.Fingerprint = strings.Split(stdout, " ")[1] // Save SSH key. - if _, err = orm.Insert(key); err != nil { + if _, err = x.Insert(key); err != nil { return err } else if err = saveAuthorizedKeyFile(key); err != nil { // Roll back. - if _, err2 := orm.Delete(key); err2 != nil { + if _, err2 := x.Delete(key); err2 != nil { return err2 } return err @@ -146,7 +146,7 @@ func AddPublicKey(key *PublicKey) (err error) { // ListPublicKey returns a list of all public keys that user has. func ListPublicKey(uid int64) ([]PublicKey, error) { keys := make([]PublicKey, 0, 5) - err := orm.Find(&keys, &PublicKey{OwnerId: uid}) + err := x.Find(&keys, &PublicKey{OwnerId: uid}) return keys, err } @@ -161,7 +161,7 @@ func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { } defer fr.Close() - fw, err := os.Create(tmpP) + fw, err := os.OpenFile(tmpP, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return err } @@ -205,19 +205,19 @@ func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { // DeletePublicKey deletes SSH key information both in database and authorized_keys file. func DeletePublicKey(key *PublicKey) error { - has, err := orm.Get(key) + has, err := x.Get(key) if err != nil { return err } else if !has { return ErrKeyNotExist } - if _, err = orm.Delete(key); err != nil { + if _, err = x.Delete(key); err != nil { return err } - fpath := filepath.Join(sshPath, "authorized_keys") - tmpPath := filepath.Join(sshPath, "authorized_keys.tmp") + fpath := filepath.Join(SshPath, "authorized_keys") + tmpPath := filepath.Join(SshPath, "authorized_keys.tmp") log.Trace("publickey.DeletePublicKey(authorized_keys): %s", fpath) if err = rewriteAuthorizedKeys(key, fpath, tmpPath); err != nil { diff --git a/models/release.go b/models/release.go index e6c3d56152..3e1a78118c 100644 --- a/models/release.go +++ b/models/release.go @@ -6,15 +6,16 @@ package models import ( "errors" + "sort" "strings" "time" - "github.com/Unknwon/com" "github.com/gogits/git" ) var ( ErrReleaseAlreadyExist = errors.New("Release already exist") + ErrReleaseNotExist = errors.New("Release does not exist") ) // Release represents a release of repository. @@ -23,21 +24,17 @@ type Release struct { RepoId int64 PublisherId int64 Publisher *User `xorm:"-"` - Title string TagName string LowerTagName string - SHA1 string + Target string + Title string + Sha1 string `xorm:"VARCHAR(40)"` NumCommits int NumCommitsBehind int `xorm:"-"` Note string `xorm:"TEXT"` + IsDraft bool `xorm:"NOT NULL DEFAULT false"` IsPrerelease bool - Created time.Time `xorm:"created"` -} - -// GetReleasesByRepoId returns a list of releases of repository. -func GetReleasesByRepoId(repoId int64) (rels []*Release, err error) { - err = orm.Desc("created").Find(&rels, Release{RepoId: repoId}) - return rels, err + Created time.Time `xorm:"CREATED"` } // IsReleaseExist returns true if release with given tag name already exists. @@ -46,7 +43,34 @@ func IsReleaseExist(repoId int64, tagName string) (bool, error) { return false, nil } - return orm.Get(&Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)}) + return x.Get(&Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)}) +} + +func createTag(gitRepo *git.Repository, rel *Release) error { + // Only actual create when publish. + if !rel.IsDraft { + if !gitRepo.IsTagExist(rel.TagName) { + commit, err := gitRepo.GetCommitOfBranch(rel.Target) + if err != nil { + return err + } + + if err = gitRepo.CreateTag(rel.TagName, commit.Id.String()); err != nil { + return err + } + } else { + commit, err := gitRepo.GetCommitOfTag(rel.TagName) + if err != nil { + return err + } + + rel.NumCommits, err = commit.CommitsCount() + if err != nil { + return err + } + } + } + return nil } // CreateRelease creates a new release of repository. @@ -58,26 +82,65 @@ func CreateRelease(gitRepo *git.Repository, rel *Release) error { return ErrReleaseAlreadyExist } - if !gitRepo.IsTagExist(rel.TagName) { - _, stderr, err := com.ExecCmdDir(gitRepo.Path, "git", "tag", rel.TagName, "-m", rel.Title) - if err != nil { - return err - } else if strings.Contains(stderr, "fatal:") { - return errors.New(stderr) - } - } else { - commit, err := gitRepo.GetCommitOfTag(rel.TagName) - if err != nil { - return err - } - - rel.NumCommits, err = commit.CommitsCount() - if err != nil { - return err - } + if err = createTag(gitRepo, rel); err != nil { + return err } - rel.LowerTagName = strings.ToLower(rel.TagName) - _, err = orm.InsertOne(rel) + _, err = x.InsertOne(rel) + return err +} + +// GetRelease returns release by given ID. +func GetRelease(repoId int64, tagName string) (*Release, error) { + isExist, err := IsReleaseExist(repoId, tagName) + if err != nil { + return nil, err + } else if !isExist { + return nil, ErrReleaseNotExist + } + + rel := &Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)} + _, err = x.Get(rel) + return rel, err +} + +// GetReleasesByRepoId returns a list of releases of repository. +func GetReleasesByRepoId(repoId int64) (rels []*Release, err error) { + err = x.Desc("created").Find(&rels, Release{RepoId: repoId}) + return rels, err +} + +type ReleaseSorter struct { + rels []*Release +} + +func (rs *ReleaseSorter) Len() int { + return len(rs.rels) +} + +func (rs *ReleaseSorter) Less(i, j int) bool { + diffNum := rs.rels[i].NumCommits - rs.rels[j].NumCommits + if diffNum != 0 { + return diffNum > 0 + } + return rs.rels[i].Created.After(rs.rels[j].Created) +} + +func (rs *ReleaseSorter) Swap(i, j int) { + rs.rels[i], rs.rels[j] = rs.rels[j], rs.rels[i] +} + +// SortReleases sorts releases by number of commits and created time. +func SortReleases(rels []*Release) { + sorter := &ReleaseSorter{rels: rels} + sort.Sort(sorter) +} + +// UpdateRelease updates information of a release. +func UpdateRelease(gitRepo *git.Repository, rel *Release) (err error) { + if err = createTag(gitRepo, rel); err != nil { + return err + } + _, err = x.Id(rel.Id).AllCols().Update(rel) return err } diff --git a/models/repo.go b/models/repo.go index 600827344a..8eec131fee 100644 --- a/models/repo.go +++ b/models/repo.go @@ -9,9 +9,9 @@ import ( "fmt" "io/ioutil" "os" - "os/exec" "path" "path/filepath" + "sort" "strings" "time" "unicode/utf8" @@ -24,9 +24,14 @@ import ( "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/bin" "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/process" "github.com/gogits/gogs/modules/setting" ) +const ( + TPL_UPDATE_HOOK = "#!/usr/bin/env %s\n%s update $1 $2 $3\n" +) + var ( ErrRepoAlreadyExist = errors.New("Repository already exist") ErrRepoNotExist = errors.New("Repository does not exist") @@ -75,19 +80,21 @@ func LoadRepoConfig() { LanguageIgns = typeFiles[0] Licenses = typeFiles[1] + sort.Strings(LanguageIgns) + sort.Strings(Licenses) } func NewRepoContext() { zip.Verbose = false // Check if server has basic git setting. - stdout, stderr, err := com.ExecCmd("git", "config", "--get", "user.name") + stdout, stderr, err := process.Exec("NewRepoContext(get setting)", "git", "config", "--get", "user.name") if strings.Contains(stderr, "fatal:") { log.Fatal("repo.NewRepoContext(fail to get git user.name): %s", stderr) } else if err != nil || len(strings.TrimSpace(stdout)) == 0 { - if _, stderr, err = com.ExecCmd("git", "config", "--global", "user.email", "gogitservice@gmail.com"); err != nil { + if _, stderr, err = process.Exec("NewRepoContext(set email)", "git", "config", "--global", "user.email", "gogitservice@gmail.com"); err != nil { log.Fatal("repo.NewRepoContext(fail to set git user.email): %s", stderr) - } else if _, stderr, err = com.ExecCmd("git", "config", "--global", "user.name", "Gogs"); err != nil { + } else if _, stderr, err = process.Exec("NewRepoContext(set name)", "git", "config", "--global", "user.name", "Gogs"); err != nil { log.Fatal("repo.NewRepoContext(fail to set git user.name): %s", stderr) } } @@ -106,11 +113,11 @@ func NewRepoContext() { // Repository represents a git repository. type Repository struct { Id int64 - OwnerId int64 `xorm:"unique(s)"` + OwnerId int64 `xorm:"UNIQUE(s)"` Owner *User `xorm:"-"` ForkId int64 - LowerName string `xorm:"unique(s) index not null"` - Name string `xorm:"index not null"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string `xorm:"INDEX NOT NULL"` Description string Website string NumWatches int @@ -128,8 +135,8 @@ type Repository struct { IsBare bool IsGoget bool DefaultBranch string - Created time.Time `xorm:"created"` - Updated time.Time `xorm:"updated"` + Created time.Time `xorm:"CREATED"` + Updated time.Time `xorm:"UPDATED"` } func (repo *Repository) GetOwner() (err error) { @@ -140,7 +147,7 @@ func (repo *Repository) GetOwner() (err error) { // IsRepositoryExist returns true if the repository with given name under user has already existed. func IsRepositoryExist(u *User, repoName string) (bool, error) { repo := Repository{OwnerId: u.Id} - has, err := orm.Where("lower_name = ?", strings.ToLower(repoName)).Get(&repo) + has, err := x.Where("lower_name = ?", strings.ToLower(repoName)).Get(&repo) if err != nil { return has, err } else if !has { @@ -151,7 +158,7 @@ func IsRepositoryExist(u *User, repoName string) (bool, error) { } var ( - illegalEquals = []string{"raw", "install", "api", "avatar", "user", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin"} + illegalEquals = []string{"raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin"} illegalSuffixs = []string{".git"} ) @@ -181,53 +188,16 @@ type Mirror struct { NextUpdate time.Time } -func GetMirror(repoId int64) (*Mirror, error) { - m := &Mirror{RepoId: repoId} - has, err := orm.Get(m) - if err != nil { - return nil, err - } else if !has { - return nil, ErrMirrorNotExist - } - return m, nil -} - -func UpdateMirror(m *Mirror) error { - _, err := orm.Id(m.Id).Update(m) - return err -} - -// MirrorUpdate checks and updates mirror repositories. -func MirrorUpdate() { - if err := orm.Iterate(new(Mirror), func(idx int, bean interface{}) error { - m := bean.(*Mirror) - if m.NextUpdate.After(time.Now()) { - return nil - } - - repoPath := filepath.Join(setting.RepoRootPath, m.RepoName+".git") - _, stderr, err := com.ExecCmdDir(repoPath, "git", "remote", "update") - if err != nil { - return errors.New("git remote update: " + stderr) - } else if err = git.UnpackRefs(repoPath); err != nil { - return err - } - - m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour) - return UpdateMirror(m) - }); err != nil { - log.Error("repo.MirrorUpdate: %v", err) - } -} - // MirrorRepository creates a mirror repository from source. func MirrorRepository(repoId int64, userName, repoName, repoPath, url string) error { - _, stderr, err := com.ExecCmd("git", "clone", "--mirror", url, repoPath) + // TODO: need timeout. + _, stderr, err := process.Exec(fmt.Sprintf("MirrorRepository: %s/%s", userName, repoName), + "git", "clone", "--mirror", url, repoPath) if err != nil { return errors.New("git clone --mirror: " + stderr) } - if _, err = orm.InsertOne(&Mirror{ + if _, err = x.InsertOne(&Mirror{ RepoId: repoId, RepoName: strings.ToLower(userName + "/" + repoName), Interval: 24, @@ -239,9 +209,50 @@ func MirrorRepository(repoId int64, userName, repoName, repoPath, url string) er return git.UnpackRefs(repoPath) } +func GetMirror(repoId int64) (*Mirror, error) { + m := &Mirror{RepoId: repoId} + has, err := x.Get(m) + if err != nil { + return nil, err + } else if !has { + return nil, ErrMirrorNotExist + } + return m, nil +} + +func UpdateMirror(m *Mirror) error { + _, err := x.Id(m.Id).Update(m) + return err +} + +// MirrorUpdate checks and updates mirror repositories. +func MirrorUpdate() { + if err := x.Iterate(new(Mirror), func(idx int, bean interface{}) error { + m := bean.(*Mirror) + if m.NextUpdate.After(time.Now()) { + return nil + } + + // TODO: need timeout. + repoPath := filepath.Join(setting.RepoRootPath, m.RepoName+".git") + if _, stderr, err := process.ExecDir( + repoPath, fmt.Sprintf("MirrorUpdate: %s", repoPath), + "git", "remote", "update"); err != nil { + return errors.New("git remote update: " + stderr) + } else if err = git.UnpackRefs(repoPath); err != nil { + return errors.New("UnpackRefs: " + err.Error()) + } + + m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour) + return UpdateMirror(m) + }); err != nil { + log.Error("repo.MirrorUpdate: %v", err) + } +} + // MigrateRepository migrates a existing repository from other project hosting. -func MigrateRepository(user *User, name, desc string, private, mirror bool, url string) (*Repository, error) { - repo, err := CreateRepository(user, name, desc, "", "", private, mirror, false) +func MigrateRepository(u *User, name, desc string, private, mirror bool, url string) (*Repository, error) { + repo, err := CreateRepository(u, name, desc, "", "", private, mirror, false) if err != nil { return nil, err } @@ -250,144 +261,45 @@ func MigrateRepository(user *User, name, desc string, private, mirror bool, url tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond())) os.MkdirAll(tmpDir, os.ModePerm) - repoPath := RepoPath(user.Name, name) + repoPath := RepoPath(u.Name, name) repo.IsBare = false if mirror { - if err = MirrorRepository(repo.Id, user.Name, repo.Name, repoPath, url); err != nil { + if err = MirrorRepository(repo.Id, u.Name, repo.Name, repoPath, url); err != nil { return repo, err } repo.IsMirror = true return repo, UpdateRepository(repo) } + // TODO: need timeout. // Clone from local repository. - _, stderr, err := com.ExecCmd("git", "clone", repoPath, tmpDir) + _, stderr, err := process.Exec( + fmt.Sprintf("MigrateRepository(git clone): %s", repoPath), + "git", "clone", repoPath, tmpDir) if err != nil { return repo, errors.New("git clone: " + stderr) } + // TODO: need timeout. // Pull data from source. - _, stderr, err = com.ExecCmdDir(tmpDir, "git", "pull", url) - if err != nil { + if _, stderr, err = process.ExecDir( + tmpDir, fmt.Sprintf("MigrateRepository(git pull): %s", repoPath), + "git", "pull", url); err != nil { return repo, errors.New("git pull: " + stderr) } + // TODO: need timeout. // Push data to local repository. - if _, stderr, err = com.ExecCmdDir(tmpDir, "git", "push", "origin", "master"); err != nil { + if _, stderr, err = process.ExecDir( + tmpDir, fmt.Sprintf("MigrateRepository(git push): %s", repoPath), + "git", "push", "origin", "master"); err != nil { return repo, errors.New("git push: " + stderr) } return repo, UpdateRepository(repo) } -// CreateRepository creates a repository for given user or orgnaziation. -func CreateRepository(user *User, name, desc, lang, license string, private, mirror, initReadme bool) (*Repository, error) { - if !IsLegalName(name) { - return nil, ErrRepoNameIllegal - } - - isExist, err := IsRepositoryExist(user, name) - if err != nil { - return nil, err - } else if isExist { - return nil, ErrRepoAlreadyExist - } - - repo := &Repository{ - OwnerId: user.Id, - Name: name, - LowerName: strings.ToLower(name), - Description: desc, - IsPrivate: private, - IsBare: lang == "" && license == "" && !initReadme, - } - if !repo.IsBare { - repo.DefaultBranch = "master" - } - - repoPath := RepoPath(user.Name, repo.Name) - - sess := orm.NewSession() - defer sess.Close() - sess.Begin() - - if _, err = sess.Insert(repo); err != nil { - if err2 := os.RemoveAll(repoPath); err2 != nil { - log.Error("repo.CreateRepository(repo): %v", err) - return nil, errors.New(fmt.Sprintf( - "delete repo directory %s/%s failed(1): %v", user.Name, repo.Name, err2)) - } - sess.Rollback() - return nil, err - } - - mode := AU_WRITABLE - if mirror { - mode = AU_READABLE - } - access := Access{ - UserName: user.LowerName, - RepoName: strings.ToLower(path.Join(user.Name, repo.Name)), - Mode: mode, - } - if _, err = sess.Insert(&access); err != nil { - sess.Rollback() - if err2 := os.RemoveAll(repoPath); err2 != nil { - log.Error("repo.CreateRepository(access): %v", err) - return nil, errors.New(fmt.Sprintf( - "delete repo directory %s/%s failed(2): %v", user.Name, repo.Name, err2)) - } - return nil, err - } - - rawSql := "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, user.Id); err != nil { - sess.Rollback() - if err2 := os.RemoveAll(repoPath); err2 != nil { - log.Error("repo.CreateRepository(repo count): %v", err) - return nil, errors.New(fmt.Sprintf( - "delete repo directory %s/%s failed(3): %v", user.Name, repo.Name, err2)) - } - return nil, err - } - - if err = sess.Commit(); err != nil { - sess.Rollback() - if err2 := os.RemoveAll(repoPath); err2 != nil { - log.Error("repo.CreateRepository(commit): %v", err) - return nil, errors.New(fmt.Sprintf( - "delete repo directory %s/%s failed(3): %v", user.Name, repo.Name, err2)) - } - return nil, err - } - - if err = WatchRepo(user.Id, repo.Id, true); err != nil { - log.Error("repo.CreateRepository(WatchRepo): %v", err) - } - - if err = NewRepoAction(user, repo); err != nil { - log.Error("repo.CreateRepository(NewRepoAction): %v", err) - } - - // No need for init for mirror. - if mirror { - return repo, nil - } - - if err = initRepository(repoPath, user, repo, initReadme, lang, license); err != nil { - return nil, err - } - - c := exec.Command("git", "update-server-info") - c.Dir = repoPath - if err = c.Run(); err != nil { - log.Error("repo.CreateRepository(exec update-server-info): %v", err) - } - - return repo, nil -} - // extractGitBareZip extracts git-bare.zip to repository path. func extractGitBareZip(repoPath string) error { z, err := zip.Open(filepath.Join(setting.RepoRootPath, "git-bare.zip")) @@ -402,15 +314,22 @@ func extractGitBareZip(repoPath string) error { // initRepoCommit temporarily changes with work directory. func initRepoCommit(tmpPath string, sig *git.Signature) (err error) { var stderr string - if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "add", "--all"); err != nil { + if _, stderr, err = process.ExecDir( + tmpPath, fmt.Sprintf("initRepoCommit(git add): %s", tmpPath), + "git", "add", "--all"); err != nil { return errors.New("git add: " + stderr) } - if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), + + if _, stderr, err = process.ExecDir( + tmpPath, fmt.Sprintf("initRepoCommit(git commit): %s", tmpPath), + "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", "Init commit"); err != nil { return errors.New("git commit: " + stderr) } - if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "push", "origin", "master"); err != nil { + if _, stderr, err = process.ExecDir( + tmpPath, fmt.Sprintf("initRepoCommit(git push): %s", tmpPath), + "git", "push", "origin", "master"); err != nil { return errors.New("git push: " + stderr) } return nil @@ -447,7 +366,7 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep rp := strings.NewReplacer("\\", "/", " ", "\\ ") // hook/post-update if err := createHookUpdate(filepath.Join(repoPath, "hooks", "update"), - fmt.Sprintf("#!/usr/bin/env %s\n%s update $1 $2 $3\n", setting.ScriptType, + fmt.Sprintf(TPL_UPDATE_HOOK, setting.ScriptType, rp.Replace(appPath))); err != nil { return err } @@ -468,9 +387,11 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep tmpDir := filepath.Join(os.TempDir(), base.ToStr(time.Now().Nanosecond())) os.MkdirAll(tmpDir, os.ModePerm) - _, stderr, err := com.ExecCmd("git", "clone", repoPath, tmpDir) + _, stderr, err := process.Exec( + fmt.Sprintf("initRepository(git clone): %s", repoPath), + "git", "clone", repoPath, tmpDir) if err != nil { - return errors.New("git clone: " + stderr) + return errors.New("initRepository(git clone): " + stderr) } // README @@ -486,22 +407,40 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep // .gitignore if repoLang != "" { filePath := "conf/gitignore/" + repoLang - if com.IsFile(filePath) { - if err := com.Copy(filePath, - filepath.Join(tmpDir, fileName["gitign"])); err != nil { + targetPath := path.Join(tmpDir, fileName["gitign"]) + data, err := bin.Asset(filePath) + if err == nil { + if err = ioutil.WriteFile(targetPath, data, os.ModePerm); err != nil { return err } + } else { + // Check custom files. + filePath = path.Join(setting.CustomPath, "conf/gitignore", repoLang) + if com.IsFile(filePath) { + if err := com.Copy(filePath, targetPath); err != nil { + return err + } + } } } // LICENSE if license != "" { filePath := "conf/license/" + license - if com.IsFile(filePath) { - if err := com.Copy(filePath, - filepath.Join(tmpDir, fileName["license"])); err != nil { + targetPath := path.Join(tmpDir, fileName["license"]) + data, err := bin.Asset(filePath) + if err == nil { + if err = ioutil.WriteFile(targetPath, data, os.ModePerm); err != nil { return err } + } else { + // Check custom files. + filePath = path.Join(setting.CustomPath, "conf/license", license) + if com.IsFile(filePath) { + if err := com.Copy(filePath, targetPath); err != nil { + return err + } + } } } @@ -515,17 +454,156 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep return initRepoCommit(tmpDir, user.NewGitSig()) } +// CreateRepository creates a repository for given user or organization. +func CreateRepository(u *User, name, desc, lang, license string, private, mirror, initReadme bool) (*Repository, error) { + if !IsLegalName(name) { + return nil, ErrRepoNameIllegal + } + + isExist, err := IsRepositoryExist(u, name) + if err != nil { + return nil, err + } else if isExist { + return nil, ErrRepoAlreadyExist + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return nil, err + } + + repo := &Repository{ + OwnerId: u.Id, + Owner: u, + Name: name, + LowerName: strings.ToLower(name), + Description: desc, + IsPrivate: private, + IsBare: lang == "" && license == "" && !initReadme, + } + if !repo.IsBare { + repo.DefaultBranch = "master" + } + + if _, err = sess.Insert(repo); err != nil { + sess.Rollback() + return nil, err + } + + var t *Team // Owner team. + + mode := WRITABLE + if mirror { + mode = READABLE + } + access := &Access{ + UserName: u.LowerName, + RepoName: strings.ToLower(path.Join(u.Name, repo.Name)), + Mode: mode, + } + // Give access to all members in owner team. + if u.IsOrganization() { + t, err = u.GetOwnerTeam() + if err != nil { + sess.Rollback() + return nil, err + } + us, err := GetTeamMembers(u.Id, t.Id) + if err != nil { + sess.Rollback() + return nil, err + } + for _, u := range us { + access.UserName = u.LowerName + if _, err = sess.Insert(access); err != nil { + sess.Rollback() + return nil, err + } + } + } else { + if _, err = sess.Insert(access); err != nil { + sess.Rollback() + return nil, err + } + } + + rawSql := "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?" + if _, err = sess.Exec(rawSql, u.Id); err != nil { + sess.Rollback() + return nil, err + } + + // Update owner team info and count. + if u.IsOrganization() { + t.RepoIds += "$" + base.ToStr(repo.Id) + "|" + t.NumRepos++ + if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { + sess.Rollback() + return nil, err + } + } + + if err = sess.Commit(); err != nil { + return nil, err + } + + if u.IsOrganization() { + ous, err := GetOrgUsersByOrgId(u.Id) + if err != nil { + log.Error("repo.CreateRepository(GetOrgUsersByOrgId): %v", err) + } else { + for _, ou := range ous { + if err = WatchRepo(ou.Uid, repo.Id, true); err != nil { + log.Error("repo.CreateRepository(WatchRepo): %v", err) + } + } + } + } + if err = WatchRepo(u.Id, repo.Id, true); err != nil { + log.Error("repo.CreateRepository(WatchRepo2): %v", err) + } + + if err = NewRepoAction(u, repo); err != nil { + log.Error("repo.CreateRepository(NewRepoAction): %v", err) + } + + // No need for init for mirror. + if mirror { + return repo, nil + } + + repoPath := RepoPath(u.Name, repo.Name) + if err = initRepository(repoPath, u, repo, initReadme, lang, license); err != nil { + if err2 := os.RemoveAll(repoPath); err2 != nil { + log.Error("repo.CreateRepository(initRepository): %v", err) + return nil, errors.New(fmt.Sprintf( + "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2)) + } + return nil, err + } + + _, stderr, err := process.ExecDir( + repoPath, fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath), + "git", "update-server-info") + if err != nil { + return nil, errors.New("CreateRepository(git update-server-info): " + stderr) + } + + return repo, nil +} + // GetRepositoriesWithUsers returns given number of repository objects with offset. // It also auto-gets corresponding users. func GetRepositoriesWithUsers(num, offset int) ([]*Repository, error) { repos := make([]*Repository, 0, num) - if err := orm.Limit(num, offset).Asc("id").Find(&repos); err != nil { + if err := x.Limit(num, offset).Asc("id").Find(&repos); err != nil { return nil, err } for _, repo := range repos { repo.Owner = &User{Id: repo.OwnerId} - has, err := orm.Get(repo.Owner) + has, err := x.Get(repo.Owner) if err != nil { return nil, err } else if !has { @@ -550,11 +628,11 @@ func TransferOwnership(user *User, newOwner string, repo *Repository) (err error // Update accesses. accesses := make([]Access, 0, 10) - if err = orm.Find(&accesses, &Access{RepoName: user.LowerName + "/" + repo.LowerName}); err != nil { + if err = x.Find(&accesses, &Access{RepoName: user.LowerName + "/" + repo.LowerName}); err != nil { return err } - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err @@ -615,11 +693,11 @@ func TransferOwnership(user *User, newOwner string, repo *Repository) (err error func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error) { // Update accesses. accesses := make([]Access, 0, 10) - if err = orm.Find(&accesses, &Access{RepoName: strings.ToLower(userName + "/" + oldRepoName)}); err != nil { + if err = x.Find(&accesses, &Access{RepoName: strings.ToLower(userName + "/" + oldRepoName)}); err != nil { return err } - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err @@ -650,25 +728,26 @@ func UpdateRepository(repo *Repository) error { if len(repo.Website) > 255 { repo.Website = repo.Website[:255] } - _, err := orm.Id(repo.Id).AllCols().Update(repo) + _, err := x.Id(repo.Id).AllCols().Update(repo) return err } // DeleteRepository deletes a repository for a user or orgnaztion. -func DeleteRepository(userId, repoId int64, userName string) (err error) { +func DeleteRepository(userId, repoId int64, userName string) error { repo := &Repository{Id: repoId, OwnerId: userId} - has, err := orm.Get(repo) + has, err := x.Get(repo) if err != nil { return err } else if !has { return ErrRepoNotExist } - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err } + if _, err = sess.Delete(&Repository{Id: repoId}); err != nil { sess.Rollback() return err @@ -703,7 +782,7 @@ func DeleteRepository(userId, repoId int64, userName string) (err error) { } // Delete comments. - if err = orm.Iterate(&Issue{RepoId: repoId}, func(idx int, bean interface{}) error { + if err = x.Iterate(&Issue{RepoId: repoId}, func(idx int, bean interface{}) error { issue := bean.(*Issue) if _, err = sess.Delete(&Comment{IssueId: issue.Id}); err != nil { sess.Rollback() @@ -725,16 +804,11 @@ func DeleteRepository(userId, repoId int64, userName string) (err error) { sess.Rollback() return err } - if err = sess.Commit(); err != nil { + if err = os.RemoveAll(RepoPath(userName, repo.Name)); err != nil { sess.Rollback() return err } - if err = os.RemoveAll(RepoPath(userName, repo.Name)); err != nil { - // TODO: log and delete manully - log.Error("delete repo %s/%s failed: %v", userName, repo.Name, err) - return err - } - return nil + return sess.Commit() } // GetRepositoryByName returns the repository by given name under user if exists. @@ -743,7 +817,7 @@ func GetRepositoryByName(userId int64, repoName string) (*Repository, error) { OwnerId: userId, LowerName: strings.ToLower(repoName), } - has, err := orm.Get(repo) + has, err := x.Get(repo) if err != nil { return nil, err } else if !has { @@ -755,7 +829,7 @@ func GetRepositoryByName(userId int64, repoName string) (*Repository, error) { // GetRepositoryById returns the repository by given id if exists. func GetRepositoryById(id int64) (*Repository, error) { repo := &Repository{} - has, err := orm.Id(id).Get(repo) + has, err := x.Id(id).Get(repo) if err != nil { return nil, err } else if !has { @@ -767,7 +841,7 @@ func GetRepositoryById(id int64) (*Repository, error) { // GetRepositories returns a list of repositories of given user. func GetRepositories(uid int64, private bool) ([]*Repository, error) { repos := make([]*Repository, 0, 10) - sess := orm.Desc("updated") + sess := x.Desc("updated") if !private { sess.Where("is_private=?", false) } @@ -778,19 +852,19 @@ func GetRepositories(uid int64, private bool) ([]*Repository, error) { // GetRecentUpdatedRepositories returns the list of repositories that are recently updated. func GetRecentUpdatedRepositories() (repos []*Repository, err error) { - err = orm.Where("is_private=?", false).Limit(5).Desc("updated").Find(&repos) + err = x.Where("is_private=?", false).Limit(5).Desc("updated").Find(&repos) return repos, err } // GetRepositoryCount returns the total number of repositories of user. func GetRepositoryCount(user *User) (int64, error) { - return orm.Count(&Repository{OwnerId: user.Id}) + return x.Count(&Repository{OwnerId: user.Id}) } // GetCollaboratorNames returns a list of user name of repository's collaborators. func GetCollaboratorNames(repoName string) ([]string, error) { accesses := make([]*Access, 0, 10) - if err := orm.Find(&accesses, &Access{RepoName: strings.ToLower(repoName)}); err != nil { + if err := x.Find(&accesses, &Access{RepoName: strings.ToLower(repoName)}); err != nil { return nil, err } @@ -805,17 +879,17 @@ func GetCollaboratorNames(repoName string) ([]string, error) { func GetCollaborativeRepos(uname string) ([]*Repository, error) { uname = strings.ToLower(uname) accesses := make([]*Access, 0, 10) - if err := orm.Find(&accesses, &Access{UserName: uname}); err != nil { + if err := x.Find(&accesses, &Access{UserName: uname}); err != nil { return nil, err } repos := make([]*Repository, 0, 10) for _, access := range accesses { - if strings.HasPrefix(access.RepoName, uname) { + infos := strings.Split(access.RepoName, "/") + if infos[0] == uname { continue } - infos := strings.Split(access.RepoName, "/") u, err := GetUserByName(infos[0]) if err != nil { return nil, err @@ -834,7 +908,7 @@ func GetCollaborativeRepos(uname string) ([]*Repository, error) { // GetCollaborators returns a list of users of repository's collaborators. func GetCollaborators(repoName string) (us []*User, err error) { accesses := make([]*Access, 0, 10) - if err = orm.Find(&accesses, &Access{RepoName: strings.ToLower(repoName)}); err != nil { + if err = x.Find(&accesses, &Access{RepoName: strings.ToLower(repoName)}); err != nil { return nil, err } @@ -858,18 +932,18 @@ type Watch struct { // Watch or unwatch repository. func WatchRepo(uid, rid int64, watch bool) (err error) { if watch { - if _, err = orm.Insert(&Watch{RepoId: rid, UserId: uid}); err != nil { + if _, err = x.Insert(&Watch{RepoId: rid, UserId: uid}); err != nil { return err } rawSql := "UPDATE `repository` SET num_watches = num_watches + 1 WHERE id = ?" - _, err = orm.Exec(rawSql, rid) + _, err = x.Exec(rawSql, rid) } else { - if _, err = orm.Delete(&Watch{0, uid, rid}); err != nil { + if _, err = x.Delete(&Watch{0, uid, rid}); err != nil { return err } rawSql := "UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?" - _, err = orm.Exec(rawSql, rid) + _, err = x.Exec(rawSql, rid) } return err } @@ -877,7 +951,7 @@ func WatchRepo(uid, rid int64, watch bool) (err error) { // GetWatchers returns all watchers of given repository. func GetWatchers(rid int64) ([]*Watch, error) { watches := make([]*Watch, 0, 10) - err := orm.Find(&watches, &Watch{RepoId: rid}) + err := x.Find(&watches, &Watch{RepoId: rid}) return watches, err } @@ -891,7 +965,7 @@ func NotifyWatchers(act *Action) error { // Add feed for actioner. act.UserId = act.ActUserId - if _, err = orm.InsertOne(act); err != nil { + if _, err = x.InsertOne(act); err != nil { return errors.New("repo.NotifyWatchers(create action): " + err.Error()) } @@ -902,7 +976,7 @@ func NotifyWatchers(act *Action) error { act.Id = 0 act.UserId = watches[i].UserId - if _, err = orm.InsertOne(act); err != nil { + if _, err = x.InsertOne(act); err != nil { return errors.New("repo.NotifyWatchers(create action): " + err.Error()) } } @@ -911,7 +985,7 @@ func NotifyWatchers(act *Action) error { // IsWatching checks if user has watched given repository. func IsWatching(uid, rid int64) bool { - has, _ := orm.Get(&Watch{0, uid, rid}) + has, _ := x.Get(&Watch{0, uid, rid}) return has } diff --git a/models/update.go b/models/update.go index b7242cde85..3328f2213f 100644 --- a/models/update.go +++ b/models/update.go @@ -6,21 +6,21 @@ package models import ( "container/list" + "fmt" "os/exec" "strings" - qlog "github.com/qiniu/log" - "github.com/gogits/git" "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" ) -func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName string, userId int64) { +func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName string, userId int64) error { isNew := strings.HasPrefix(oldCommitId, "0000000") if isNew && strings.HasPrefix(newCommitId, "0000000") { - qlog.Fatal("old rev and new rev both 000000") + return fmt.Errorf("old rev and new rev both 000000") } f := RepoPath(repoUserName, repoName) @@ -31,19 +31,18 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName isDel := strings.HasPrefix(newCommitId, "0000000") if isDel { - qlog.Info("del rev", refName, "from", userName+"/"+repoName+".git", "by", userId) - return + log.GitLogger.Info("del rev", refName, "from", userName+"/"+repoName+".git", "by", userId) + return nil } repo, err := git.OpenRepository(f) if err != nil { - qlog.Fatalf("runUpdate.Open repoId: %v", err) + return fmt.Errorf("runUpdate.Open repoId: %v", err) } newCommit, err := repo.GetCommit(newCommitId) if err != nil { - qlog.Fatalf("runUpdate GetCommit of newCommitId: %v", err) - return + return fmt.Errorf("runUpdate GetCommit of newCommitId: %v", err) } var l *list.List @@ -51,28 +50,27 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName if isNew { l, err = newCommit.CommitsBefore() if err != nil { - qlog.Fatalf("Find CommitsBefore erro: %v", err) + return fmt.Errorf("Find CommitsBefore erro: %v", err) } } else { l, err = newCommit.CommitsBeforeUntil(oldCommitId) if err != nil { - qlog.Fatalf("Find CommitsBeforeUntil erro: %v", err) - return + return fmt.Errorf("Find CommitsBeforeUntil erro: %v", err) } } if err != nil { - qlog.Fatalf("runUpdate.Commit repoId: %v", err) + return fmt.Errorf("runUpdate.Commit repoId: %v", err) } ru, err := GetUserByName(repoUserName) if err != nil { - qlog.Fatalf("runUpdate.GetUserByName: %v", err) + return fmt.Errorf("runUpdate.GetUserByName: %v", err) } repos, err := GetRepositoryByName(ru.Id, repoName) if err != nil { - qlog.Fatalf("runUpdate.GetRepositoryByName userId: %v", err) + return fmt.Errorf("runUpdate.GetRepositoryByName userId: %v", err) } commits := make([]*base.PushCommit, 0) @@ -96,6 +94,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName //commits = append(commits, []string{lastCommit.Id().String(), lastCommit.Message()}) if err = CommitRepoAction(userId, ru.Id, userName, actEmail, repos.Id, repoUserName, repoName, refName, &base.PushCommits{l.Len(), commits}); err != nil { - qlog.Fatalf("runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) + return fmt.Errorf("runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) } + return nil } diff --git a/models/user.go b/models/user.go index f95f303b2b..9b0bdebe6b 100644 --- a/models/user.go +++ b/models/user.go @@ -21,14 +21,16 @@ import ( "github.com/gogits/gogs/modules/setting" ) -// User types. +type UserType int + const ( - UT_INDIVIDUAL = iota + 1 - UT_ORGANIZATION + INDIVIDUAL UserType = iota // Historic reason to make it starts at 0. + ORGANIZATION ) var ( ErrUserOwnRepos = errors.New("User still have ownership of repositories") + ErrUserHasOrgs = errors.New("User still have membership of organization") ErrUserAlreadyExist = errors.New("User already exist") ErrUserNotExist = errors.New("User does not exist") ErrUserNotKeyOwner = errors.New("User does not the owner of public key") @@ -47,10 +49,11 @@ type User struct { FullName string Email string `xorm:"unique not null"` Passwd string `xorm:"not null"` - LoginType int + LoginType LoginType LoginSource int64 `xorm:"not null default 0"` LoginName string - Type int + Type UserType + Orgs []*User `xorm:"-"` NumFollowers int NumFollowings int NumStars int @@ -65,43 +68,63 @@ type User struct { Salt string `xorm:"VARCHAR(10)"` Created time.Time `xorm:"created"` Updated time.Time `xorm:"updated"` + + // For organization. + Description string + NumTeams int + NumMembers int } // HomeLink returns the user home page link. -func (user *User) HomeLink() string { - return "/user/" + user.Name +func (u *User) HomeLink() string { + return "/user/" + u.Name } -// AvatarLink returns the user gravatar link. -func (user *User) AvatarLink() string { +// AvatarLink returns user gravatar link. +func (u *User) AvatarLink() string { if setting.DisableGravatar { return "/img/avatar_default.jpg" } else if setting.Service.EnableCacheAvatar { - return "/avatar/" + user.Avatar + return "/avatar/" + u.Avatar } - return "//1.gravatar.com/avatar/" + user.Avatar + return "//1.gravatar.com/avatar/" + u.Avatar } // NewGitSig generates and returns the signature of given user. -func (user *User) NewGitSig() *git.Signature { +func (u *User) NewGitSig() *git.Signature { return &git.Signature{ - Name: user.Name, - Email: user.Email, + Name: u.Name, + Email: u.Email, When: time.Now(), } } // EncodePasswd encodes password to safe format. -func (user *User) EncodePasswd() { - newPasswd := base.PBKDF2([]byte(user.Passwd), []byte(user.Salt), 10000, 50, sha256.New) - user.Passwd = fmt.Sprintf("%x", newPasswd) +func (u *User) EncodePasswd() { + newPasswd := base.PBKDF2([]byte(u.Passwd), []byte(u.Salt), 10000, 50, sha256.New) + u.Passwd = fmt.Sprintf("%x", newPasswd) } -// Member represents user is member of organization. -type Member struct { - Id int64 - OrgId int64 `xorm:"unique(member) index"` - UserId int64 `xorm:"unique(member)"` +// IsOrganization returns true if user is actually a organization. +func (u *User) IsOrganization() bool { + return u.Type == ORGANIZATION +} + +// GetOrganizations returns all organizations that user belongs to. +func (u *User) GetOrganizations() error { + ous, err := GetOrgUsersByUserId(u.Id) + if err != nil { + return err + } + + u.Orgs = make([]*User, len(ous)) + for i, ou := range ous { + u.Orgs[i], err = GetUserById(ou.OrgId) + if err != nil { + return err + } + } + return nil } // IsUserExist checks if given user name exist, @@ -110,7 +133,7 @@ func IsUserExist(name string) (bool, error) { if len(name) == 0 { return false, nil } - return orm.Get(&User{LowerName: strings.ToLower(name)}) + return x.Get(&User{LowerName: strings.ToLower(name)}) } // IsEmailUsed returns true if the e-mail has been used. @@ -118,7 +141,7 @@ func IsEmailUsed(email string) (bool, error) { if len(email) == 0 { return false, nil } - return orm.Get(&User{Email: email}) + return x.Get(&User{Email: email}) } // GetUserSalt returns a user salt token @@ -126,55 +149,66 @@ func GetUserSalt() string { return base.GetRandomString(10) } -// RegisterUser creates record of a new user. -func RegisterUser(user *User) (*User, error) { - - if !IsLegalName(user.Name) { +// CreateUser creates record of a new user. +func CreateUser(u *User) (*User, error) { + if !IsLegalName(u.Name) { return nil, ErrUserNameIllegal } - isExist, err := IsUserExist(user.Name) + isExist, err := IsUserExist(u.Name) if err != nil { return nil, err } else if isExist { return nil, ErrUserAlreadyExist } - isExist, err = IsEmailUsed(user.Email) + isExist, err = IsEmailUsed(u.Email) if err != nil { return nil, err } else if isExist { return nil, ErrEmailAlreadyUsed } - user.LowerName = strings.ToLower(user.Name) - user.Avatar = base.EncodeMd5(user.Email) - user.AvatarEmail = user.Email - user.Rands = GetUserSalt() - user.Salt = GetUserSalt() - user.EncodePasswd() - if _, err = orm.Insert(user); err != nil { - return nil, err - } else if err = os.MkdirAll(UserPath(user.Name), os.ModePerm); err != nil { - if _, err := orm.Id(user.Id).Delete(&User{}); err != nil { - return nil, errors.New(fmt.Sprintf( - "both create userpath %s and delete table record faild: %v", user.Name, err)) - } + u.LowerName = strings.ToLower(u.Name) + u.Avatar = base.EncodeMd5(u.Email) + u.AvatarEmail = u.Email + u.Rands = GetUserSalt() + u.Salt = GetUserSalt() + u.EncodePasswd() + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { return nil, err } - if user.Id == 1 { - user.IsAdmin = true - user.IsActive = true - _, err = orm.Id(user.Id).UseBool().Update(user) + if _, err = sess.Insert(u); err != nil { + sess.Rollback() + return nil, err } - return user, err + + if err = os.MkdirAll(UserPath(u.Name), os.ModePerm); err != nil { + sess.Rollback() + return nil, err + } + + if err = sess.Commit(); err != nil { + return nil, err + } + + // Auto-set admin for user whose ID is 1. + if u.Id == 1 { + u.IsAdmin = true + u.IsActive = true + _, err = x.Id(u.Id).UseBool().Update(u) + } + return u, err } // GetUsers returns given number of user objects with offset. func GetUsers(num, offset int) ([]User, error) { users := make([]User, 0, num) - err := orm.Limit(num, offset).Asc("id").Find(&users) + err := x.Limit(num, offset).Asc("id").Find(&users) return users, err } @@ -218,11 +252,11 @@ func ChangeUserName(user *User, newUserName string) (err error) { // Update accesses of user. accesses := make([]Access, 0, 10) - if err = orm.Find(&accesses, &Access{UserName: user.LowerName}); err != nil { + if err = x.Find(&accesses, &Access{UserName: user.LowerName}); err != nil { return err } - sess := orm.NewSession() + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err @@ -245,7 +279,7 @@ func ChangeUserName(user *User, newUserName string) (err error) { for i := range repos { accesses = make([]Access, 0, 10) // Update accesses of user repository. - if err = orm.Find(&accesses, &Access{RepoName: user.LowerName + "/" + repos[i].LowerName}); err != nil { + if err = x.Find(&accesses, &Access{RepoName: user.LowerName + "/" + repos[i].LowerName}); err != nil { return err } @@ -268,60 +302,68 @@ func ChangeUserName(user *User, newUserName string) (err error) { } // UpdateUser updates user's information. -func UpdateUser(user *User) (err error) { - user.LowerName = strings.ToLower(user.Name) +func UpdateUser(u *User) (err error) { + u.LowerName = strings.ToLower(u.Name) - if len(user.Location) > 255 { - user.Location = user.Location[:255] + if len(u.Location) > 255 { + u.Location = u.Location[:255] } - if len(user.Website) > 255 { - user.Website = user.Website[:255] + if len(u.Website) > 255 { + u.Website = u.Website[:255] + } + if len(u.Description) > 255 { + u.Description = u.Description[:255] } - _, err = orm.Id(user.Id).AllCols().Update(user) + _, err = x.Id(u.Id).AllCols().Update(u) return err } -// DeleteUser completely deletes everything of the user. -func DeleteUser(user *User) error { +// TODO: need some kind of mechanism to record failure. +// DeleteUser completely and permanently deletes everything of user. +func DeleteUser(u *User) error { // Check ownership of repository. - count, err := GetRepositoryCount(user) + count, err := GetRepositoryCount(u) if err != nil { - return errors.New("modesl.GetRepositories: " + err.Error()) + return errors.New("modesl.GetRepositories(GetRepositoryCount): " + err.Error()) } else if count > 0 { return ErrUserOwnRepos } + // Check membership of organization. + count, err = GetOrganizationCount(u) + if err != nil { + return errors.New("modesl.GetRepositories(GetOrganizationCount): " + err.Error()) + } else if count > 0 { + return ErrUserHasOrgs + } + // TODO: check issues, other repos' commits + // TODO: roll backable in some point. // Delete all followers. - if _, err = orm.Delete(&Follow{FollowId: user.Id}); err != nil { + if _, err = x.Delete(&Follow{FollowId: u.Id}); err != nil { return err } - // Delete oauth2. - if _, err = orm.Delete(&Oauth2{Uid: user.Id}); err != nil { + if _, err = x.Delete(&Oauth2{Uid: u.Id}); err != nil { return err } - // Delete all feeds. - if _, err = orm.Delete(&Action{UserId: user.Id}); err != nil { + if _, err = x.Delete(&Action{UserId: u.Id}); err != nil { return err } - // Delete all watches. - if _, err = orm.Delete(&Watch{UserId: user.Id}); err != nil { + if _, err = x.Delete(&Watch{UserId: u.Id}); err != nil { return err } - // Delete all accesses. - if _, err = orm.Delete(&Access{UserName: user.LowerName}); err != nil { + if _, err = x.Delete(&Access{UserName: u.LowerName}); err != nil { return err } - // Delete all SSH keys. keys := make([]*PublicKey, 0, 10) - if err = orm.Find(&keys, &PublicKey{OwnerId: user.Id}); err != nil { + if err = x.Find(&keys, &PublicKey{OwnerId: u.Id}); err != nil { return err } for _, key := range keys { @@ -331,11 +373,17 @@ func DeleteUser(user *User) error { } // Delete user directory. - if err = os.RemoveAll(UserPath(user.Name)); err != nil { + if err = os.RemoveAll(UserPath(u.Name)); err != nil { return err } - _, err = orm.Delete(user) + _, err = x.Delete(u) + return err +} + +// DeleteInactivateUsers deletes all inactivate users. +func DeleteInactivateUsers() error { + _, err := x.Where("is_active=?", false).Delete(new(User)) return err } @@ -347,7 +395,7 @@ func UserPath(userName string) string { func GetUserByKeyId(keyId int64) (*User, error) { user := new(User) rawSql := "SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?" - has, err := orm.Sql(rawSql, keyId).Get(user) + has, err := x.Sql(rawSql, keyId).Get(user) if err != nil { return nil, err } else if !has { @@ -356,17 +404,16 @@ func GetUserByKeyId(keyId int64) (*User, error) { return user, nil } -// GetUserById returns the user object by given id if exists. +// GetUserById returns the user object by given ID if exists. func GetUserById(id int64) (*User, error) { - user := new(User) - has, err := orm.Id(id).Get(user) + u := new(User) + has, err := x.Id(id).Get(u) if err != nil { return nil, err - } - if !has { + } else if !has { return nil, ErrUserNotExist } - return user, nil + return u, nil } // GetUserByName returns the user object by given name if exists. @@ -375,7 +422,7 @@ func GetUserByName(name string) (*User, error) { return nil, ErrUserNotExist } user := &User{LowerName: strings.ToLower(name)} - has, err := orm.Get(user) + has, err := x.Get(user) if err != nil { return nil, err } else if !has { @@ -416,7 +463,7 @@ func GetUserByEmail(email string) (*User, error) { return nil, ErrUserNotExist } user := &User{Email: strings.ToLower(email)} - has, err := orm.Get(user) + has, err := x.Get(user) if err != nil { return nil, err } else if !has { @@ -440,7 +487,7 @@ func SearchUserByName(key string, limit int) (us []*User, err error) { key = strings.ToLower(key) us = make([]*User, 0, limit) - err = orm.Limit(limit).Where("lower_name like '%" + key + "%'").Find(&us) + err = x.Limit(limit).Where("lower_name like '%" + key + "%'").Find(&us) return us, err } @@ -453,7 +500,7 @@ type Follow struct { // FollowUser marks someone be another's follower. func FollowUser(userId int64, followId int64) (err error) { - session := orm.NewSession() + session := x.NewSession() defer session.Close() session.Begin() @@ -478,7 +525,7 @@ func FollowUser(userId int64, followId int64) (err error) { // UnFollowUser unmarks someone be another's follower. func UnFollowUser(userId int64, unFollowId int64) (err error) { - session := orm.NewSession() + session := x.NewSession() defer session.Close() session.Begin() diff --git a/models/webhook.go b/models/webhook.go index f10fa2131e..9044befba2 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -7,29 +7,35 @@ package models import ( "encoding/json" "errors" + "time" + "github.com/gogits/gogs/modules/httplib" "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/setting" ) var ( ErrWebhookNotExist = errors.New("Webhook does not exist") ) -// Content types. +type HookContentType int + const ( - CT_JSON = iota + 1 - CT_FORM + JSON HookContentType = iota + 1 + FORM ) +// HookEvent represents events that will delivery hook. type HookEvent struct { PushOnly bool `json:"push_only"` } +// Webhook represents a web hook object. type Webhook struct { Id int64 RepoId int64 Url string `xorm:"TEXT"` - ContentType int + ContentType HookContentType Secret string `xorm:"TEXT"` Events string `xorm:"TEXT"` *HookEvent `xorm:"-"` @@ -37,6 +43,7 @@ type Webhook struct { IsActive bool } +// GetEvent handles conversion from Events to HookEvent. func (w *Webhook) GetEvent() { w.HookEvent = &HookEvent{} if err := json.Unmarshal([]byte(w.Events), w.HookEvent); err != nil { @@ -44,12 +51,14 @@ func (w *Webhook) GetEvent() { } } -func (w *Webhook) SaveEvent() error { +// UpdateEvent handles conversion from HookEvent to Events. +func (w *Webhook) UpdateEvent() error { data, err := json.Marshal(w.HookEvent) w.Events = string(data) return err } +// HasPushEvent returns true if hook enbaled push event. func (w *Webhook) HasPushEvent() bool { if w.PushOnly { return true @@ -57,22 +66,16 @@ func (w *Webhook) HasPushEvent() bool { return false } -// CreateWebhook creates new webhook. +// CreateWebhook creates a new web hook. func CreateWebhook(w *Webhook) error { - _, err := orm.Insert(w) - return err -} - -// UpdateWebhook updates information of webhook. -func UpdateWebhook(w *Webhook) error { - _, err := orm.AllCols().Update(w) + _, err := x.Insert(w) return err } // GetWebhookById returns webhook by given ID. func GetWebhookById(hookId int64) (*Webhook, error) { w := &Webhook{Id: hookId} - has, err := orm.Get(w) + has, err := x.Get(w) if err != nil { return nil, err } else if !has { @@ -83,18 +86,124 @@ func GetWebhookById(hookId int64) (*Webhook, error) { // GetActiveWebhooksByRepoId returns all active webhooks of repository. func GetActiveWebhooksByRepoId(repoId int64) (ws []*Webhook, err error) { - err = orm.Find(&ws, &Webhook{RepoId: repoId, IsActive: true}) + err = x.Find(&ws, &Webhook{RepoId: repoId, IsActive: true}) return ws, err } // GetWebhooksByRepoId returns all webhooks of repository. func GetWebhooksByRepoId(repoId int64) (ws []*Webhook, err error) { - err = orm.Find(&ws, &Webhook{RepoId: repoId}) + err = x.Find(&ws, &Webhook{RepoId: repoId}) return ws, err } +// UpdateWebhook updates information of webhook. +func UpdateWebhook(w *Webhook) error { + _, err := x.AllCols().Update(w) + return err +} + // DeleteWebhook deletes webhook of repository. func DeleteWebhook(hookId int64) error { - _, err := orm.Delete(&Webhook{Id: hookId}) + _, err := x.Delete(&Webhook{Id: hookId}) return err } + +// ___ ___ __ ___________ __ +// / | \ ____ ____ | | _\__ ___/____ _____| | __ +// / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ / +// \ Y ( <_> | <_> ) < | | / __ \_\___ \| < +// \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \ +// \/ \/ \/ \/ \/ + +type HookTaskType int + +const ( + WEBHOOK HookTaskType = iota + 1 + SERVICE +) + +type PayloadAuthor struct { + Name string `json:"name"` + Email string `json:"email"` +} + +type PayloadCommit struct { + Id string `json:"id"` + Message string `json:"message"` + Url string `json:"url"` + Author *PayloadAuthor `json:"author"` +} + +type PayloadRepo struct { + Id int64 `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + Description string `json:"description"` + Website string `json:"website"` + Watchers int `json:"watchers"` + Owner *PayloadAuthor `json:"author"` + Private bool `json:"private"` +} + +// Payload represents a payload information of hook. +type Payload struct { + Secret string `json:"secret"` + Ref string `json:"ref"` + Commits []*PayloadCommit `json:"commits"` + Repo *PayloadRepo `json:"repository"` + Pusher *PayloadAuthor `json:"pusher"` +} + +// HookTask represents a hook task. +type HookTask struct { + Id int64 + Type HookTaskType + Url string + *Payload `xorm:"-"` + PayloadContent string `xorm:"TEXT"` + ContentType HookContentType + IsSsl bool + IsDeliveried bool +} + +// CreateHookTask creates a new hook task, +// it handles conversion from Payload to PayloadContent. +func CreateHookTask(t *HookTask) error { + data, err := json.Marshal(t.Payload) + if err != nil { + return err + } + t.PayloadContent = string(data) + _, err = x.Insert(t) + return err +} + +// UpdateHookTask updates information of hook task. +func UpdateHookTask(t *HookTask) error { + _, err := x.AllCols().Update(t) + return err +} + +// DeliverHooks checks and delivers undelivered hooks. +func DeliverHooks() { + timeout := time.Duration(setting.WebhookDeliverTimeout) * time.Second + x.Where("is_deliveried=?", false).Iterate(new(HookTask), + func(idx int, bean interface{}) error { + t := bean.(*HookTask) + // Only support JSON now. + if _, err := httplib.Post(t.Url).SetTimeout(timeout, timeout). + Body([]byte(t.PayloadContent)).Response(); err != nil { + log.Error("webhook.DeliverHooks(Delivery): %v", err) + return nil + } + + t.IsDeliveried = true + if err := UpdateHookTask(t); err != nil { + log.Error("webhook.DeliverHooks(UpdateHookTask): %v", err) + return nil + } + + log.Trace("Hook delivered: %s", t.PayloadContent) + return nil + }) +} diff --git a/modules/auth/org.go b/modules/auth/org.go new file mode 100644 index 0000000000..f87d10a707 --- /dev/null +++ b/modules/auth/org.go @@ -0,0 +1,57 @@ +// 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 auth + +import ( + "net/http" + "reflect" + + "github.com/go-martini/martini" + + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware/binding" +) + +type CreateOrgForm struct { + OrgName string `form:"orgname" binding:"Required;AlphaDashDot;MaxSize(30)"` + Email string `form:"email" binding:"Required;Email;MaxSize(50)"` +} + +func (f *CreateOrgForm) Name(field string) string { + names := map[string]string{ + "OrgName": "Organization name", + "Email": "E-mail address", + } + return names[field] +} + +func (f *CreateOrgForm) Validate(errs *binding.Errors, req *http.Request, ctx martini.Context) { + data := ctx.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) + validate(errs, data, f) +} + +type OrgSettingForm struct { + DisplayName string `form:"display_name" binding:"Required;MaxSize(100)"` + Email string `form:"email" binding:"Required;Email;MaxSize(50)"` + Description string `form:"desc" binding:"MaxSize(255)"` + Website string `form:"site" binding:"Url;MaxSize(100)"` + Location string `form:"location" binding:"MaxSize(50)"` +} + +func (f *OrgSettingForm) Name(field string) string { + names := map[string]string{ + "DisplayName": "Display name", + "Email": "E-mail address", + "Description": "Description", + "Website": "Website address", + "Location": "Location", + } + return names[field] +} + +func (f *OrgSettingForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { + data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) + validate(errors, data, f) +} diff --git a/modules/auth/repo.go b/modules/auth/repo.go index 82cd078613..d3d215322a 100644 --- a/modules/auth/repo.go +++ b/modules/auth/repo.go @@ -22,9 +22,10 @@ import ( // \/ \/|__| \/ \/ type CreateRepoForm struct { + Uid int64 `form:"uid" binding:"Required"` RepoName string `form:"repo" binding:"Required;AlphaDash;MaxSize(100)"` Private bool `form:"private"` - Description string `form:"desc" binding:"MaxSize(100)"` + Description string `form:"desc" binding:"MaxSize(255)"` Language string `form:"language"` License string `form:"license"` InitReadme bool `form:"initReadme"` @@ -47,10 +48,11 @@ type MigrateRepoForm struct { Url string `form:"url" binding:"Url"` AuthUserName string `form:"auth_username"` AuthPasswd string `form:"auth_password"` + Uid int64 `form:"uid" binding:"Required"` RepoName string `form:"repo" binding:"Required;AlphaDash;MaxSize(100)"` Mirror bool `form:"mirror"` Private bool `form:"private"` - Description string `form:"desc" binding:"MaxSize(100)"` + Description string `form:"desc" binding:"MaxSize(255)"` } func (f *MigrateRepoForm) Name(field string) string { @@ -69,7 +71,7 @@ func (f *MigrateRepoForm) Validate(errors *binding.Errors, req *http.Request, co type RepoSettingForm struct { RepoName string `form:"name" binding:"Required;AlphaDash;MaxSize(100)"` - Description string `form:"desc" binding:"MaxSize(100)"` + Description string `form:"desc" binding:"MaxSize(255)"` Website string `form:"site" binding:"Url;MaxSize(100)"` Branch string `form:"branch"` Interval int `form:"interval"` @@ -205,14 +207,17 @@ func (f *CreateLabelForm) Validate(errors *binding.Errors, req *http.Request, co type NewReleaseForm struct { TagName string `form:"tag_name" binding:"Required"` + Target string `form:"tag_target" binding:"Required"` Title string `form:"title" binding:"Required"` Content string `form:"content" binding:"Required"` + Draft string `form:"draft"` Prerelease bool `form:"prerelease"` } func (f *NewReleaseForm) Name(field string) string { names := map[string]string{ "TagName": "Tag name", + "Target": "Target", "Title": "Release title", "Content": "Release content", } @@ -223,3 +228,25 @@ func (f *NewReleaseForm) Validate(errors *binding.Errors, req *http.Request, con data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) validate(errors, data, f) } + +type EditReleaseForm struct { + Target string `form:"tag_target" binding:"Required"` + Title string `form:"title" binding:"Required"` + Content string `form:"content" binding:"Required"` + Draft string `form:"draft"` + Prerelease bool `form:"prerelease"` +} + +func (f *EditReleaseForm) Name(field string) string { + names := map[string]string{ + "Target": "Target", + "Title": "Release title", + "Content": "Release content", + } + return names[field] +} + +func (f *EditReleaseForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { + data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) + validate(errors, data, f) +} diff --git a/modules/auth/user.go b/modules/auth/user.go index e672a9c10f..4a781acfa5 100644 --- a/modules/auth/user.go +++ b/modules/auth/user.go @@ -16,57 +16,63 @@ import ( "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/middleware/binding" + "github.com/gogits/gogs/modules/setting" ) // SignedInId returns the id of signed in user. -func SignedInId(session session.SessionStore) int64 { +func SignedInId(header http.Header, sess session.SessionStore) int64 { if !models.HasEngine { return 0 } - userId := session.Get("userId") - if userId == nil { + if setting.Service.EnableReverseProxyAuth { + webAuthUser := header.Get(setting.ReverseProxyAuthUser) + if len(webAuthUser) > 0 { + u, err := models.GetUserByName(webAuthUser) + if err != nil { + if err != models.ErrUserNotExist { + log.Error("auth.user.SignedInId(GetUserByName): %v", err) + } + return 0 + } + return u.Id + } + } + + uid := sess.Get("userId") + if uid == nil { return 0 } - if s, ok := userId.(int64); ok { - if _, err := models.GetUserById(s); err != nil { + if id, ok := uid.(int64); ok { + if _, err := models.GetUserById(id); err != nil { + if err != models.ErrUserNotExist { + log.Error("auth.user.SignedInId(GetUserById): %v", err) + } return 0 } - return s + return id } return 0 } -// SignedInName returns the name of signed in user. -func SignedInName(session session.SessionStore) string { - userName := session.Get("userName") - if userName == nil { - return "" - } - if s, ok := userName.(string); ok { - return s - } - return "" -} - // SignedInUser returns the user object of signed user. -func SignedInUser(session session.SessionStore) *models.User { - id := SignedInId(session) - if id <= 0 { +func SignedInUser(header http.Header, sess session.SessionStore) *models.User { + uid := SignedInId(header, sess) + if uid <= 0 { return nil } - user, err := models.GetUserById(id) + u, err := models.GetUserById(uid) if err != nil { log.Error("user.SignedInUser: %v", err) return nil } - return user + return u } // IsSignedIn check if any user has signed in. -func IsSignedIn(session session.SessionStore) bool { - return SignedInId(session) > 0 +func IsSignedIn(header http.Header, sess session.SessionStore) bool { + return SignedInId(header, sess) > 0 } type FeedsForm struct { @@ -87,7 +93,7 @@ func (f *UpdateProfileForm) Name(field string) string { names := map[string]string{ "UserName": "Username", "Email": "E-mail address", - "Website": "Website", + "Website": "Website address", "Location": "Location", "Avatar": "Gravatar Email", } diff --git a/modules/base/base.go b/modules/base/base.go index 145fae6f13..570600c3d4 100644 --- a/modules/base/base.go +++ b/modules/base/base.go @@ -7,6 +7,7 @@ package base type ( // Type TmplData represents data in the templates. TmplData map[string]interface{} + TplName string ApiJsonErr struct { Message string `json:"message"` diff --git a/modules/bin/conf.go b/modules/bin/conf.go index f2b3fb8723..fa0822d732 100644 --- a/modules/bin/conf.go +++ b/modules/bin/conf.go @@ -1,11 +1,11 @@ package bin import ( - "bytes" - "compress/gzip" - "fmt" - "io" - "strings" + "bytes" + "compress/gzip" + "fmt" + "io" + "strings" ) func bindata_read(data []byte, name string) ([]byte, error) { @@ -27,227 +27,243 @@ func bindata_read(data []byte, name string) ([]byte, error) { func conf_app_ini() ([]byte, error) { return bindata_read([]byte{ - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0xb4, 0x58, - 0xed, 0x72, 0xdb, 0xb8, 0xd5, 0xfe, 0xcf, 0xab, 0x40, 0xf4, 0xee, 0xbe, - 0x9b, 0x74, 0x6c, 0x49, 0x76, 0x1a, 0x27, 0x6b, 0x6f, 0x66, 0x56, 0x96, - 0x28, 0x9b, 0x8d, 0xf5, 0x11, 0x92, 0x76, 0x9a, 0x66, 0x3c, 0x1c, 0x9a, - 0x84, 0x24, 0xd4, 0x24, 0x41, 0x13, 0x90, 0x15, 0xf5, 0x5f, 0x6f, 0xa1, - 0xd3, 0xab, 0xe9, 0xf5, 0xf4, 0x47, 0x2f, 0xa3, 0xcf, 0x01, 0x49, 0x99, - 0x52, 0xb4, 0xd9, 0xf4, 0x6b, 0x76, 0x27, 0x16, 0x81, 0x83, 0x83, 0x73, - 0x9e, 0xf3, 0x8d, 0x33, 0xd6, 0xcb, 0x73, 0x96, 0x85, 0x29, 0x67, 0x7a, - 0x11, 0x6a, 0xa6, 0x16, 0x72, 0xa5, 0x98, 0xcc, 0x18, 0x7f, 0xe4, 0xc5, - 0x9a, 0xe5, 0xe1, 0x1c, 0x1b, 0x42, 0x27, 0xdc, 0xea, 0x4d, 0xa7, 0xc1, - 0xb8, 0x37, 0xb2, 0xd9, 0x5b, 0x76, 0x21, 0xe7, 0xea, 0x14, 0xff, 0xb2, - 0x0b, 0xa1, 0x99, 0xc7, 0x8b, 0x47, 0x11, 0x95, 0xfb, 0x57, 0x93, 0x8b, - 0x09, 0xf6, 0x45, 0x3a, 0xef, 0xcc, 0x42, 0xac, 0xca, 0xac, 0x9d, 0x67, - 0x73, 0xeb, 0x8c, 0xf5, 0x17, 0x61, 0x06, 0x4e, 0x20, 0x17, 0x33, 0xb6, - 0x96, 0x4b, 0x56, 0x2c, 0x33, 0x96, 0xc8, 0x28, 0x4c, 0x92, 0xb5, 0xe5, - 0x5e, 0x8f, 0x83, 0x6b, 0xcf, 0x76, 0x71, 0x72, 0x2e, 0x34, 0xa8, 0x6d, - 0xa1, 0x17, 0xbc, 0x60, 0xad, 0x98, 0x3f, 0xb6, 0x0e, 0x58, 0x2b, 0x2f, - 0x64, 0xdc, 0x62, 0x12, 0x0b, 0x9a, 0x2b, 0x8d, 0x95, 0x98, 0xcf, 0xc2, - 0x65, 0x02, 0x5e, 0xaa, 0xa4, 0x31, 0x1c, 0x46, 0x93, 0x01, 0xc9, 0x86, - 0x6f, 0xcb, 0xfa, 0x54, 0xf0, 0x5c, 0x2a, 0xa1, 0x65, 0xb1, 0xbe, 0xb5, - 0xdc, 0xc9, 0xc4, 0xc7, 0x86, 0xe5, 0xf5, 0x5d, 0x67, 0xea, 0x07, 0xfe, - 0xc7, 0x29, 0xd1, 0xdd, 0x85, 0x6a, 0x01, 0x42, 0x05, 0xe9, 0x79, 0x71, - 0x6b, 0x4d, 0xdd, 0x89, 0x3f, 0xe9, 0x4f, 0xae, 0xb0, 0xb3, 0xd0, 0x3a, - 0xb7, 0x06, 0x93, 0x51, 0xcf, 0x19, 0xe3, 0xcb, 0x08, 0xb9, 0x90, 0x4a, - 0x1b, 0x3e, 0xc1, 0xb5, 0x4b, 0x24, 0xdf, 0x3f, 0xaf, 0xe9, 0x5f, 0xa8, - 0xd3, 0x4e, 0xe7, 0xfb, 0xe7, 0x25, 0x39, 0x3e, 0xbe, 0x7f, 0x7e, 0xe9, - 0xfb, 0xd3, 0x60, 0x3a, 0x71, 0xfd, 0x17, 0xaa, 0x63, 0x99, 0x8f, 0xde, - 0x60, 0x40, 0xba, 0x59, 0x9b, 0x1d, 0x7c, 0xbc, 0xec, 0x76, 0xbb, 0x96, - 0xe7, 0x5d, 0xd6, 0xdf, 0xc7, 0xc7, 0xd0, 0x7b, 0x20, 0x54, 0x78, 0x97, - 0x70, 0xd6, 0x1f, 0x8c, 0x09, 0xff, 0x8c, 0x89, 0xac, 0xd6, 0x3e, 0x95, - 0x31, 0xb7, 0x26, 0xc3, 0xe1, 0x95, 0x33, 0xb6, 0x6b, 0x55, 0x67, 0x61, - 0xa2, 0xb8, 0x35, 0x70, 0xbc, 0xde, 0xf9, 0x95, 0x1d, 0xb8, 0x93, 0x6b, - 0xdf, 0x76, 0xc9, 0x04, 0x9b, 0xad, 0x33, 0x76, 0xc1, 0x33, 0x5e, 0x84, - 0x9a, 0x33, 0xa5, 0x79, 0xae, 0x4e, 0xb1, 0xf2, 0x1d, 0x8b, 0x62, 0x98, - 0x55, 0x2f, 0x3a, 0x5a, 0x76, 0xe6, 0x30, 0x64, 0x27, 0x5a, 0x2a, 0x2d, - 0xd3, 0x0e, 0xa9, 0xad, 0x0c, 0xc1, 0x5c, 0x1a, 0xf3, 0x7c, 0x77, 0x31, - 0x21, 0x95, 0x3b, 0xaa, 0x88, 0x3a, 0xf9, 0xfd, 0xbc, 0x13, 0x15, 0xeb, - 0x1c, 0x67, 0x74, 0xa2, 0x3a, 0xf3, 0x8a, 0x6d, 0x10, 0xf1, 0x42, 0xb7, - 0x41, 0x7f, 0x18, 0x85, 0x6f, 0x75, 0xb1, 0xe4, 0xec, 0x30, 0x5e, 0x62, - 0x43, 0xc8, 0xec, 0xed, 0x9b, 0xd7, 0x27, 0xdd, 0x45, 0x37, 0xed, 0x2a, - 0x76, 0x48, 0xf0, 0xbd, 0x4d, 0xd7, 0xf4, 0xa7, 0xcd, 0x3f, 0x87, 0x69, - 0x9e, 0xf0, 0x76, 0x24, 0x53, 0xab, 0x6f, 0xbb, 0x7e, 0x30, 0x74, 0xae, - 0x48, 0x99, 0xa6, 0x14, 0x1d, 0xc3, 0x36, 0xe7, 0xa9, 0xf5, 0xce, 0xfe, - 0xb8, 0x97, 0xe0, 0x9e, 0xaf, 0xcd, 0xfe, 0x19, 0xbb, 0xce, 0x73, 0xb8, - 0x4a, 0x02, 0xb8, 0x12, 0x26, 0x67, 0x4c, 0x73, 0x70, 0x27, 0x85, 0xc3, - 0x2c, 0x86, 0xd2, 0x10, 0x25, 0x62, 0x33, 0x01, 0x4c, 0x49, 0x65, 0x90, - 0x37, 0x5c, 0x07, 0x3e, 0x66, 0x56, 0xd9, 0x0a, 0xce, 0xc6, 0x8d, 0x53, - 0xd3, 0x32, 0xff, 0xcc, 0xa3, 0xa5, 0xe6, 0xb1, 0xe5, 0xf9, 0x3d, 0xdf, - 0xe9, 0x07, 0xc6, 0xec, 0xd3, 0x9e, 0x7f, 0x49, 0x26, 0xb4, 0x3e, 0xc5, - 0xa1, 0x0e, 0xe1, 0x3b, 0xfc, 0xb6, 0xe1, 0xa7, 0xe9, 0x5a, 0x3d, 0x24, - 0xc6, 0x53, 0xa1, 0xe1, 0xbc, 0xe0, 0xaa, 0xf4, 0x56, 0x2c, 0x0a, 0xcd, - 0x5f, 0x62, 0x43, 0xe8, 0x1f, 0x14, 0xb9, 0x7d, 0xc1, 0xa2, 0x85, 0xa4, - 0x60, 0x19, 0x9c, 0xd7, 0x7e, 0x68, 0xce, 0x5a, 0x97, 0x13, 0x8f, 0xbc, - 0xe0, 0xe8, 0xf8, 0x75, 0xbb, 0x8b, 0xff, 0x8e, 0x4e, 0x5f, 0xbe, 0xec, - 0x9e, 0x58, 0x55, 0xb8, 0x91, 0x95, 0xac, 0x2a, 0x40, 0x0a, 0x29, 0xb5, - 0x35, 0xed, 0x79, 0xde, 0x87, 0x01, 0x7b, 0x0b, 0x11, 0x86, 0x74, 0x51, - 0xe3, 0xda, 0x2c, 0x59, 0x1f, 0x30, 0x5e, 0xc7, 0x4f, 0xe9, 0x4f, 0x24, - 0x59, 0xc1, 0x1f, 0x96, 0xa2, 0xe0, 0xa5, 0x60, 0xf0, 0x78, 0x31, 0x5b, - 0x1f, 0xce, 0x96, 0x49, 0xd2, 0x82, 0x13, 0x5e, 0x6d, 0x62, 0xa7, 0xa4, - 0xaf, 0xd9, 0xd6, 0xf2, 0x1b, 0xae, 0x56, 0x05, 0x01, 0xe9, 0x6f, 0xfc, - 0xa6, 0x1d, 0xdf, 0x01, 0x8e, 0x30, 0x4e, 0x45, 0x76, 0x6b, 0x02, 0x29, - 0x5a, 0x16, 0x42, 0x23, 0xde, 0x9c, 0x31, 0x90, 0xbb, 0xba, 0x82, 0x27, - 0xf6, 0xdf, 0x35, 0x5c, 0xf1, 0xd9, 0xb3, 0xfe, 0x65, 0x6f, 0x7c, 0x61, - 0x33, 0xff, 0xd2, 0xf1, 0x98, 0x3f, 0x61, 0xef, 0x6c, 0x7b, 0xca, 0x3e, - 0x4e, 0xae, 0x5d, 0x66, 0x74, 0x1b, 0xf4, 0xfc, 0x1e, 0xf3, 0x7a, 0x43, - 0xfb, 0xd9, 0x33, 0xcb, 0xb3, 0xfb, 0xae, 0xed, 0x07, 0xb0, 0x3e, 0x18, - 0x3c, 0xfb, 0xbf, 0x9f, 0x87, 0x03, 0xfb, 0x83, 0x8b, 0xff, 0xff, 0xff, - 0x37, 0xcf, 0xc1, 0xa9, 0xb7, 0xd4, 0xf2, 0x30, 0x91, 0x73, 0x44, 0x47, - 0xc1, 0x53, 0x9e, 0xde, 0x41, 0xd7, 0x38, 0x5c, 0x2b, 0x0b, 0xbe, 0xef, - 0x8c, 0x03, 0xd7, 0x1e, 0xd9, 0xa3, 0x73, 0x84, 0xc2, 0xa0, 0xf7, 0xd1, - 0xc3, 0xf9, 0xd7, 0x56, 0x7f, 0x32, 0x79, 0xe7, 0xd8, 0x26, 0xc7, 0x34, - 0x20, 0x0d, 0xc2, 0x15, 0x57, 0x32, 0xe5, 0xf5, 0xf6, 0xe6, 0x5c, 0x93, - 0x46, 0x64, 0x51, 0xc1, 0x63, 0x41, 0xa8, 0x94, 0xc9, 0x02, 0xd6, 0xbb, - 0xb5, 0x7a, 0x7d, 0xdf, 0xb9, 0xb1, 0x83, 0x3e, 0x60, 0x0b, 0xae, 0xe8, - 0xd7, 0xc8, 0x19, 0x23, 0xfa, 0xe8, 0xb6, 0xa3, 0x37, 0x5d, 0xcb, 0xb5, - 0x3d, 0x9b, 0x7c, 0x86, 0xac, 0xf4, 0x8b, 0x44, 0x70, 0x5d, 0xf0, 0x63, - 0x19, 0xe7, 0x31, 0xd3, 0x92, 0x21, 0x57, 0xce, 0x44, 0x91, 0x32, 0x7e, - 0x98, 0x86, 0x22, 0x61, 0x33, 0x18, 0xa0, 0xe0, 0x73, 0xa1, 0x74, 0x19, - 0x4e, 0xe0, 0x79, 0xe1, 0x78, 0x14, 0xe0, 0x36, 0x32, 0xcd, 0x15, 0xb8, - 0x8e, 0x87, 0x8e, 0x3b, 0x6a, 0xe0, 0x3b, 0x90, 0x5c, 0xb1, 0x4c, 0x6a, - 0x86, 0x9c, 0x2a, 0x57, 0xd5, 0x61, 0x5c, 0x40, 0x81, 0x60, 0xac, 0xc4, - 0xa0, 0x89, 0x89, 0x8c, 0x28, 0x92, 0xcb, 0x4c, 0x97, 0x56, 0xdd, 0x64, - 0x0f, 0xc3, 0xde, 0x85, 0xc7, 0x4f, 0xc6, 0x0d, 0xa6, 0x46, 0xc4, 0x14, - 0x91, 0xc7, 0x94, 0x98, 0x9b, 0x7c, 0x04, 0x51, 0x1f, 0x05, 0x5f, 0x81, - 0xed, 0x5a, 0x2f, 0x44, 0x36, 0x6f, 0x43, 0xb2, 0xf7, 0xd7, 0x8e, 0x6b, - 0x07, 0x9e, 0x73, 0x31, 0x06, 0xfc, 0x37, 0x8e, 0xfd, 0xa1, 0xc1, 0xa1, - 0x1f, 0x46, 0x88, 0xb3, 0xf0, 0x11, 0x6e, 0x03, 0x59, 0x14, 0xcb, 0x45, - 0xa4, 0x97, 0x05, 0xb7, 0xec, 0xb1, 0xb9, 0xb7, 0xdf, 0xeb, 0x5f, 0xda, - 0x41, 0xef, 0x06, 0xc6, 0x77, 0x1b, 0xa7, 0x46, 0x84, 0x01, 0x94, 0x11, - 0x33, 0x11, 0x95, 0xfa, 0x57, 0xf4, 0xe3, 0x89, 0xef, 0x0c, 0x3f, 0x06, - 0x84, 0xc1, 0x86, 0xdc, 0xfa, 0x44, 0x90, 0x51, 0x16, 0x2f, 0x89, 0x06, - 0x0d, 0x46, 0xe7, 0xcb, 0xd9, 0xcc, 0xe4, 0x87, 0x6c, 0x8e, 0x48, 0x47, - 0x82, 0x88, 0x50, 0x89, 0x32, 0x9e, 0x1c, 0xb0, 0x7b, 0xce, 0x73, 0x2a, - 0x48, 0x90, 0x49, 0x98, 0x7c, 0x50, 0x55, 0xa6, 0x58, 0x66, 0x3f, 0x68, - 0x76, 0x9f, 0x01, 0xc3, 0x15, 0x55, 0x44, 0xb3, 0xd9, 0x86, 0x4b, 0x8e, - 0x07, 0xc1, 0xf9, 0xf5, 0x70, 0x48, 0x39, 0xd6, 0x26, 0x8c, 0x8e, 0xc8, - 0x86, 0x63, 0xaa, 0x9c, 0x88, 0x1b, 0x24, 0x9d, 0x35, 0x0c, 0x09, 0x80, - 0x8c, 0xf9, 0xca, 0x92, 0xe9, 0x5d, 0x9f, 0xff, 0xce, 0xee, 0xfb, 0xa6, - 0x60, 0xd4, 0xe5, 0xf3, 0x85, 0xaa, 0xd5, 0x2b, 0x4b, 0x0f, 0x25, 0x69, - 0x3a, 0x72, 0xca, 0x54, 0xaa, 0xf3, 0xf6, 0x9c, 0x7e, 0x53, 0x72, 0x3c, - 0x7d, 0xf5, 0xe6, 0x35, 0xf6, 0xde, 0xbf, 0xaf, 0x36, 0x1e, 0x1e, 0xcc, - 0xea, 0xf1, 0xab, 0x3a, 0x57, 0xd4, 0x6c, 0x66, 0x85, 0x4c, 0x61, 0xe0, - 0x18, 0xf1, 0xaf, 0xac, 0xa1, 0x3b, 0x19, 0x3d, 0xed, 0x41, 0xf1, 0xa5, - 0xf1, 0x31, 0x12, 0x92, 0xfc, 0x20, 0x0f, 0x95, 0x5a, 0xc9, 0x22, 0xae, - 0xb3, 0xc9, 0x26, 0x93, 0x50, 0x66, 0x93, 0xe1, 0x52, 0x2f, 0xbe, 0xc4, - 0xb0, 0xda, 0x68, 0xa3, 0x34, 0x2f, 0x96, 0x77, 0x5f, 0xee, 0xf7, 0xaf, - 0x1c, 0x7b, 0xec, 0x07, 0x8e, 0xe1, 0x52, 0x7d, 0x94, 0xf1, 0x5b, 0x16, - 0xdd, 0xc9, 0xd4, 0xb8, 0xbc, 0xc9, 0xdb, 0xa8, 0x95, 0x61, 0x2e, 0x2a, - 0x56, 0xa4, 0x4f, 0x87, 0xe4, 0xb3, 0x7a, 0xd7, 0xfe, 0x65, 0x55, 0x59, - 0x6b, 0xb2, 0x06, 0x89, 0x89, 0xf4, 0x8e, 0x11, 0xa2, 0x43, 0xff, 0xc8, - 0x42, 0xfc, 0x89, 0x5b, 0xfe, 0xe4, 0x9d, 0x3d, 0xfe, 0xc6, 0x43, 0x51, - 0x04, 0x6c, 0x02, 0x2d, 0xef, 0x79, 0x66, 0x99, 0xa2, 0xa8, 0x59, 0x94, - 0x08, 0x8e, 0x18, 0x10, 0x71, 0x59, 0x28, 0x38, 0x62, 0x43, 0x1b, 0x28, - 0xb1, 0x5f, 0xb3, 0x43, 0x48, 0x2a, 0x89, 0x52, 0x15, 0x53, 0x71, 0x91, - 0x28, 0x33, 0x0a, 0xa5, 0x4e, 0xce, 0xcb, 0xe2, 0xd5, 0x41, 0x5d, 0xfe, - 0x23, 0x8f, 0xf4, 0x06, 0x1e, 0xb3, 0xf3, 0x1f, 0xc3, 0xb3, 0x5a, 0xad, - 0x2a, 0x56, 0x00, 0x4a, 0x99, 0x8b, 0x8c, 0x0e, 0x84, 0x93, 0xc8, 0x66, - 0xb2, 0xcd, 0x8d, 0x7f, 0x7d, 0x33, 0x39, 0xa4, 0xa4, 0xf2, 0xb7, 0x0f, - 0xe2, 0x2a, 0x0f, 0x6c, 0x29, 0x25, 0x4b, 0xc8, 0x8e, 0x0d, 0x97, 0xbd, - 0x18, 0x7f, 0xf5, 0x54, 0x05, 0x71, 0x05, 0xc9, 0xc3, 0xc3, 0xbf, 0x0d, - 0x07, 0x72, 0x98, 0x71, 0x7e, 0xf6, 0xf7, 0xbf, 0xfd, 0xe5, 0x1f, 0x7f, - 0xfe, 0x2b, 0x25, 0xfd, 0x3d, 0x3e, 0x52, 0x84, 0xf9, 0xa2, 0x0a, 0x8c, - 0x4a, 0x82, 0x76, 0xb7, 0xe1, 0x22, 0x67, 0x6c, 0xaf, 0x93, 0xec, 0x3d, - 0x55, 0x4a, 0x8e, 0x13, 0x3c, 0x8b, 0xc8, 0x31, 0x56, 0x5c, 0xdc, 0xc9, - 0x7d, 0xa8, 0xc1, 0x0f, 0xb2, 0xb6, 0xae, 0xcf, 0x47, 0x73, 0x71, 0x78, - 0x57, 0x3b, 0xda, 0xf1, 0xaf, 0xb8, 0xe7, 0xd7, 0x8f, 0x6e, 0x39, 0x69, - 0x85, 0xa0, 0x5e, 0x09, 0xad, 0xf7, 0x25, 0xb6, 0x7f, 0x01, 0xc6, 0x7d, - 0x96, 0x47, 0x0c, 0x56, 0xac, 0x9f, 0x50, 0xf8, 0x15, 0xe1, 0x7f, 0xe1, - 0xcc, 0x3e, 0xa9, 0x0d, 0x76, 0xff, 0x0b, 0x99, 0x0d, 0xe3, 0x86, 0xdd, - 0xbe, 0x41, 0xe4, 0x2f, 0x8f, 0x6c, 0x4b, 0x1c, 0x51, 0x79, 0xda, 0xea, - 0xe5, 0x78, 0x8a, 0xa9, 0xa1, 0x6c, 0x99, 0x90, 0xd7, 0xf1, 0x43, 0x96, - 0xab, 0x86, 0x72, 0x67, 0xf8, 0xa8, 0x88, 0xad, 0xde, 0xa0, 0x37, 0xf5, - 0x4d, 0x46, 0x2d, 0x57, 0xea, 0x0e, 0xaa, 0xda, 0xaf, 0xda, 0xb2, 0x8b, - 0x3e, 0xea, 0x03, 0xf0, 0x7b, 0x0c, 0x13, 0x2a, 0x14, 0x48, 0x3a, 0x32, - 0x8b, 0xd5, 0x16, 0xc7, 0x93, 0x2e, 0xda, 0x27, 0x70, 0xba, 0xe9, 0x91, - 0x22, 0x27, 0xdd, 0x9a, 0x51, 0x29, 0x8b, 0xc9, 0x55, 0x4d, 0x59, 0xc0, - 0x20, 0x43, 0x0e, 0x42, 0x7d, 0x64, 0xd4, 0x5c, 0x6f, 0xca, 0xc0, 0x19, - 0x33, 0x07, 0x4e, 0x59, 0xeb, 0xf4, 0xa4, 0xfb, 0xf2, 0xc7, 0x16, 0x16, - 0xea, 0x53, 0x58, 0x7b, 0xea, 0x32, 0x8f, 0x8e, 0x8e, 0x8f, 0x8e, 0x5a, - 0x55, 0x45, 0x31, 0x0d, 0x8e, 0x52, 0x60, 0xb6, 0x1f, 0x0f, 0xca, 0x23, - 0x4f, 0xb8, 0x94, 0xb0, 0x54, 0x8d, 0xef, 0x3e, 0x4c, 0x30, 0x21, 0xdd, - 0x38, 0x03, 0x03, 0x8a, 0xc9, 0x40, 0x67, 0x6c, 0x5a, 0xc8, 0x47, 0x11, - 0x83, 0xa9, 0xe9, 0x75, 0xe6, 0x4c, 0xe6, 0x24, 0xb9, 0x2a, 0x85, 0xc3, - 0x99, 0x53, 0xd3, 0xbe, 0x2c, 0xc2, 0x47, 0x2a, 0x56, 0xeb, 0x9a, 0x6a, - 0xcd, 0x69, 0x24, 0x24, 0x16, 0xa8, 0x84, 0xa5, 0x7c, 0x4f, 0x1d, 0x3d, - 0x7a, 0xdd, 0xf6, 0xbc, 0x8d, 0x4e, 0x97, 0xba, 0xd2, 0x6a, 0x57, 0xb5, - 0x9e, 0xf4, 0xaf, 0x78, 0x24, 0xe2, 0x9e, 0x97, 0x4b, 0x55, 0xd5, 0x35, - 0x48, 0x1d, 0xb0, 0x5c, 0xca, 0xc4, 0x83, 0xfb, 0x1c, 0x6c, 0x2a, 0x63, - 0xcd, 0xf0, 0x09, 0xa3, 0x93, 0x97, 0xaf, 0x7f, 0x3c, 0x38, 0xea, 0x76, - 0x0f, 0x42, 0x8c, 0x13, 0x9f, 0x05, 0x37, 0x60, 0x92, 0xde, 0xa7, 0xe8, - 0x10, 0x0f, 0xf1, 0xf7, 0x30, 0x2e, 0x04, 0x58, 0x76, 0xcc, 0x22, 0x8b, - 0x55, 0x56, 0xdf, 0x8a, 0xde, 0x0d, 0x0d, 0x52, 0xcd, 0x91, 0x3a, 0xf7, - 0xd3, 0xfa, 0x9a, 0x9f, 0x6b, 0x61, 0x03, 0x6d, 0x3a, 0xf4, 0x0d, 0x5a, - 0x65, 0x63, 0x77, 0x51, 0x37, 0xda, 0xb5, 0x4a, 0xb8, 0xd3, 0xab, 0x74, - 0x8f, 0xa4, 0xbc, 0x17, 0xdc, 0xd4, 0xf4, 0xba, 0x73, 0xad, 0x1a, 0x56, - 0x11, 0x90, 0x9e, 0x01, 0xfa, 0x56, 0xa1, 0xe9, 0x84, 0x53, 0x36, 0x34, - 0xa8, 0x05, 0x1b, 0xe0, 0xe0, 0x76, 0x26, 0x3a, 0x2a, 0x8f, 0x6c, 0xd8, - 0xad, 0x8a, 0xd1, 0x92, 0x21, 0xc2, 0xf2, 0xda, 0xb5, 0x1b, 0x6d, 0x94, - 0x9d, 0x99, 0xc1, 0x54, 0x51, 0xe5, 0x34, 0xf7, 0x6f, 0x9d, 0xa5, 0xc9, - 0xaf, 0x6e, 0xd0, 0xa8, 0xf3, 0x2d, 0xb9, 0xe0, 0xb8, 0xd9, 0x78, 0x12, - 0x1d, 0x01, 0xa0, 0x05, 0x5a, 0x91, 0x3a, 0x0a, 0xb6, 0x98, 0xbc, 0x39, - 0xf9, 0x2d, 0x46, 0xe2, 0x8b, 0x7e, 0x50, 0x07, 0x40, 0xe0, 0x3b, 0x46, - 0xad, 0x72, 0xe3, 0x89, 0x4b, 0x22, 0x66, 0xdc, 0xf0, 0xd9, 0x73, 0xdc, - 0xb3, 0x3d, 0x0f, 0x1d, 0x2c, 0xfa, 0xed, 0xa1, 0xbd, 0x7b, 0x7e, 0x83, - 0x41, 0x0c, 0x1f, 0x53, 0x0b, 0x36, 0x5b, 0x66, 0xd1, 0xc1, 0xc6, 0xcf, - 0xd5, 0x22, 0x3c, 0x22, 0xef, 0xc6, 0xdf, 0xe3, 0x57, 0x27, 0x95, 0x7b, - 0xc7, 0xaf, 0x5a, 0xcd, 0x3b, 0x88, 0x66, 0x73, 0x85, 0x33, 0x08, 0x2e, - 0x7b, 0xde, 0xe5, 0xf0, 0x7a, 0xdc, 0xc7, 0x25, 0x66, 0xeb, 0x49, 0x46, - 0x73, 0x01, 0x86, 0xd4, 0x2d, 0x11, 0xc9, 0x10, 0x05, 0x42, 0x18, 0xfd, - 0x5a, 0xe9, 0x1a, 0xbb, 0xbc, 0xcc, 0xbc, 0x83, 0x30, 0xac, 0x7a, 0x64, - 0x0a, 0x43, 0x9f, 0x86, 0xd4, 0x24, 0x8c, 0x38, 0x35, 0xde, 0xd5, 0xba, - 0x71, 0x8d, 0xa7, 0x29, 0xaf, 0xf4, 0xe8, 0x52, 0xe2, 0x07, 0x91, 0x89, - 0xe5, 0x4e, 0x40, 0x56, 0xfb, 0xb8, 0xcc, 0xbd, 0x71, 0xfa, 0x84, 0x48, - 0xd5, 0x79, 0xd6, 0xbd, 0xff, 0x85, 0xbb, 0xd3, 0x7f, 0x5b, 0x9f, 0xd0, - 0x3e, 0x95, 0x0f, 0x27, 0xd5, 0xe4, 0xdb, 0x48, 0x08, 0x55, 0x57, 0xd4, - 0xcc, 0x08, 0x94, 0x86, 0x0c, 0x76, 0x68, 0x54, 0x4b, 0x39, 0xea, 0x29, - 0x79, 0x47, 0x94, 0xfa, 0x6c, 0x39, 0x59, 0xc0, 0x95, 0xd2, 0x34, 0x24, - 0xc5, 0x14, 0xcf, 0x43, 0xf3, 0x4c, 0x91, 0x82, 0x52, 0xe4, 0xf0, 0x34, - 0x7a, 0xef, 0x50, 0x75, 0xe8, 0x54, 0xc7, 0x0e, 0x4c, 0xdc, 0xb7, 0xac, - 0x6a, 0x5a, 0xad, 0x56, 0xff, 0x9b, 0x4d, 0xfe, 0x4e, 0x7f, 0xdf, 0x35, - 0x7e, 0x53, 0x2b, 0xee, 0x17, 0x30, 0x03, 0xa9, 0x39, 0xe0, 0x77, 0xcb, - 0x39, 0xfd, 0x70, 0xd0, 0x61, 0xd1, 0xdf, 0x0f, 0x61, 0x61, 0xf4, 0xb7, - 0x8b, 0x42, 0x16, 0xf4, 0xa3, 0x8f, 0x49, 0x18, 0x83, 0xcb, 0x6e, 0x6a, - 0x2c, 0x39, 0x58, 0x57, 0xf6, 0x8d, 0x4d, 0xe9, 0xdd, 0x7c, 0x5a, 0x75, - 0x8a, 0xaf, 0xb1, 0x31, 0xaa, 0x97, 0xc3, 0x19, 0x99, 0xa1, 0x5d, 0xad, - 0xdf, 0x6e, 0x8e, 0x6d, 0x4e, 0x18, 0x34, 0x76, 0xc9, 0x69, 0xb1, 0x41, - 0x4b, 0x8f, 0x27, 0x75, 0x7e, 0xc0, 0x76, 0x39, 0xb9, 0xe3, 0x87, 0x71, - 0x2d, 0x7a, 0xed, 0x30, 0x81, 0xad, 0x18, 0x8a, 0xa3, 0x4c, 0x61, 0x81, - 0x98, 0xa8, 0x58, 0x21, 0x35, 0x7e, 0x3f, 0x57, 0xa8, 0xf7, 0x91, 0x01, - 0x74, 0x26, 0x69, 0xa8, 0x84, 0xcb, 0xd6, 0x49, 0xfb, 0xc5, 0x97, 0x09, - 0x00, 0xd3, 0x77, 0xe0, 0x4e, 0xfc, 0x9e, 0xdf, 0x88, 0xfc, 0x51, 0xf8, - 0x19, 0xf1, 0x9a, 0x21, 0x5d, 0x2d, 0xcd, 0x98, 0x0e, 0x56, 0x0a, 0x5c, - 0x60, 0x60, 0x92, 0x73, 0x8b, 0x87, 0x81, 0x1b, 0x80, 0x8f, 0x7a, 0xbf, - 0x0f, 0xe8, 0x95, 0xcb, 0xab, 0x4d, 0x60, 0x8c, 0x40, 0x8c, 0x14, 0x32, - 0x35, 0x02, 0x4d, 0xcc, 0xf4, 0xd7, 0xf8, 0x1c, 0xbf, 0x41, 0x39, 0x09, - 0x33, 0x30, 0x64, 0x3f, 0xfd, 0x84, 0xaf, 0x03, 0x86, 0x78, 0x1e, 0x9d, - 0x1b, 0xbe, 0x9e, 0xf3, 0x07, 0x64, 0xa8, 0x4b, 0x67, 0x68, 0x9e, 0xdc, - 0xde, 0x98, 0x80, 0x9d, 0xa7, 0xd4, 0xef, 0x91, 0xd6, 0x31, 0x3a, 0xeb, - 0xf5, 0x97, 0x7a, 0x0d, 0x30, 0x6b, 0x7e, 0xfc, 0x42, 0x33, 0xfb, 0x73, - 0x2e, 0x50, 0x51, 0xcc, 0xc3, 0x03, 0x89, 0x43, 0x0c, 0x48, 0x96, 0xe7, - 0x31, 0x4f, 0x38, 0x4d, 0xd9, 0x33, 0x1a, 0xbe, 0x53, 0x88, 0x4d, 0x14, - 0xdb, 0x70, 0xbd, 0x36, 0xc2, 0x6c, 0x9e, 0x27, 0x1a, 0x1e, 0x90, 0xed, - 0x33, 0x7f, 0xd6, 0xb0, 0xe7, 0x19, 0x73, 0x79, 0x55, 0xf5, 0xcb, 0x92, - 0x4f, 0x0f, 0x05, 0xe5, 0x5b, 0x6d, 0x05, 0x48, 0x8a, 0x14, 0x14, 0xce, - 0xf9, 0x9e, 0xe4, 0xee, 0xda, 0x28, 0x2e, 0x63, 0x0c, 0xa4, 0x01, 0x52, - 0xce, 0xc8, 0x6b, 0xbe, 0x13, 0xfa, 0x38, 0x8f, 0x38, 0x2c, 0x36, 0xbc, - 0x57, 0x0b, 0x9e, 0x35, 0xdb, 0x0b, 0x30, 0x49, 0x70, 0xdd, 0xd7, 0xb8, - 0x36, 0xcb, 0x45, 0x15, 0x32, 0x3a, 0xca, 0x29, 0x1c, 0x96, 0x99, 0xf8, - 0x5c, 0xe6, 0x85, 0x65, 0x9c, 0xef, 0xc4, 0x04, 0x91, 0x34, 0x5f, 0x5f, - 0xf1, 0x0d, 0x06, 0x97, 0xcd, 0x6e, 0xa6, 0x7e, 0x3f, 0xdd, 0xbc, 0x4b, - 0x99, 0x34, 0xb3, 0x83, 0x13, 0x2d, 0x6e, 0xe1, 0xf4, 0xb5, 0xc9, 0x7c, - 0x5b, 0x84, 0x81, 0x08, 0xe7, 0x19, 0x2e, 0x14, 0x51, 0x0d, 0x5e, 0x39, - 0x54, 0x9b, 0x34, 0xd9, 0x6a, 0x4c, 0xf1, 0x5f, 0x25, 0xdc, 0x19, 0xeb, - 0xb7, 0xa7, 0xf4, 0x6f, 0x9f, 0xc4, 0x4b, 0x0b, 0x73, 0xea, 0x28, 0x90, - 0xff, 0xa2, 0x30, 0x63, 0x77, 0xa4, 0x26, 0x27, 0xf8, 0xd0, 0x24, 0xf1, - 0x2a, 0x27, 0x7e, 0x6a, 0x1d, 0xfd, 0xdc, 0x78, 0x4a, 0x6d, 0x1d, 0xb4, - 0x8e, 0xb7, 0xbe, 0x6f, 0xc9, 0x2e, 0xb6, 0x73, 0x63, 0xbb, 0x5e, 0x13, - 0xba, 0x4d, 0x5e, 0xde, 0x85, 0xef, 0xe9, 0x59, 0x73, 0x03, 0xe1, 0xc0, - 0xa5, 0xe3, 0xa6, 0x59, 0x87, 0x81, 0xe9, 0xef, 0x3f, 0x03, 0x00, 0x00, - 0xff, 0xff, 0x77, 0x1f, 0xe2, 0xa5, 0x2d, 0x18, 0x00, 0x00, - }, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x00, 0xff, 0xb4, 0x59, + 0xeb, 0x72, 0xdb, 0xc8, 0x95, 0xfe, 0x8f, 0xa7, 0x68, 0x73, 0x67, 0x76, + 0xec, 0x2d, 0x89, 0xa4, 0xe4, 0xb5, 0xec, 0x91, 0xc7, 0xb5, 0xa6, 0x48, + 0x50, 0xc2, 0x9a, 0x17, 0x0d, 0x00, 0xc9, 0xa3, 0xb8, 0x54, 0x28, 0x08, + 0x68, 0x92, 0x1d, 0x01, 0x68, 0x08, 0xdd, 0x14, 0xc5, 0xfc, 0xcb, 0x2b, + 0xa4, 0xf2, 0x34, 0x79, 0x9e, 0xfc, 0xc8, 0x63, 0xe4, 0x3b, 0x0d, 0x80, + 0x02, 0x65, 0x8e, 0xc6, 0xb9, 0x55, 0x52, 0x16, 0xd1, 0xdd, 0xe7, 0xf4, + 0xb9, 0x7c, 0xe7, 0xd6, 0xf3, 0x9e, 0xf5, 0xf2, 0x9c, 0x65, 0x61, 0xca, + 0x99, 0x5e, 0x84, 0x9a, 0xa9, 0x85, 0x5c, 0x29, 0x26, 0x33, 0xc6, 0xef, + 0x79, 0xb1, 0x66, 0x79, 0x38, 0xc7, 0x86, 0xd0, 0x09, 0xb7, 0x7a, 0xe7, + 0xe7, 0xc1, 0xa4, 0x37, 0xb6, 0xd9, 0x07, 0x76, 0x2a, 0xe7, 0xea, 0x18, + 0xff, 0xb2, 0x53, 0xa1, 0x99, 0xc7, 0x8b, 0x7b, 0x11, 0x95, 0xfb, 0xa3, + 0xe9, 0xe9, 0x14, 0xfb, 0x22, 0x9d, 0x77, 0x66, 0x21, 0x56, 0x65, 0xd6, + 0xce, 0xb3, 0xb9, 0xf5, 0x9e, 0xf5, 0x17, 0x61, 0x06, 0x4e, 0x38, 0x2e, + 0x66, 0x6c, 0x2d, 0x97, 0xac, 0x58, 0x66, 0x2c, 0x91, 0x51, 0x98, 0x24, + 0x6b, 0xcb, 0xbd, 0x98, 0x04, 0x17, 0x9e, 0xed, 0x82, 0x72, 0x2e, 0x34, + 0x4e, 0xdb, 0x42, 0x2f, 0x78, 0xc1, 0x5a, 0x31, 0xbf, 0x6f, 0xed, 0xb1, + 0x56, 0x5e, 0xc8, 0xb8, 0xc5, 0x24, 0x16, 0x34, 0x57, 0x1a, 0x2b, 0x31, + 0x9f, 0x85, 0xcb, 0x04, 0xbc, 0x54, 0x79, 0xc6, 0x70, 0x18, 0x4f, 0x07, + 0x24, 0x1b, 0xbe, 0x2d, 0xeb, 0x4b, 0xc1, 0x73, 0xa9, 0x84, 0x96, 0xc5, + 0xfa, 0xda, 0x72, 0xa7, 0x53, 0x1f, 0x1b, 0x96, 0xd7, 0x77, 0x9d, 0x73, + 0x3f, 0xf0, 0xaf, 0xce, 0xe9, 0xdc, 0x4d, 0xa8, 0x16, 0x38, 0xa8, 0x20, + 0x3d, 0x2f, 0xae, 0xad, 0x73, 0x77, 0xea, 0x4f, 0xfb, 0xd3, 0x11, 0x76, + 0x16, 0x5a, 0xe7, 0xd6, 0x60, 0x3a, 0xee, 0x39, 0x13, 0x7c, 0x19, 0x21, + 0x17, 0x52, 0x69, 0xc3, 0x27, 0xb8, 0x70, 0xe9, 0xc8, 0xf7, 0x2f, 0xeb, + 0xf3, 0xaf, 0xd4, 0x71, 0xa7, 0xf3, 0xfd, 0xcb, 0xf2, 0x38, 0x3e, 0xbe, + 0x7f, 0x79, 0xe6, 0xfb, 0xe7, 0xc1, 0xf9, 0xd4, 0xf5, 0x5f, 0xa9, 0x8e, + 0x65, 0x3e, 0x7a, 0x83, 0x01, 0xe9, 0x66, 0x6d, 0x76, 0xf0, 0xf1, 0xba, + 0xdb, 0xed, 0x5a, 0x9e, 0x77, 0x56, 0x7f, 0x1f, 0x1e, 0x42, 0xef, 0x81, + 0x50, 0xe1, 0x4d, 0xc2, 0x59, 0x7f, 0x30, 0x21, 0xfb, 0x67, 0x4c, 0x64, + 0xb5, 0xf6, 0xa9, 0x8c, 0xb9, 0x35, 0x1d, 0x0e, 0x47, 0xce, 0xc4, 0xae, + 0x55, 0x9d, 0x85, 0x89, 0xe2, 0xd6, 0xc0, 0xf1, 0x7a, 0x27, 0x23, 0x3b, + 0x70, 0xa7, 0x17, 0xbe, 0xed, 0x92, 0x0b, 0x36, 0x5b, 0xef, 0xd9, 0x29, + 0xcf, 0x78, 0x11, 0x6a, 0xce, 0x94, 0xe6, 0xb9, 0x3a, 0xc6, 0xca, 0x77, + 0x2c, 0x8a, 0xe1, 0x56, 0xbd, 0xe8, 0x68, 0xd9, 0x99, 0xc3, 0x91, 0x9d, + 0x68, 0xa9, 0xb4, 0x4c, 0x3b, 0xa4, 0xb6, 0x32, 0x07, 0xe6, 0xd2, 0xb8, + 0xe7, 0xbb, 0xd3, 0x29, 0xa9, 0xdc, 0x51, 0x45, 0xd4, 0xc9, 0x6f, 0xe7, + 0x9d, 0xa8, 0x58, 0xe7, 0xa0, 0xd1, 0x89, 0xea, 0xcc, 0x2b, 0xb6, 0x41, + 0xc4, 0x0b, 0xdd, 0xc6, 0xf9, 0xfd, 0x28, 0xfc, 0xa0, 0x8b, 0x25, 0x67, + 0xfb, 0xf1, 0x12, 0x1b, 0x42, 0x66, 0x1f, 0xde, 0xbd, 0x3d, 0xea, 0x2e, + 0xba, 0x69, 0x57, 0xb1, 0x7d, 0x32, 0xdf, 0x87, 0x74, 0x4d, 0x7f, 0xda, + 0xfc, 0x21, 0x4c, 0xf3, 0x84, 0xb7, 0x23, 0x99, 0x5a, 0x7d, 0xdb, 0xf5, + 0x83, 0xa1, 0x33, 0x22, 0x65, 0x9a, 0x52, 0x74, 0x0c, 0xdb, 0x9c, 0xa7, + 0xd6, 0x27, 0xfb, 0x6a, 0xe7, 0x81, 0x5b, 0xbe, 0x36, 0xfb, 0xef, 0xd9, + 0x45, 0x9e, 0x03, 0x2a, 0x09, 0xcc, 0x95, 0x30, 0x39, 0x63, 0x9a, 0x83, + 0x3b, 0x29, 0x1c, 0x66, 0x31, 0x94, 0x86, 0x28, 0x11, 0x9b, 0x09, 0xd8, + 0x94, 0x54, 0xc6, 0xf1, 0x06, 0x74, 0x80, 0x31, 0xb3, 0xca, 0x56, 0x00, + 0x1b, 0x37, 0xa0, 0xa6, 0x65, 0xfe, 0xc0, 0xa3, 0xa5, 0xe6, 0xb1, 0xe5, + 0xf9, 0x3d, 0xdf, 0xe9, 0x07, 0xc6, 0xed, 0xe7, 0x3d, 0xff, 0x8c, 0x5c, + 0x68, 0x7d, 0x89, 0x43, 0x1d, 0x02, 0x3b, 0xfc, 0xba, 0x81, 0xd3, 0x74, + 0xad, 0xee, 0x12, 0x83, 0x54, 0x68, 0x38, 0x2f, 0xb8, 0x2a, 0xd1, 0x8a, + 0x45, 0xa1, 0xf9, 0x6b, 0x6c, 0x08, 0xfd, 0x83, 0x22, 0xd8, 0x17, 0x2c, + 0x5a, 0x48, 0x0a, 0x96, 0xc1, 0x49, 0x8d, 0x43, 0x43, 0x6b, 0x9d, 0x4d, + 0x3d, 0x42, 0xc1, 0xc1, 0xe1, 0xdb, 0x76, 0x17, 0xff, 0x3b, 0x38, 0x7e, + 0xfd, 0xba, 0x7b, 0x64, 0x55, 0xe1, 0x46, 0x5e, 0xb2, 0xaa, 0x00, 0x29, + 0xa4, 0xd4, 0xd6, 0x79, 0xcf, 0xf3, 0x3e, 0x0f, 0xd8, 0x07, 0x88, 0x30, + 0xa4, 0x8b, 0x1a, 0xd7, 0x66, 0xc9, 0x7a, 0x8f, 0xf1, 0x3a, 0x7e, 0x4a, + 0x3c, 0x91, 0x64, 0x05, 0xbf, 0x5b, 0x8a, 0x82, 0x97, 0x82, 0x01, 0xf1, + 0x62, 0xb6, 0xde, 0x9f, 0x2d, 0x93, 0xa4, 0x05, 0x10, 0x8e, 0x36, 0xb1, + 0x53, 0x9e, 0xaf, 0xd9, 0xd6, 0xf2, 0x1b, 0xae, 0x56, 0x65, 0x02, 0xd2, + 0xdf, 0xe0, 0xa6, 0x1d, 0xdf, 0xc0, 0x1c, 0x61, 0x9c, 0x8a, 0xec, 0xda, + 0x04, 0x52, 0xb4, 0x2c, 0x84, 0x46, 0xbc, 0x39, 0x13, 0x58, 0x6e, 0x34, + 0x02, 0x12, 0xfb, 0x9f, 0x1a, 0x50, 0x7c, 0xf1, 0xa2, 0x7f, 0xd6, 0x9b, + 0x9c, 0xda, 0xcc, 0x3f, 0x73, 0x3c, 0xe6, 0x4f, 0xd9, 0x27, 0xdb, 0x3e, + 0x67, 0x57, 0xd3, 0x0b, 0x97, 0x19, 0xdd, 0x06, 0x3d, 0xbf, 0xc7, 0xbc, + 0xde, 0xd0, 0x7e, 0xf1, 0xc2, 0xf2, 0xec, 0xbe, 0x6b, 0xfb, 0x01, 0xbc, + 0x0f, 0x06, 0x2f, 0xfe, 0xeb, 0xe3, 0x70, 0x60, 0x7f, 0x76, 0xf1, 0xff, + 0xff, 0xfe, 0x9f, 0x97, 0xe0, 0xd4, 0x5b, 0x6a, 0xb9, 0x9f, 0xc8, 0x39, + 0xa2, 0xa3, 0xe0, 0x29, 0x4f, 0x6f, 0xa0, 0x6b, 0x1c, 0xae, 0x95, 0x05, + 0xec, 0x3b, 0x93, 0xc0, 0xb5, 0xc7, 0xf6, 0xf8, 0x04, 0xa1, 0x30, 0xe8, + 0x5d, 0x79, 0xa0, 0x7f, 0x6b, 0xf5, 0xa7, 0xd3, 0x4f, 0x8e, 0x6d, 0x72, + 0x4c, 0xc3, 0xa4, 0x41, 0xb8, 0xe2, 0x4a, 0xa6, 0xbc, 0xde, 0xde, 0xd0, + 0x35, 0xcf, 0x88, 0x2c, 0x2a, 0x78, 0x2c, 0x4a, 0xab, 0xb8, 0x94, 0x14, + 0x15, 0x50, 0x53, 0xc8, 0x87, 0x35, 0x0b, 0x97, 0xb0, 0x72, 0x06, 0x80, + 0x19, 0xbc, 0xb3, 0x05, 0x0f, 0x63, 0x08, 0x62, 0x52, 0x29, 0x80, 0xb8, + 0x54, 0xd5, 0x87, 0xe5, 0xda, 0x97, 0xb6, 0xeb, 0xd9, 0x01, 0x52, 0xc6, + 0x2f, 0x57, 0x41, 0xef, 0xc2, 0x3f, 0xb3, 0x27, 0x00, 0x16, 0xc0, 0x35, + 0xdd, 0xe4, 0xbd, 0x5f, 0xf6, 0x3f, 0xdb, 0x27, 0xb4, 0xb5, 0x4f, 0x0b, + 0x55, 0x5e, 0x02, 0x50, 0xae, 0xad, 0x5e, 0xdf, 0x77, 0x2e, 0xed, 0xa0, + 0x0f, 0x0f, 0x05, 0x23, 0xfa, 0x35, 0x76, 0x26, 0x08, 0x74, 0x52, 0xec, + 0xe0, 0x5d, 0x17, 0xcc, 0x3d, 0x9b, 0xe0, 0x49, 0x80, 0xf8, 0xd5, 0x43, + 0x88, 0x12, 0x23, 0x0d, 0xe7, 0x31, 0xd3, 0x92, 0x21, 0x2d, 0xcf, 0x44, + 0x91, 0x32, 0xbe, 0x9f, 0x86, 0x22, 0x61, 0x33, 0xf8, 0xba, 0xe0, 0x73, + 0xa1, 0x74, 0x19, 0xb9, 0xe0, 0x79, 0xea, 0x78, 0x94, 0x4b, 0x6c, 0x24, + 0xb5, 0x11, 0xb8, 0x4e, 0x86, 0x8e, 0x3b, 0x6e, 0xb8, 0x72, 0x20, 0xb9, + 0x62, 0x99, 0xd4, 0x0c, 0xe9, 0x5b, 0xae, 0x2a, 0x62, 0x5c, 0x40, 0x31, + 0x67, 0x00, 0xc1, 0x60, 0x34, 0x13, 0x84, 0x51, 0x24, 0x97, 0x99, 0x2e, + 0x01, 0xb4, 0x49, 0x54, 0x86, 0xbd, 0x6b, 0xf4, 0x6f, 0x30, 0x35, 0x22, + 0xa6, 0x08, 0x72, 0xa6, 0xc4, 0xdc, 0xa4, 0x3e, 0x88, 0x7a, 0x2f, 0xf8, + 0x0a, 0x6c, 0xd7, 0x7a, 0x21, 0xb2, 0x79, 0x1b, 0x92, 0xfd, 0x7c, 0xe1, + 0xb8, 0x76, 0xe0, 0x39, 0xa7, 0x13, 0x78, 0xfa, 0xd2, 0xb1, 0x3f, 0x37, + 0x38, 0xf4, 0xc3, 0x08, 0x21, 0x1d, 0xde, 0x03, 0xa1, 0x90, 0x45, 0xb1, + 0x5c, 0x44, 0x7a, 0x59, 0x70, 0xcb, 0x9e, 0x98, 0x7b, 0xfb, 0xbd, 0xfe, + 0x99, 0x1d, 0xf4, 0x2e, 0x81, 0x33, 0xb7, 0x41, 0x35, 0x26, 0x1b, 0x40, + 0x19, 0x31, 0xab, 0x3c, 0x59, 0x9f, 0x9f, 0x4c, 0x7d, 0x67, 0x78, 0x15, + 0x90, 0x0d, 0x9a, 0xc7, 0x25, 0x72, 0x45, 0xcc, 0x35, 0xa8, 0x8e, 0x4d, + 0xa9, 0xa0, 0x02, 0x80, 0xb2, 0xb5, 0x58, 0xde, 0x50, 0x4e, 0xa3, 0xd0, + 0x10, 0x5a, 0x95, 0x99, 0x55, 0x28, 0xb5, 0xe4, 0xaa, 0x73, 0x70, 0xf4, + 0xa6, 0xe6, 0xf9, 0x1c, 0x16, 0x36, 0x97, 0x58, 0x5f, 0x56, 0xfc, 0x66, + 0x21, 0xe5, 0x2d, 0xe5, 0x98, 0x7e, 0x01, 0x6c, 0xe9, 0x50, 0xdd, 0xc2, + 0x22, 0xb0, 0xf1, 0x7d, 0x98, 0x90, 0x69, 0x60, 0x63, 0xe4, 0x28, 0x65, + 0xf9, 0x3d, 0xef, 0x53, 0xe0, 0x4c, 0xe0, 0xac, 0xcb, 0x1e, 0x49, 0x79, + 0x40, 0xde, 0xe1, 0x89, 0x00, 0x4e, 0x51, 0xb6, 0x53, 0x2e, 0x97, 0x9a, + 0x8e, 0x23, 0x38, 0x65, 0x16, 0x2b, 0x6b, 0x60, 0x13, 0x3a, 0xdc, 0xc0, + 0x77, 0xc6, 0x36, 0xca, 0x05, 0x08, 0xde, 0xe0, 0x36, 0x42, 0x01, 0xd5, + 0xc0, 0x52, 0xc6, 0x41, 0x43, 0xd9, 0x93, 0xe5, 0x6c, 0x66, 0xb2, 0x6b, + 0x36, 0x47, 0x9e, 0x04, 0xaa, 0x23, 0xd4, 0xf1, 0x8c, 0x27, 0x7b, 0xec, + 0x96, 0xf3, 0x9c, 0xca, 0x39, 0xcc, 0x2c, 0x4c, 0x36, 0xad, 0xea, 0x7a, + 0x2c, 0xb3, 0x1f, 0x34, 0xbb, 0xcd, 0x00, 0x8b, 0x15, 0xf5, 0x13, 0x66, + 0xb3, 0x8d, 0x80, 0x9e, 0x0c, 0x82, 0x93, 0x8b, 0xe1, 0x90, 0x2a, 0x94, + 0x4d, 0xaa, 0x1e, 0x10, 0x2c, 0x27, 0x14, 0x2c, 0xc8, 0x3a, 0x48, 0xd9, + 0x6b, 0x60, 0x93, 0x14, 0x23, 0x6f, 0x94, 0x0d, 0x87, 0x77, 0x71, 0xf2, + 0xff, 0x76, 0xdf, 0x37, 0xe5, 0xb6, 0x6e, 0x3e, 0x5e, 0xa9, 0xda, 0x63, + 0x65, 0xe1, 0xa6, 0x12, 0x97, 0x1a, 0x57, 0xa8, 0x54, 0xe7, 0xed, 0x39, + 0xfd, 0x26, 0x37, 0x1c, 0xbf, 0x79, 0xf7, 0x16, 0x7b, 0x3f, 0xff, 0x5c, + 0x6d, 0xdc, 0xdd, 0x99, 0xd5, 0xc3, 0x37, 0x75, 0xa6, 0xad, 0xd9, 0xcc, + 0x0a, 0x99, 0x02, 0xb3, 0x31, 0xb2, 0xa7, 0xb2, 0x86, 0xee, 0x74, 0xfc, + 0xb8, 0x07, 0xc5, 0x37, 0x41, 0x6c, 0xa0, 0x9d, 0x87, 0x4a, 0xad, 0x64, + 0x11, 0xd7, 0xb9, 0x78, 0x93, 0x87, 0xa9, 0x2e, 0x48, 0x4a, 0x07, 0x5f, + 0xdb, 0xb0, 0xda, 0x68, 0x97, 0x08, 0xf9, 0x7a, 0xbf, 0x3f, 0x72, 0x80, + 0x80, 0xc0, 0x31, 0x5c, 0xaa, 0x8f, 0x32, 0xfb, 0x95, 0x2d, 0xcb, 0xf4, + 0xdc, 0x44, 0x71, 0x0d, 0xb4, 0x30, 0x17, 0xed, 0x06, 0xd8, 0x48, 0x3e, + 0x8b, 0x50, 0x54, 0xf5, 0x25, 0x3b, 0xf0, 0x68, 0xf2, 0x64, 0xc7, 0x08, + 0xd1, 0xa1, 0x7f, 0x64, 0x21, 0xfe, 0xc0, 0x2d, 0x7f, 0xfa, 0xc9, 0x9e, + 0x7c, 0x23, 0x51, 0x14, 0xc1, 0x36, 0x81, 0x96, 0xb7, 0x3c, 0xb3, 0x4c, + 0x4b, 0xa1, 0x59, 0x94, 0x08, 0x64, 0x3e, 0x26, 0xe2, 0xb2, 0xcc, 0x72, + 0x84, 0xbb, 0x36, 0xa6, 0xc4, 0x7e, 0xcd, 0x0e, 0x88, 0x53, 0x12, 0x85, + 0x3e, 0xa6, 0xd2, 0x2c, 0x51, 0xa4, 0x15, 0x1a, 0x05, 0x39, 0x2f, 0x4b, + 0x7f, 0x07, 0x29, 0xf4, 0xf7, 0x3c, 0xd2, 0x1b, 0xf3, 0x98, 0x9d, 0x7f, + 0xd9, 0x3c, 0xab, 0xd5, 0xaa, 0x62, 0x05, 0x43, 0x29, 0x73, 0x91, 0xd1, + 0x81, 0xec, 0x24, 0xb2, 0x99, 0x6c, 0x73, 0x83, 0xaf, 0x6f, 0x3e, 0x0e, + 0x29, 0xa9, 0x79, 0xd8, 0x65, 0xe2, 0x2a, 0xb5, 0x6d, 0x29, 0x25, 0x4b, + 0x93, 0x1d, 0x1a, 0x2e, 0x3b, 0x6d, 0xfc, 0x2c, 0x55, 0x65, 0xe2, 0xca, + 0x24, 0x77, 0x77, 0xff, 0xb4, 0x39, 0x90, 0x96, 0x0d, 0xf8, 0xd9, 0x5f, + 0xff, 0xf2, 0xa7, 0xbf, 0xfd, 0xf1, 0xcf, 0x54, 0x32, 0x77, 0x60, 0xa4, + 0x08, 0xf3, 0x45, 0x15, 0x18, 0x95, 0x04, 0xed, 0x6e, 0x03, 0x22, 0xef, + 0xd9, 0x4e, 0x90, 0xec, 0xa4, 0x2a, 0x25, 0x07, 0x05, 0xcf, 0x22, 0x02, + 0xc6, 0x8a, 0x8b, 0x1b, 0xb9, 0xcb, 0x6a, 0xc0, 0x41, 0xd6, 0xd6, 0x35, + 0x7d, 0x34, 0x17, 0xfb, 0x37, 0x35, 0xd0, 0x0e, 0x7f, 0x03, 0x9e, 0xcf, + 0x93, 0x6e, 0x81, 0xb4, 0xb2, 0xa0, 0x5e, 0x09, 0xad, 0x77, 0x25, 0xb6, + 0x7f, 0xc0, 0x8c, 0xbb, 0x3c, 0x8f, 0x18, 0xac, 0x58, 0x3f, 0x5a, 0xe1, + 0x37, 0x84, 0xff, 0x15, 0x9a, 0x5d, 0x52, 0x1b, 0xdb, 0xfd, 0x27, 0x64, + 0x36, 0x8c, 0x1b, 0x7e, 0xfb, 0x06, 0x91, 0xbf, 0x26, 0xd9, 0x96, 0x38, + 0xa2, 0x8a, 0xbb, 0xd5, 0x09, 0xf3, 0x14, 0x33, 0x57, 0xd9, 0x70, 0x22, + 0xaf, 0xe3, 0x87, 0x2c, 0x57, 0xcd, 0xc9, 0x27, 0xa3, 0x5b, 0x75, 0xd8, + 0xea, 0x0d, 0x7a, 0xe7, 0xbe, 0xc9, 0xa8, 0xe5, 0x4a, 0xdd, 0x7f, 0x56, + 0xfb, 0x55, 0x53, 0x7b, 0xda, 0xdf, 0xaa, 0x80, 0x55, 0x49, 0xdb, 0xe2, + 0x78, 0xd4, 0xb5, 0x1a, 0xb5, 0xf0, 0xa8, 0x5b, 0x33, 0x2a, 0x65, 0x31, + 0xb9, 0xaa, 0x29, 0x0b, 0x18, 0x64, 0xc8, 0x41, 0xa6, 0x79, 0x43, 0x07, + 0xbd, 0x29, 0x03, 0xef, 0x99, 0x21, 0x38, 0x66, 0xad, 0xe3, 0xa3, 0xee, + 0xeb, 0x1f, 0x5b, 0x58, 0xa8, 0xa9, 0xb0, 0xf6, 0xd8, 0xa3, 0x1f, 0x1c, + 0x1c, 0x1e, 0x1c, 0xb4, 0xaa, 0x8a, 0x62, 0x7a, 0x36, 0xa5, 0xc0, 0x6c, + 0xb7, 0x3d, 0x28, 0x8f, 0x3c, 0xda, 0xa5, 0x34, 0x4b, 0x35, 0x36, 0xec, + 0xb2, 0x09, 0x1a, 0x84, 0x4b, 0x67, 0x60, 0x8c, 0x62, 0x32, 0xd0, 0x7b, + 0x76, 0x5e, 0xc8, 0x7b, 0x41, 0x1d, 0xa6, 0x69, 0xdf, 0xe6, 0x4c, 0xe6, + 0x24, 0xb9, 0x2a, 0x85, 0x03, 0xcd, 0xb1, 0xe9, 0xc8, 0x16, 0xe1, 0x3d, + 0x15, 0xab, 0x75, 0x7d, 0x6a, 0xcd, 0x69, 0xa0, 0x26, 0x16, 0xa8, 0x84, + 0xa5, 0x7c, 0x8f, 0xf3, 0x10, 0x26, 0x85, 0xf6, 0xbc, 0x8d, 0x39, 0x81, + 0x7a, 0xfa, 0x6a, 0x57, 0xb5, 0x1e, 0xf5, 0xaf, 0x78, 0x24, 0xe2, 0x96, + 0x97, 0x4b, 0x55, 0xd5, 0x35, 0x96, 0xda, 0x63, 0xb9, 0x94, 0x89, 0x07, + 0xf8, 0xec, 0x6d, 0x2a, 0x63, 0xcd, 0xf0, 0xd1, 0x46, 0x47, 0xaf, 0xdf, + 0xfe, 0xb8, 0x77, 0xd0, 0xed, 0xee, 0x85, 0x18, 0xc6, 0x1e, 0x04, 0x37, + 0xc6, 0x24, 0xbd, 0x8f, 0xd1, 0x5f, 0xef, 0xe3, 0xef, 0x7e, 0x5c, 0x50, + 0xb7, 0xd2, 0x31, 0x8b, 0x2c, 0x56, 0x59, 0x7d, 0x2b, 0xda, 0x51, 0xf4, + 0x7c, 0x35, 0x47, 0x9a, 0x7b, 0x8e, 0xeb, 0x6b, 0x3e, 0xd6, 0xc2, 0x06, + 0xda, 0xcc, 0x37, 0x1b, 0x6b, 0x95, 0xbd, 0xea, 0x69, 0x3d, 0xa6, 0xd4, + 0x2a, 0xe1, 0x4e, 0xaf, 0xd2, 0x3d, 0x42, 0x5b, 0x25, 0x78, 0xd9, 0x98, + 0x57, 0x7d, 0x7f, 0xd5, 0xee, 0x8b, 0x80, 0xf4, 0x0c, 0xca, 0xfe, 0x0d, + 0x14, 0x4e, 0xd9, 0xd0, 0xa0, 0x16, 0x6c, 0x0c, 0x07, 0xd8, 0x99, 0xe8, + 0xa8, 0x10, 0xd9, 0xf0, 0x5b, 0x15, 0xa3, 0x25, 0x43, 0x84, 0xe5, 0x85, + 0x6b, 0x37, 0xda, 0x28, 0x3b, 0x33, 0x63, 0xbd, 0xa2, 0xca, 0x69, 0xee, + 0xdf, 0xa2, 0xa5, 0xb9, 0xb9, 0xee, 0x0f, 0xa9, 0x99, 0x2f, 0xb9, 0x80, + 0xdc, 0x6c, 0x3c, 0x8a, 0x8e, 0x00, 0xa0, 0x96, 0x6e, 0x13, 0x05, 0x5b, + 0x4c, 0xde, 0x1d, 0xfd, 0x6f, 0xb7, 0x6b, 0x9d, 0xf6, 0x37, 0xcd, 0xa0, + 0xe9, 0xf1, 0xc0, 0xa4, 0xdc, 0x78, 0xe4, 0x92, 0x88, 0x19, 0x37, 0x7c, + 0x76, 0x90, 0x7b, 0xb6, 0xe7, 0xd1, 0x50, 0x32, 0x72, 0x86, 0xf6, 0x53, + 0xfa, 0x8d, 0x0d, 0x62, 0x60, 0x4c, 0x2d, 0xd8, 0x6c, 0x99, 0x45, 0x7b, + 0x1b, 0x9c, 0xab, 0x45, 0x78, 0x40, 0xe8, 0xc6, 0xdf, 0xc3, 0x37, 0x47, + 0x15, 0xbc, 0xe3, 0x37, 0xad, 0xe6, 0x1d, 0x74, 0x66, 0x73, 0x85, 0x33, + 0x08, 0xce, 0x7a, 0xde, 0xd9, 0xf0, 0x62, 0xd2, 0xc7, 0x25, 0x66, 0xeb, + 0x51, 0x46, 0x73, 0x01, 0x46, 0xfc, 0x2d, 0x11, 0xc9, 0x11, 0x05, 0x42, + 0x18, 0xfd, 0x5a, 0x09, 0x8d, 0xa7, 0xbc, 0xcc, 0xb4, 0x88, 0x30, 0xac, + 0xda, 0x7e, 0x0a, 0x43, 0x9f, 0x46, 0xfc, 0x24, 0x8c, 0x38, 0xcd, 0x12, + 0xd5, 0xba, 0x81, 0xc6, 0xe3, 0x8c, 0x5c, 0x22, 0xba, 0x94, 0xf8, 0x4e, + 0x64, 0x62, 0xf9, 0x24, 0x20, 0xab, 0x7d, 0x5c, 0xe6, 0x5e, 0x3a, 0x7d, + 0xb2, 0x48, 0xd5, 0x79, 0xd6, 0xe3, 0xcc, 0xa9, 0xfb, 0x64, 0xa4, 0xb0, + 0xbe, 0xa0, 0x7d, 0x2a, 0x9f, 0x9d, 0xaa, 0x77, 0x83, 0x46, 0x42, 0xa8, + 0xba, 0xa2, 0x66, 0x46, 0xa0, 0x34, 0x64, 0x6c, 0x87, 0x46, 0xb5, 0x94, + 0xa3, 0x7e, 0x63, 0x78, 0x22, 0x4a, 0x4d, 0x5b, 0x0e, 0x4b, 0x80, 0x52, + 0x9a, 0x86, 0xa4, 0x98, 0xe2, 0x79, 0x68, 0x1e, 0x79, 0x52, 0x9c, 0x14, + 0x39, 0x90, 0x46, 0xaf, 0x45, 0xaa, 0x0e, 0x9d, 0x8a, 0x6c, 0xcf, 0xc4, + 0x7d, 0xcb, 0xaa, 0x66, 0xfd, 0x6a, 0xf5, 0xdf, 0xd9, 0xe4, 0x3f, 0xe9, + 0xef, 0xbb, 0x06, 0x37, 0xb5, 0xe2, 0x7e, 0x01, 0x37, 0x90, 0x9a, 0x03, + 0x7e, 0xb3, 0x9c, 0xd3, 0x0f, 0x07, 0x1d, 0x16, 0xfd, 0xfd, 0x1c, 0x16, + 0x46, 0x7f, 0xbb, 0x28, 0x64, 0x41, 0x3f, 0xfa, 0x85, 0xa0, 0xa9, 0xfa, + 0x69, 0x6a, 0x2c, 0x39, 0x58, 0x23, 0x8c, 0x50, 0x94, 0xde, 0xcd, 0xa7, + 0x55, 0xa7, 0xf8, 0xda, 0x36, 0x46, 0xf5, 0x72, 0xde, 0x24, 0x37, 0xb4, + 0xab, 0xf5, 0xeb, 0x0d, 0xd9, 0x86, 0xc2, 0x58, 0xe3, 0xe9, 0x71, 0x5a, + 0x6c, 0x9c, 0xa5, 0xa7, 0xa7, 0x3a, 0x3f, 0x60, 0xbb, 0x7c, 0xf7, 0xc0, + 0x0f, 0x03, 0x2d, 0x7a, 0x2b, 0x32, 0x81, 0xad, 0xe8, 0x29, 0x40, 0xa6, + 0xf0, 0x40, 0x4c, 0xa7, 0x58, 0x21, 0x35, 0x7e, 0xbf, 0x54, 0xa8, 0xf7, + 0x91, 0x31, 0xe8, 0x4c, 0xd2, 0x9c, 0x0c, 0xc8, 0xd6, 0x49, 0xfb, 0xd5, + 0xd7, 0x09, 0x60, 0x34, 0x3d, 0x0d, 0xdc, 0xa9, 0xdf, 0xf3, 0x1b, 0x91, + 0x3f, 0x0e, 0x1f, 0x10, 0xaf, 0x19, 0xd2, 0xd5, 0xd2, 0x3c, 0x72, 0x80, + 0x95, 0x02, 0x17, 0x38, 0x98, 0xe4, 0xdc, 0xe2, 0x61, 0xcc, 0x0d, 0x83, + 0x8f, 0x7b, 0xbf, 0x04, 0xf4, 0x46, 0xe8, 0xd5, 0x2e, 0x30, 0x4e, 0x20, + 0x46, 0x0a, 0x99, 0x1a, 0x81, 0x26, 0x66, 0xfa, 0x39, 0x3e, 0x87, 0xef, + 0x50, 0x4e, 0xc2, 0x0c, 0x0c, 0xd9, 0x4f, 0x3f, 0xe1, 0x6b, 0x8f, 0x21, + 0x9e, 0xc7, 0x27, 0x86, 0xaf, 0xe7, 0xfc, 0x0e, 0x19, 0xea, 0xcc, 0x19, + 0x9a, 0x07, 0xcb, 0x77, 0x26, 0x60, 0xe7, 0x29, 0xf5, 0x7b, 0xa4, 0x75, + 0x8c, 0xce, 0x7a, 0xfd, 0xb5, 0x5e, 0x03, 0x8c, 0xcf, 0x57, 0x5f, 0x69, + 0x66, 0x3f, 0xe4, 0x02, 0x15, 0xc5, 0x3c, 0xdb, 0x90, 0x38, 0xc4, 0x80, + 0x64, 0x79, 0x19, 0xf3, 0x84, 0xd3, 0xc3, 0xc1, 0x8c, 0xde, 0x13, 0x52, + 0x88, 0x4d, 0x27, 0xb6, 0xcd, 0xf5, 0xd6, 0x08, 0xb3, 0x79, 0xdc, 0x69, + 0x20, 0x20, 0xdb, 0xe5, 0xfe, 0xac, 0xe1, 0x4f, 0x7a, 0xc2, 0xa9, 0xaa, + 0x7e, 0x59, 0xf2, 0xe9, 0xed, 0xa3, 0x7c, 0xe9, 0xae, 0x0c, 0x92, 0x22, + 0x05, 0x85, 0x73, 0xbe, 0x23, 0xb9, 0xbb, 0x36, 0x8a, 0xcb, 0x04, 0x03, + 0x69, 0x80, 0x94, 0x33, 0xf6, 0x9a, 0xaf, 0xac, 0x3e, 0xe8, 0x11, 0x87, + 0xc5, 0x86, 0xf7, 0x6a, 0xc1, 0xb3, 0x66, 0x7b, 0x01, 0x26, 0x09, 0xae, + 0x7b, 0x8e, 0x6b, 0xb3, 0x5c, 0x54, 0x21, 0xa3, 0xa3, 0x9c, 0xc2, 0x61, + 0x99, 0x89, 0x87, 0x32, 0x2f, 0x2c, 0xe3, 0xfc, 0x49, 0x4c, 0xd0, 0x91, + 0xe6, 0xdb, 0x35, 0xbe, 0xc1, 0xe0, 0xac, 0xd9, 0xcd, 0xd4, 0xaf, 0xcf, + 0x9b, 0x57, 0x3d, 0x93, 0x66, 0x9e, 0xd8, 0x89, 0x16, 0xb7, 0xec, 0xf4, + 0xdc, 0x64, 0xbe, 0x2d, 0xc2, 0x40, 0x84, 0xf3, 0x0c, 0x17, 0x8a, 0xa8, + 0x36, 0x5e, 0x39, 0x54, 0x9b, 0x34, 0xd9, 0x6a, 0x4c, 0xf1, 0xcf, 0x1e, + 0x7c, 0x32, 0xd6, 0x6f, 0x4f, 0xe9, 0xdf, 0x3e, 0x89, 0x97, 0x1e, 0xe6, + 0xd4, 0x51, 0x20, 0xff, 0x45, 0x61, 0xc6, 0x6e, 0x48, 0x4d, 0x4e, 0xe6, + 0x43, 0x93, 0xc4, 0xab, 0x9c, 0xf8, 0xa5, 0x75, 0xf0, 0xb1, 0xf1, 0x10, + 0xdd, 0xda, 0x6b, 0x1d, 0x6e, 0x7d, 0x5f, 0x93, 0x5f, 0x6c, 0x7a, 0x2a, + 0xf1, 0x9a, 0xa6, 0xdb, 0xe4, 0xe5, 0xa7, 0xe6, 0x7b, 0x7c, 0x14, 0x6e, + 0x98, 0x70, 0xfb, 0x75, 0x98, 0x6d, 0x3d, 0xd4, 0x5a, 0x03, 0x97, 0xb8, + 0x97, 0x07, 0x4f, 0x40, 0x19, 0xd3, 0x7f, 0x73, 0x79, 0x90, 0x45, 0x5a, + 0x4a, 0x78, 0x6c, 0x1e, 0x7a, 0x8f, 0xe9, 0x9f, 0x8f, 0x9b, 0xff, 0x02, + 0x61, 0xd2, 0xcf, 0xff, 0x21, 0x3b, 0x17, 0xe8, 0x24, 0x3e, 0x2c, 0xf5, + 0xec, 0x9d, 0x45, 0xe0, 0x21, 0x26, 0x7f, 0x0f, 0x00, 0x00, 0xff, 0xff, + 0xc9, 0x2e, 0x07, 0x65, 0xc7, 0x19, 0x00, 0x00, + }, "conf/app.ini", ) } @@ -893,7 +909,7 @@ func conf_content_git_bare_zip() ([]byte, error) { 0x28, 0x3f, 0xfe, 0xba, 0x0a, 0x40, 0x72, 0xf5, 0xcf, 0xf9, 0x6a, 0x9b, 0x11, 0xa6, 0xf9, 0x31, 0xfa, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x01, 0x81, 0x55, 0x99, 0xb6, 0x26, 0x00, 0x00, - }, + }, "conf/content/git-bare.zip", ) } @@ -946,7 +962,7 @@ func conf_etc_supervisord_conf() ([]byte, error) { 0x8d, 0x88, 0x90, 0x28, 0xbf, 0x3f, 0xd4, 0xfe, 0xe7, 0x05, 0xbd, 0x28, 0xc2, 0x24, 0xff, 0x1b, 0x00, 0x00, 0xff, 0xff, 0xbc, 0x75, 0xb0, 0x31, 0xf7, 0x04, 0x00, 0x00, - }, + }, "conf/etc/supervisord.conf", ) } @@ -970,7 +986,7 @@ func conf_gitignore_android() ([]byte, error) { 0xb8, 0xa3, 0xb5, 0xe2, 0x2c, 0x81, 0x0c, 0xe2, 0x75, 0xc9, 0xf2, 0x07, 0x2f, 0x5e, 0x58, 0x0b, 0x39, 0x3d, 0xa4, 0xf9, 0x3f, 0x00, 0x00, 0xff, 0xff, 0x00, 0x96, 0x67, 0x2c, 0x0e, 0x01, 0x00, 0x00, - }, + }, "conf/gitignore/Android", ) } @@ -989,7 +1005,7 @@ func conf_gitignore_c() ([]byte, error) { 0xeb, 0x8e, 0x79, 0xeb, 0x31, 0x1d, 0x73, 0xb8, 0xa3, 0x8d, 0x6e, 0xdd, 0xea, 0xd7, 0xf5, 0x1f, 0x00, 0x00, 0xff, 0xff, 0xca, 0x54, 0xa9, 0x22, 0x8f, 0x00, 0x00, 0x00, - }, + }, "conf/gitignore/C", ) } @@ -1065,7 +1081,7 @@ func conf_gitignore_c_sharp() ([]byte, error) { 0x8b, 0x52, 0xd1, 0xf6, 0x63, 0x0e, 0x6e, 0xd8, 0x98, 0xaa, 0x6a, 0xd8, 0xb4, 0xfb, 0xc9, 0x76, 0x55, 0xfd, 0xd7, 0x1f, 0x9f, 0xfe, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xfe, 0xac, 0xdb, 0x69, 0xf1, 0x05, 0x00, 0x00, - }, + }, "conf/gitignore/C Sharp", ) } @@ -1081,7 +1097,7 @@ func conf_gitignore_c_() ([]byte, error) { 0x5c, 0x92, 0x58, 0x82, 0xa6, 0x32, 0x27, 0x31, 0x13, 0x4c, 0x02, 0x89, 0x44, 0x2e, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa4, 0xe6, 0x21, 0x26, 0x7e, 0x00, 0x00, 0x00, - }, + }, "conf/gitignore/C++", ) } @@ -1106,7 +1122,7 @@ func conf_gitignore_google_go() ([]byte, error) { 0x22, 0xd5, 0x42, 0x03, 0xe7, 0x8f, 0xcc, 0x91, 0xf3, 0x76, 0xe7, 0x08, 0x5a, 0xe9, 0x27, 0x00, 0x00, 0xff, 0xff, 0x3c, 0xab, 0x59, 0x6f, 0xfb, 0x00, 0x00, 0x00, - }, + }, "conf/gitignore/Google Go", ) } @@ -1128,7 +1144,7 @@ func conf_gitignore_java() ([]byte, error) { 0xc4, 0xd9, 0x51, 0x29, 0x52, 0x96, 0x20, 0x55, 0xb3, 0x54, 0x7b, 0x4f, 0x6c, 0x82, 0x2e, 0x7d, 0x5c, 0x72, 0x5c, 0xc7, 0x47, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe7, 0xd6, 0xf7, 0xa4, 0xbc, 0x00, 0x00, 0x00, - }, + }, "conf/gitignore/Java", ) } @@ -1153,7 +1169,7 @@ func conf_gitignore_objective_c() ([]byte, error) { 0xc9, 0x07, 0xae, 0xa1, 0xb9, 0x4c, 0x22, 0x3f, 0x5b, 0x4d, 0x65, 0x7b, 0x3d, 0x9f, 0x60, 0x5c, 0x71, 0xf9, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa9, 0x17, 0x4f, 0x2a, 0x18, 0x01, 0x00, 0x00, - }, + }, "conf/gitignore/Objective-C", ) } @@ -1180,7 +1196,7 @@ func conf_gitignore_python() ([]byte, error) { 0x78, 0xeb, 0xf6, 0x9c, 0x58, 0x85, 0x7f, 0x28, 0x58, 0x2b, 0xb6, 0xa6, 0x1c, 0xdd, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x02, 0xf0, 0xe2, 0xc0, 0x3a, 0x01, 0x00, 0x00, - }, + }, "conf/gitignore/Python", ) } @@ -1200,7 +1216,7 @@ func conf_gitignore_ruby() ([]byte, error) { 0x41, 0xb1, 0xbc, 0x23, 0x4d, 0xdf, 0x7d, 0xf0, 0x88, 0x6c, 0xbf, 0x3b, 0xf1, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xb1, 0xca, 0xf7, 0x91, 0x9e, 0x00, 0x00, 0x00, - }, + }, "conf/gitignore/Ruby", ) } @@ -2189,7 +2205,7 @@ func conf_license_affero_gpl() ([]byte, error) { 0x42, 0xc2, 0x5f, 0x88, 0x57, 0x1b, 0xd8, 0x89, 0x3e, 0x15, 0x0e, 0xb8, 0x30, 0xdf, 0x60, 0x88, 0xff, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x0c, 0xd2, 0xa8, 0x4c, 0xc3, 0x86, 0x00, 0x00, - }, + }, "conf/license/Affero GPL", ) } @@ -2527,7 +2543,7 @@ func conf_license_apache_v2_license() ([]byte, error) { 0x37, 0x23, 0x02, 0x0e, 0x94, 0x00, 0x65, 0xa1, 0x3f, 0x7d, 0xb3, 0x2f, 0x4c, 0x4b, 0x32, 0x5f, 0x77, 0xe7, 0xe7, 0x9a, 0xff, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa8, 0x76, 0x8d, 0x12, 0x3b, 0x2c, 0x00, 0x00, - }, + }, "conf/license/Apache v2 License", ) } @@ -2809,7 +2825,7 @@ func conf_license_artistic_license_2_0() ([]byte, error) { 0xdd, 0x89, 0x97, 0xd6, 0xcf, 0xf7, 0x8f, 0x92, 0x0f, 0xb9, 0xfb, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0xaf, 0x26, 0x8b, 0xf2, 0xb7, 0x22, 0x00, 0x00, - }, + }, "conf/license/Artistic License 2.0", ) } @@ -2883,7 +2899,7 @@ func conf_license_bsd_3_clause_license() ([]byte, error) { 0x95, 0xb8, 0xec, 0x49, 0x8a, 0xac, 0xd8, 0xd0, 0x39, 0xee, 0xdb, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0x84, 0xcd, 0xba, 0x22, 0xc1, 0x05, 0x00, 0x00, - }, + }, "conf/license/BSD (3-Clause) License", ) } @@ -3459,7 +3475,7 @@ func conf_license_gpl_v2() ([]byte, error) { 0x4d, 0xee, 0x25, 0x41, 0xb2, 0x70, 0x4f, 0xe6, 0xf0, 0xef, 0xb7, 0x30, 0xc7, 0xff, 0x7e, 0x8b, 0x83, 0x22, 0xe6, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0x82, 0x4d, 0xf9, 0x2b, 0x69, 0x46, 0x00, 0x00, - }, + }, "conf/license/GPL v2", ) } @@ -3521,7 +3537,7 @@ func conf_license_mit_license() ([]byte, error) { 0x42, 0x26, 0x78, 0x8e, 0x5c, 0x15, 0x81, 0x29, 0xe2, 0xdb, 0xf0, 0xd3, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x49, 0x86, 0xab, 0x31, 0x29, 0x04, 0x00, 0x00, - }, + }, "conf/license/MIT License", ) } @@ -3537,7 +3553,7 @@ func conf_mysql_sql() ([]byte, error) { 0xa1, 0xe0, 0xec, 0xef, 0xe3, 0x03, 0xd2, 0x06, 0xe2, 0xc4, 0xa7, 0xa7, 0xe6, 0xa5, 0x16, 0x25, 0xe6, 0xc4, 0x27, 0x67, 0x5a, 0x73, 0x01, 0x02, 0x00, 0x00, 0xff, 0xff, 0xcd, 0xf5, 0x53, 0x80, 0x6d, 0x00, 0x00, 0x00, - }, + }, "conf/mysql.sql", ) } @@ -3557,12 +3573,11 @@ func conf_supervisor_ini() ([]byte, error) { 0xa1, 0xed, 0x82, 0x8e, 0x38, 0x6f, 0x11, 0x92, 0x13, 0x67, 0x75, 0xe7, 0xeb, 0xe5, 0xe4, 0x86, 0xef, 0xd7, 0xc1, 0x18, 0xfa, 0x04, 0x00, 0x00, 0xff, 0xff, 0x61, 0x60, 0x15, 0x6f, 0xc9, 0x00, 0x00, 0x00, - }, + }, "conf/supervisor.ini", ) } - // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. @@ -3584,7 +3599,7 @@ func AssetNames() []string { } // _bindata is a table, holding each asset generator, mapped to its name. -var _bindata = map[string] func() ([]byte, error) { +var _bindata = map[string]func() ([]byte, error){ "conf/app.ini": conf_app_ini, "conf/content/git-bare.zip": conf_content_git_bare_zip, "conf/etc/supervisord.conf": conf_etc_supervisord_conf, diff --git a/modules/cron/constantdelay.go b/modules/cron/constantdelay.go new file mode 100644 index 0000000000..cd6e7b1be9 --- /dev/null +++ b/modules/cron/constantdelay.go @@ -0,0 +1,27 @@ +package cron + +import "time" + +// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". +// It does not support jobs more frequent than once a second. +type ConstantDelaySchedule struct { + Delay time.Duration +} + +// Every returns a crontab Schedule that activates once every duration. +// Delays of less than a second are not supported (will round up to 1 second). +// Any fields less than a Second are truncated. +func Every(duration time.Duration) ConstantDelaySchedule { + if duration < time.Second { + duration = time.Second + } + return ConstantDelaySchedule{ + Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, + } +} + +// Next returns the next time this should be run. +// This rounds so that the next activation time will be on the second. +func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { + return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) +} diff --git a/modules/cron/constantdelay_test.go b/modules/cron/constantdelay_test.go new file mode 100644 index 0000000000..f43a58ad26 --- /dev/null +++ b/modules/cron/constantdelay_test.go @@ -0,0 +1,54 @@ +package cron + +import ( + "testing" + "time" +) + +func TestConstantDelayNext(t *testing.T) { + tests := []struct { + time string + delay time.Duration + expected string + }{ + // Simple cases + {"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, + {"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"}, + {"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"}, + + // Wrap around hours + {"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"}, + + // Wrap around days + {"Mon Jul 9 23:46 2012", 14 * time.Minute, "Tue Jul 10 00:00 2012"}, + {"Mon Jul 9 23:45 2012", 35 * time.Minute, "Tue Jul 10 00:20 2012"}, + {"Mon Jul 9 23:35:51 2012", 44*time.Minute + 24*time.Second, "Tue Jul 10 00:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Thu Jul 11 01:20:15 2012"}, + + // Wrap around months + {"Mon Jul 9 23:35 2012", 91*24*time.Hour + 25*time.Minute, "Thu Oct 9 00:00 2012"}, + + // Wrap around minute, hour, day, month, and year + {"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"}, + + // Round to nearest second on the delay + {"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, + + // Round up to 1 second if the duration is less. + {"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:01 2012"}, + + // Round to nearest second when calculating the next time. + {"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"}, + + // Round to nearest second for both. + {"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, + } + + for _, c := range tests { + actual := Every(c.delay).Next(getTime(c.time)) + expected := getTime(c.expected) + if actual != expected { + t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual) + } + } +} diff --git a/modules/cron/cron.go b/modules/cron/cron.go index 27b1fc41bb..dbf0174b86 100644 --- a/modules/cron/cron.go +++ b/modules/cron/cron.go @@ -1,3 +1,4 @@ +// Copyright 2012 Rob Figueiredo. All rights reserved. // Copyright 2014 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -5,13 +6,208 @@ package cron import ( - "github.com/robfig/cron" - - "github.com/gogits/gogs/models" + "sort" + "time" ) -func NewCronContext() { - c := cron.New() - c.AddFunc("@every 1h", models.MirrorUpdate) - c.Start() +// Cron keeps track of any number of entries, invoking the associated func as +// specified by the schedule. It may be started, stopped, and the entries may +// be inspected while running. +type Cron struct { + entries []*Entry + stop chan struct{} + add chan *Entry + snapshot chan []*Entry + running bool +} + +// Job is an interface for submitted cron jobs. +type Job interface { + Run() +} + +// The Schedule describes a job's duty cycle. +type Schedule interface { + // Return the next activation time, later than the given time. + // Next is invoked initially, and then each time the job is run. + Next(time.Time) time.Time +} + +// Entry consists of a schedule and the func to execute on that schedule. +type Entry struct { + Description string + Spec string + + // The schedule on which this job should be run. + Schedule Schedule + + // The next time the job will run. This is the zero time if Cron has not been + // started or this entry's schedule is unsatisfiable + Next time.Time + + // The last time this job was run. This is the zero time if the job has never + // been run. + Prev time.Time + + // The Job to run. + Job Job + + ExecTimes int // Execute times count. +} + +// byTime is a wrapper for sorting the entry array by time +// (with zero time at the end). +type byTime []*Entry + +func (s byTime) Len() int { return len(s) } +func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byTime) Less(i, j int) bool { + // Two zero times should return false. + // Otherwise, zero is "greater" than any other time. + // (To sort it at the end of the list.) + if s[i].Next.IsZero() { + return false + } + if s[j].Next.IsZero() { + return true + } + return s[i].Next.Before(s[j].Next) +} + +// New returns a new Cron job runner. +func New() *Cron { + return &Cron{ + entries: nil, + add: make(chan *Entry), + stop: make(chan struct{}), + snapshot: make(chan []*Entry), + running: false, + } +} + +// A wrapper that turns a func() into a cron.Job +type FuncJob func() + +func (f FuncJob) Run() { f() } + +// AddFunc adds a func to the Cron to be run on the given schedule. +func (c *Cron) AddFunc(desc, spec string, cmd func()) error { + return c.AddJob(desc, spec, FuncJob(cmd)) +} + +// AddFunc adds a Job to the Cron to be run on the given schedule. +func (c *Cron) AddJob(desc, spec string, cmd Job) error { + schedule, err := Parse(spec) + if err != nil { + return err + } + c.Schedule(desc, spec, schedule, cmd) + return nil +} + +// Schedule adds a Job to the Cron to be run on the given schedule. +func (c *Cron) Schedule(desc, spec string, schedule Schedule, cmd Job) { + entry := &Entry{ + Description: desc, + Spec: spec, + Schedule: schedule, + Job: cmd, + } + if !c.running { + c.entries = append(c.entries, entry) + return + } + + c.add <- entry +} + +// Entries returns a snapshot of the cron entries. +func (c *Cron) Entries() []*Entry { + if c.running { + c.snapshot <- nil + x := <-c.snapshot + return x + } + return c.entrySnapshot() +} + +// Start the cron scheduler in its own go-routine. +func (c *Cron) Start() { + c.running = true + go c.run() +} + +// Run the scheduler.. this is private just due to the need to synchronize +// access to the 'running' state variable. +func (c *Cron) run() { + // Figure out the next activation times for each entry. + now := time.Now().Local() + for _, entry := range c.entries { + entry.Next = entry.Schedule.Next(now) + } + + for { + // Determine the next entry to run. + sort.Sort(byTime(c.entries)) + + var effective time.Time + if len(c.entries) == 0 || c.entries[0].Next.IsZero() { + // If there are no entries yet, just sleep - it still handles new entries + // and stop requests. + effective = now.AddDate(10, 0, 0) + } else { + effective = c.entries[0].Next + } + + select { + case now = <-time.After(effective.Sub(now)): + // Run every entry whose next time was this effective time. + for _, e := range c.entries { + if e.Next != effective { + break + } + go e.Job.Run() + e.ExecTimes++ + e.Prev = e.Next + e.Next = e.Schedule.Next(effective) + } + continue + + case newEntry := <-c.add: + c.entries = append(c.entries, newEntry) + newEntry.Next = newEntry.Schedule.Next(now) + + case <-c.snapshot: + c.snapshot <- c.entrySnapshot() + + case <-c.stop: + return + } + + // 'now' should be updated after newEntry and snapshot cases. + now = time.Now().Local() + } +} + +// Stop the cron scheduler. +func (c *Cron) Stop() { + c.stop <- struct{}{} + c.running = false +} + +// entrySnapshot returns a copy of the current cron entry list. +func (c *Cron) entrySnapshot() []*Entry { + entries := make([]*Entry, 0, len(c.entries)) + for _, e := range c.entries { + entries = append(entries, &Entry{ + Description: e.Description, + Spec: e.Spec, + Schedule: e.Schedule, + Next: e.Next, + Prev: e.Prev, + Job: e.Job, + ExecTimes: e.ExecTimes, + }) + } + return entries } diff --git a/modules/cron/cron_test.go b/modules/cron/cron_test.go new file mode 100644 index 0000000000..417247a05a --- /dev/null +++ b/modules/cron/cron_test.go @@ -0,0 +1,255 @@ +package cron + +import ( + "fmt" + "sync" + "testing" + "time" +) + +// Many tests schedule a job for every second, and then wait at most a second +// for it to run. This amount is just slightly larger than 1 second to +// compensate for a few milliseconds of runtime. +const ONE_SECOND = 1*time.Second + 10*time.Millisecond + +// Start and stop cron with no entries. +func TestNoEntries(t *testing.T) { + cron := New() + cron.Start() + + select { + case <-time.After(ONE_SECOND): + t.FailNow() + case <-stop(cron): + } +} + +// Start, stop, then add an entry. Verify entry doesn't run. +func TestStopCausesJobsToNotRun(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := New() + cron.Start() + cron.Stop() + cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) + + select { + case <-time.After(ONE_SECOND): + // No job ran! + case <-wait(wg): + t.FailNow() + } +} + +// Add a job, start cron, expect it runs. +func TestAddBeforeRunning(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := New() + cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) + cron.Start() + defer cron.Stop() + + // Give cron 2 seconds to run our job (which is always activated). + select { + case <-time.After(ONE_SECOND): + t.FailNow() + case <-wait(wg): + } +} + +// Start cron, add a job, expect it runs. +func TestAddWhileRunning(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := New() + cron.Start() + defer cron.Stop() + cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) + + select { + case <-time.After(ONE_SECOND): + t.FailNow() + case <-wait(wg): + } +} + +// Test timing with Entries. +func TestSnapshotEntries(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := New() + cron.AddFunc("", "@every 2s", func() { wg.Done() }) + cron.Start() + defer cron.Stop() + + // Cron should fire in 2 seconds. After 1 second, call Entries. + select { + case <-time.After(ONE_SECOND): + cron.Entries() + } + + // Even though Entries was called, the cron should fire at the 2 second mark. + select { + case <-time.After(ONE_SECOND): + t.FailNow() + case <-wait(wg): + } + +} + +// Test that the entries are correctly sorted. +// Add a bunch of long-in-the-future entries, and an immediate entry, and ensure +// that the immediate entry runs immediately. +// Also: Test that multiple jobs run in the same instant. +func TestMultipleEntries(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(2) + + cron := New() + cron.AddFunc("", "0 0 0 1 1 ?", func() {}) + cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) + cron.AddFunc("", "0 0 0 31 12 ?", func() {}) + cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) + + cron.Start() + defer cron.Stop() + + select { + case <-time.After(ONE_SECOND): + t.FailNow() + case <-wait(wg): + } +} + +// Test running the same job twice. +func TestRunningJobTwice(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(2) + + cron := New() + cron.AddFunc("", "0 0 0 1 1 ?", func() {}) + cron.AddFunc("", "0 0 0 31 12 ?", func() {}) + cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) + + cron.Start() + defer cron.Stop() + + select { + case <-time.After(2 * ONE_SECOND): + t.FailNow() + case <-wait(wg): + } +} + +func TestRunningMultipleSchedules(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(2) + + cron := New() + cron.AddFunc("", "0 0 0 1 1 ?", func() {}) + cron.AddFunc("", "0 0 0 31 12 ?", func() {}) + cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) + cron.Schedule("", "", Every(time.Minute), FuncJob(func() {})) + cron.Schedule("", "", Every(time.Second), FuncJob(func() { wg.Done() })) + cron.Schedule("", "", Every(time.Hour), FuncJob(func() {})) + + cron.Start() + defer cron.Stop() + + select { + case <-time.After(2 * ONE_SECOND): + t.FailNow() + case <-wait(wg): + } +} + +// Test that the cron is run in the local time zone (as opposed to UTC). +func TestLocalTimezone(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + now := time.Now().Local() + spec := fmt.Sprintf("%d %d %d %d %d ?", + now.Second()+1, now.Minute(), now.Hour(), now.Day(), now.Month()) + + cron := New() + cron.AddFunc("", spec, func() { wg.Done() }) + cron.Start() + defer cron.Stop() + + select { + case <-time.After(ONE_SECOND): + t.FailNow() + case <-wait(wg): + } +} + +type testJob struct { + wg *sync.WaitGroup + name string +} + +func (t testJob) Run() { + t.wg.Done() +} + +// Simple test using Runnables. +func TestJob(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := New() + cron.AddJob("", "0 0 0 30 Feb ?", testJob{wg, "job0"}) + cron.AddJob("", "0 0 0 1 1 ?", testJob{wg, "job1"}) + cron.AddJob("", "* * * * * ?", testJob{wg, "job2"}) + cron.AddJob("", "1 0 0 1 1 ?", testJob{wg, "job3"}) + cron.Schedule("", "", Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"}) + cron.Schedule("", "", Every(5*time.Minute), testJob{wg, "job5"}) + + cron.Start() + defer cron.Stop() + + select { + case <-time.After(ONE_SECOND): + t.FailNow() + case <-wait(wg): + } + + // Ensure the entries are in the right order. + expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"} + + var actuals []string + for _, entry := range cron.Entries() { + actuals = append(actuals, entry.Job.(testJob).name) + } + + for i, expected := range expecteds { + if actuals[i] != expected { + t.Errorf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals) + t.FailNow() + } + } +} + +func wait(wg *sync.WaitGroup) chan bool { + ch := make(chan bool) + go func() { + wg.Wait() + ch <- true + }() + return ch +} + +func stop(cron *Cron) chan bool { + ch := make(chan bool) + go func() { + cron.Stop() + ch <- true + }() + return ch +} diff --git a/modules/cron/doc.go b/modules/cron/doc.go new file mode 100644 index 0000000000..dbdf50127a --- /dev/null +++ b/modules/cron/doc.go @@ -0,0 +1,129 @@ +/* +Package cron implements a cron spec parser and job runner. + +Usage + +Callers may register Funcs to be invoked on a given schedule. Cron will run +them in their own goroutines. + + c := cron.New() + c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) + c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) + c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) + c.Start() + .. + // Funcs are invoked in their own goroutine, asynchronously. + ... + // Funcs may also be added to a running Cron + c.AddFunc("@daily", func() { fmt.Println("Every day") }) + .. + // Inspect the cron job entries' next and previous run times. + inspect(c.Entries()) + .. + c.Stop() // Stop the scheduler (does not stop any jobs already running). + +CRON Expression Format + +A cron expression represents a set of times, using 6 space-separated fields. + + Field name | Mandatory? | Allowed values | Allowed special characters + ---------- | ---------- | -------------- | -------------------------- + Seconds | Yes | 0-59 | * / , - + Minutes | Yes | 0-59 | * / , - + Hours | Yes | 0-23 | * / , - + Day of month | Yes | 1-31 | * / , - ? + Month | Yes | 1-12 or JAN-DEC | * / , - + Day of week | Yes | 0-6 or SUN-SAT | * / , - ? + +Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun", +and "sun" are equally accepted. + +Special Characters + +Asterisk ( * ) + +The asterisk indicates that the cron expression will match for all values of the +field; e.g., using an asterisk in the 5th field (month) would indicate every +month. + +Slash ( / ) + +Slashes are used to describe increments of ranges. For example 3-59/15 in the +1st field (minutes) would indicate the 3rd minute of the hour and every 15 +minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", +that is, an increment over the largest possible range of the field. The form +"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the +increment until the end of that specific range. It does not wrap around. + +Comma ( , ) + +Commas are used to separate items of a list. For example, using "MON,WED,FRI" in +the 5th field (day of week) would mean Mondays, Wednesdays and Fridays. + +Hyphen ( - ) + +Hyphens are used to define ranges. For example, 9-17 would indicate every +hour between 9am and 5pm inclusive. + +Question mark ( ? ) + +Question mark may be used instead of '*' for leaving either day-of-month or +day-of-week blank. + +Predefined schedules + +You may use one of several pre-defined schedules in place of a cron expression. + + Entry | Description | Equivalent To + ----- | ----------- | ------------- + @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * + @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * + @weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 + @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * + @hourly | Run once an hour, beginning of hour | 0 0 * * * * + +Intervals + +You may also schedule a job to execute at fixed intervals. This is supported by +formatting the cron spec like this: + + @every + +where "duration" is a string accepted by time.ParseDuration +(http://golang.org/pkg/time/#ParseDuration). + +For example, "@every 1h30m10s" would indicate a schedule that activates every +1 hour, 30 minutes, 10 seconds. + +Note: The interval does not take the job runtime into account. For example, +if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, +it will have only 2 minutes of idle time between each run. + +Time zones + +All interpretation and scheduling is done in the machine's local time zone (as +provided by the Go time package (http://www.golang.org/pkg/time). + +Be aware that jobs scheduled during daylight-savings leap-ahead transitions will +not be run! + +Thread safety + +Since the Cron service runs concurrently with the calling code, some amount of +care must be taken to ensure proper synchronization. + +All cron methods are designed to be correctly synchronized as long as the caller +ensures that invocations have a clear happens-before ordering between them. + +Implementation + +Cron entries are stored in an array, sorted by their next activation time. Cron +sleeps until the next job is due to be run. + +Upon waking: + - it runs each entry that is active on that second + - it calculates the next run times for the jobs that were run + - it re-sorts the array of entries by next activation time. + - it goes to sleep until the soonest job. +*/ +package cron diff --git a/modules/cron/manager.go b/modules/cron/manager.go new file mode 100644 index 0000000000..563426fb79 --- /dev/null +++ b/modules/cron/manager.go @@ -0,0 +1,24 @@ +// 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 cron + +import ( + "fmt" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/setting" +) + +var c = New() + +func NewCronContext() { + c.AddFunc("Update mirrors", "@every 1h", models.MirrorUpdate) + c.AddFunc("Deliver hooks", fmt.Sprintf("@every %dm", setting.WebhookTaskInterval), models.DeliverHooks) + c.Start() +} + +func ListEntries() []*Entry { + return c.Entries() +} diff --git a/modules/cron/parser.go b/modules/cron/parser.go new file mode 100644 index 0000000000..4224fa9308 --- /dev/null +++ b/modules/cron/parser.go @@ -0,0 +1,231 @@ +package cron + +import ( + "fmt" + "log" + "math" + "strconv" + "strings" + "time" +) + +// Parse returns a new crontab schedule representing the given spec. +// It returns a descriptive error if the spec is not valid. +// +// It accepts +// - Full crontab specs, e.g. "* * * * * ?" +// - Descriptors, e.g. "@midnight", "@every 1h30m" +func Parse(spec string) (_ Schedule, err error) { + // Convert panics into errors + defer func() { + if recovered := recover(); recovered != nil { + err = fmt.Errorf("%v", recovered) + } + }() + + if spec[0] == '@' { + return parseDescriptor(spec), nil + } + + // Split on whitespace. We require 5 or 6 fields. + // (second) (minute) (hour) (day of month) (month) (day of week, optional) + fields := strings.Fields(spec) + if len(fields) != 5 && len(fields) != 6 { + log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec) + } + + // If a sixth field is not provided (DayOfWeek), then it is equivalent to star. + if len(fields) == 5 { + fields = append(fields, "*") + } + + schedule := &SpecSchedule{ + Second: getField(fields[0], seconds), + Minute: getField(fields[1], minutes), + Hour: getField(fields[2], hours), + Dom: getField(fields[3], dom), + Month: getField(fields[4], months), + Dow: getField(fields[5], dow), + } + + return schedule, nil +} + +// getField returns an Int with the bits set representing all of the times that +// the field represents. A "field" is a comma-separated list of "ranges". +func getField(field string, r bounds) uint64 { + // list = range {"," range} + var bits uint64 + ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) + for _, expr := range ranges { + bits |= getRange(expr, r) + } + return bits +} + +// getRange returns the bits indicated by the given expression: +// number | number "-" number [ "/" number ] +func getRange(expr string, r bounds) uint64 { + + var ( + start, end, step uint + rangeAndStep = strings.Split(expr, "/") + lowAndHigh = strings.Split(rangeAndStep[0], "-") + singleDigit = len(lowAndHigh) == 1 + ) + + var extra_star uint64 + if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { + start = r.min + end = r.max + extra_star = starBit + } else { + start = parseIntOrName(lowAndHigh[0], r.names) + switch len(lowAndHigh) { + case 1: + end = start + case 2: + end = parseIntOrName(lowAndHigh[1], r.names) + default: + log.Panicf("Too many hyphens: %s", expr) + } + } + + switch len(rangeAndStep) { + case 1: + step = 1 + case 2: + step = mustParseInt(rangeAndStep[1]) + + // Special handling: "N/step" means "N-max/step". + if singleDigit { + end = r.max + } + default: + log.Panicf("Too many slashes: %s", expr) + } + + if start < r.min { + log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr) + } + if end > r.max { + log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr) + } + if start > end { + log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr) + } + + return getBits(start, end, step) | extra_star +} + +// parseIntOrName returns the (possibly-named) integer contained in expr. +func parseIntOrName(expr string, names map[string]uint) uint { + if names != nil { + if namedInt, ok := names[strings.ToLower(expr)]; ok { + return namedInt + } + } + return mustParseInt(expr) +} + +// mustParseInt parses the given expression as an int or panics. +func mustParseInt(expr string) uint { + num, err := strconv.Atoi(expr) + if err != nil { + log.Panicf("Failed to parse int from %s: %s", expr, err) + } + if num < 0 { + log.Panicf("Negative number (%d) not allowed: %s", num, expr) + } + + return uint(num) +} + +// getBits sets all bits in the range [min, max], modulo the given step size. +func getBits(min, max, step uint) uint64 { + var bits uint64 + + // If step is 1, use shifts. + if step == 1 { + return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) + } + + // Else, use a simple loop. + for i := min; i <= max; i += step { + bits |= 1 << i + } + return bits +} + +// all returns all bits within the given bounds. (plus the star bit) +func all(r bounds) uint64 { + return getBits(r.min, r.max, 1) | starBit +} + +// parseDescriptor returns a pre-defined schedule for the expression, or panics +// if none matches. +func parseDescriptor(spec string) Schedule { + switch spec { + case "@yearly", "@annually": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: 1 << months.min, + Dow: all(dow), + } + + case "@monthly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: all(months), + Dow: all(dow), + } + + case "@weekly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: 1 << dow.min, + } + + case "@daily", "@midnight": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: all(dow), + } + + case "@hourly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: all(hours), + Dom: all(dom), + Month: all(months), + Dow: all(dow), + } + } + + const every = "@every " + if strings.HasPrefix(spec, every) { + duration, err := time.ParseDuration(spec[len(every):]) + if err != nil { + log.Panicf("Failed to parse duration %s: %s", spec, err) + } + return Every(duration) + } + + log.Panicf("Unrecognized descriptor: %s", spec) + return nil +} diff --git a/modules/cron/parser_test.go b/modules/cron/parser_test.go new file mode 100644 index 0000000000..9050cf7869 --- /dev/null +++ b/modules/cron/parser_test.go @@ -0,0 +1,117 @@ +package cron + +import ( + "reflect" + "testing" + "time" +) + +func TestRange(t *testing.T) { + ranges := []struct { + expr string + min, max uint + expected uint64 + }{ + {"5", 0, 7, 1 << 5}, + {"0", 0, 7, 1 << 0}, + {"7", 0, 7, 1 << 7}, + + {"5-5", 0, 7, 1 << 5}, + {"5-6", 0, 7, 1<<5 | 1<<6}, + {"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7}, + + {"5-6/2", 0, 7, 1 << 5}, + {"5-7/2", 0, 7, 1<<5 | 1<<7}, + {"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7}, + + {"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit}, + {"*/2", 1, 3, 1<<1 | 1<<3 | starBit}, + } + + for _, c := range ranges { + actual := getRange(c.expr, bounds{c.min, c.max, nil}) + if actual != c.expected { + t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) + } + } +} + +func TestField(t *testing.T) { + fields := []struct { + expr string + min, max uint + expected uint64 + }{ + {"5", 1, 7, 1 << 5}, + {"5,6", 1, 7, 1<<5 | 1<<6}, + {"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7}, + {"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3}, + } + + for _, c := range fields { + actual := getField(c.expr, bounds{c.min, c.max, nil}) + if actual != c.expected { + t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) + } + } +} + +func TestBits(t *testing.T) { + allBits := []struct { + r bounds + expected uint64 + }{ + {minutes, 0xfffffffffffffff}, // 0-59: 60 ones + {hours, 0xffffff}, // 0-23: 24 ones + {dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero + {months, 0x1ffe}, // 1-12: 12 ones, 1 zero + {dow, 0x7f}, // 0-6: 7 ones + } + + for _, c := range allBits { + actual := all(c.r) // all() adds the starBit, so compensate for that.. + if c.expected|starBit != actual { + t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", + c.r.min, c.r.max, 1, c.expected|starBit, actual) + } + } + + bits := []struct { + min, max, step uint + expected uint64 + }{ + + {0, 0, 1, 0x1}, + {1, 1, 1, 0x2}, + {1, 5, 2, 0x2a}, // 101010 + {1, 4, 2, 0xa}, // 1010 + } + + for _, c := range bits { + actual := getBits(c.min, c.max, c.step) + if c.expected != actual { + t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", + c.min, c.max, c.step, c.expected, actual) + } + } +} + +func TestSpecSchedule(t *testing.T) { + entries := []struct { + expr string + expected Schedule + }{ + {"* 5 * * * *", &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}}, + {"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}}, + } + + for _, c := range entries { + actual, err := Parse(c.expr) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(actual, c.expected) { + t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual) + } + } +} diff --git a/modules/cron/spec.go b/modules/cron/spec.go new file mode 100644 index 0000000000..cb3743325d --- /dev/null +++ b/modules/cron/spec.go @@ -0,0 +1,161 @@ +package cron + +import ( + "time" +) + +// SpecSchedule specifies a duty cycle (to the second granularity), based on a +// traditional crontab specification. It is computed initially and stored as bit sets. +type SpecSchedule struct { + Second, Minute, Hour, Dom, Month, Dow uint64 +} + +// bounds provides a range of acceptable values (plus a map of name to value). +type bounds struct { + min, max uint + names map[string]uint +} + +// The bounds for each field. +var ( + seconds = bounds{0, 59, nil} + minutes = bounds{0, 59, nil} + hours = bounds{0, 23, nil} + dom = bounds{1, 31, nil} + months = bounds{1, 12, map[string]uint{ + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, + }} + dow = bounds{0, 6, map[string]uint{ + "sun": 0, + "mon": 1, + "tue": 2, + "wed": 3, + "thu": 4, + "fri": 5, + "sat": 6, + }} +) + +const ( + // Set the top bit if a star was included in the expression. + starBit = 1 << 63 +) + +// Next returns the next time this schedule is activated, greater than the given +// time. If no time can be found to satisfy the schedule, return the zero time. +func (s *SpecSchedule) Next(t time.Time) time.Time { + // General approach: + // For Month, Day, Hour, Minute, Second: + // Check if the time value matches. If yes, continue to the next field. + // If the field doesn't match the schedule, then increment the field until it matches. + // While incrementing the field, a wrap-around brings it back to the beginning + // of the field list (since it is necessary to re-verify previous field + // values) + + // Start at the earliest possible time (the upcoming second). + t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) + + // This flag indicates whether a field has been incremented. + added := false + + // If no time is found within five years, return zero. + yearLimit := t.Year() + 5 + +WRAP: + if t.Year() > yearLimit { + return time.Time{} + } + + // Find the first applicable month. + // If it's this month, then do nothing. + for 1< 0 + dowMatch bool = 1< 0 + ) + + if s.Dom&starBit > 0 || s.Dow&starBit > 0 { + return domMatch && dowMatch + } + return domMatch || dowMatch +} diff --git a/modules/cron/spec_test.go b/modules/cron/spec_test.go new file mode 100644 index 0000000000..855d79831b --- /dev/null +++ b/modules/cron/spec_test.go @@ -0,0 +1,173 @@ +package cron + +import ( + "testing" + "time" +) + +func TestActivation(t *testing.T) { + tests := []struct { + time, spec string + expected bool + }{ + // Every fifteen minutes. + {"Mon Jul 9 15:00 2012", "0 0/15 * * *", true}, + {"Mon Jul 9 15:45 2012", "0 0/15 * * *", true}, + {"Mon Jul 9 15:40 2012", "0 0/15 * * *", false}, + + // Every fifteen minutes, starting at 5 minutes. + {"Mon Jul 9 15:05 2012", "0 5/15 * * *", true}, + {"Mon Jul 9 15:20 2012", "0 5/15 * * *", true}, + {"Mon Jul 9 15:50 2012", "0 5/15 * * *", true}, + + // Named months + {"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true}, + {"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false}, + + // Everything set. + {"Sun Jul 15 08:30 2012", "0 30 08 ? Jul Sun", true}, + {"Sun Jul 15 08:30 2012", "0 30 08 15 Jul ?", true}, + {"Mon Jul 16 08:30 2012", "0 30 08 ? Jul Sun", false}, + {"Mon Jul 16 08:30 2012", "0 30 08 15 Jul ?", false}, + + // Predefined schedules + {"Mon Jul 9 15:00 2012", "@hourly", true}, + {"Mon Jul 9 15:04 2012", "@hourly", false}, + {"Mon Jul 9 15:00 2012", "@daily", false}, + {"Mon Jul 9 00:00 2012", "@daily", true}, + {"Mon Jul 9 00:00 2012", "@weekly", false}, + {"Sun Jul 8 00:00 2012", "@weekly", true}, + {"Sun Jul 8 01:00 2012", "@weekly", false}, + {"Sun Jul 8 00:00 2012", "@monthly", false}, + {"Sun Jul 1 00:00 2012", "@monthly", true}, + + // Test interaction of DOW and DOM. + // If both are specified, then only one needs to match. + {"Sun Jul 15 00:00 2012", "0 * * 1,15 * Sun", true}, + {"Fri Jun 15 00:00 2012", "0 * * 1,15 * Sun", true}, + {"Wed Aug 1 00:00 2012", "0 * * 1,15 * Sun", true}, + + // However, if one has a star, then both need to match. + {"Sun Jul 15 00:00 2012", "0 * * * * Mon", false}, + {"Sun Jul 15 00:00 2012", "0 * * */10 * Sun", false}, + {"Mon Jul 9 00:00 2012", "0 * * 1,15 * *", false}, + {"Sun Jul 15 00:00 2012", "0 * * 1,15 * *", true}, + {"Sun Jul 15 00:00 2012", "0 * * */2 * Sun", true}, + } + + for _, test := range tests { + sched, err := Parse(test.spec) + if err != nil { + t.Error(err) + continue + } + actual := sched.Next(getTime(test.time).Add(-1 * time.Second)) + expected := getTime(test.time) + if test.expected && expected != actual || !test.expected && expected == actual { + t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)", + test.spec, test.time, expected, actual) + } + } +} + +func TestNext(t *testing.T) { + runs := []struct { + time, spec string + expected string + }{ + // Simple cases + {"Mon Jul 9 14:45 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, + {"Mon Jul 9 14:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, + {"Mon Jul 9 14:59:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, + + // Wrap around hours + {"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"}, + + // Wrap around days + {"Mon Jul 9 23:46 2012", "0 */15 * * *", "Tue Jul 10 00:00 2012"}, + {"Mon Jul 9 23:45 2012", "0 20-35/15 * * *", "Tue Jul 10 00:20 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * *", "Tue Jul 10 00:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * *", "Tue Jul 10 01:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * *", "Tue Jul 10 10:20:15 2012"}, + + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"}, + + // Wrap around months + {"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"}, + {"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Aug 6 00:00 2012"}, + {"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"}, + + // Wrap around years + {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"}, + {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"}, + + // Wrap around minute, hour, day, month, and year + {"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"}, + + // Leap year + {"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"}, + + // Daylight savings time EST -> EDT + {"2012-03-11T00:00:00-0500", "0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"}, + + // Daylight savings time EDT -> EST + {"2012-11-04T00:00:00-0400", "0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"}, + {"2012-11-04T01:45:00-0400", "0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"}, + + // Unsatisfiable + {"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""}, + {"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""}, + } + + for _, c := range runs { + sched, err := Parse(c.spec) + if err != nil { + t.Error(err) + continue + } + actual := sched.Next(getTime(c.time)) + expected := getTime(c.expected) + if !actual.Equal(expected) { + t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual) + } + } +} + +func TestErrors(t *testing.T) { + invalidSpecs := []string{ + "xyz", + "60 0 * * *", + "0 60 * * *", + "0 0 * * XYZ", + } + for _, spec := range invalidSpecs { + _, err := Parse(spec) + if err == nil { + t.Error("expected an error parsing: ", spec) + } + } +} + +func getTime(value string) time.Time { + if value == "" { + return time.Time{} + } + t, err := time.Parse("Mon Jan 2 15:04 2006", value) + if err != nil { + t, err = time.Parse("Mon Jan 2 15:04:05 2006", value) + if err != nil { + t, err = time.Parse("2006-01-02T15:04:05-0700", value) + if err != nil { + panic(err) + } + // Daylight savings time tests require location + if ny, err := time.LoadLocation("America/New_York"); err == nil { + t = t.In(ny) + } + } + } + + return t +} diff --git a/modules/hooks/hooks.go b/modules/hooks/hooks.go deleted file mode 100644 index 6ae4418b35..0000000000 --- a/modules/hooks/hooks.go +++ /dev/null @@ -1,95 +0,0 @@ -// 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 hooks - -import ( - "encoding/json" - "time" - - "github.com/gogits/gogs/modules/httplib" - "github.com/gogits/gogs/modules/log" -) - -// Hook task types. -const ( - HTT_WEBHOOK = iota + 1 - HTT_SERVICE -) - -type PayloadAuthor struct { - Name string `json:"name"` - Email string `json:"email"` -} - -type PayloadCommit struct { - Id string `json:"id"` - Message string `json:"message"` - Url string `json:"url"` - Author *PayloadAuthor `json:"author"` -} - -type PayloadRepo struct { - Id int64 `json:"id"` - Name string `json:"name"` - Url string `json:"url"` - Description string `json:"description"` - Website string `json:"website"` - Watchers int `json:"watchers"` - Owner *PayloadAuthor `json:"author"` - Private bool `json:"private"` -} - -// Payload represents payload information of hook. -type Payload struct { - Secret string `json:"secret"` - Ref string `json:"ref"` - Commits []*PayloadCommit `json:"commits"` - Repo *PayloadRepo `json:"repository"` - Pusher *PayloadAuthor `json:"pusher"` -} - -// HookTask represents hook task. -type HookTask struct { - Type int - Url string - *Payload - ContentType int - IsSsl bool -} - -var ( - taskQueue = make(chan *HookTask, 1000) -) - -// AddHookTask adds new hook task to task queue. -func AddHookTask(t *HookTask) { - taskQueue <- t -} - -func init() { - go handleQueue() -} - -func handleQueue() { - for { - select { - case t := <-taskQueue: - // Only support JSON now. - data, err := json.MarshalIndent(t.Payload, "", "\t") - if err != nil { - log.Error("hooks.handleQueue(json): %v", err) - continue - } - - _, err = httplib.Post(t.Url).SetTimeout(5*time.Second, 5*time.Second). - Body(data).Response() - if err != nil { - log.Error("hooks.handleQueue: Fail to deliver hook: %v", err) - continue - } - log.Info("Hook delivered: %s", string(data)) - } - } -} diff --git a/modules/log/log.go b/modules/log/log.go index f83ec0ad4f..24f0442d1e 100644 --- a/modules/log/log.go +++ b/modules/log/log.go @@ -6,13 +6,16 @@ package log import ( + "fmt" "os" + "path" "github.com/gogits/logs" ) var ( - loggers []*logs.BeeLogger + loggers []*logs.BeeLogger + GitLogger *logs.BeeLogger ) func init() { @@ -33,7 +36,15 @@ func NewLogger(bufLen int64, mode, config string) { loggers = append(loggers, logger) } logger.SetLogFuncCallDepth(3) - logger.SetLogger(mode, config) + if err := logger.SetLogger(mode, config); err != nil { + Fatal("Fail to set logger(%s): %v", mode, err) + } +} + +func NewGitLogger(logPath string) { + os.MkdirAll(path.Dir(logPath), os.ModePerm) + GitLogger = logs.NewLogger(0) + GitLogger.SetLogger("file", fmt.Sprintf(`{"level":0,"filename":"%s","rotate":false}`, logPath)) } func Trace(format string, v ...interface{}) { diff --git a/modules/mailer/mail.go b/modules/mailer/mail.go index 6e34439e15..62e15cd7fe 100644 --- a/modules/mailer/mail.go +++ b/modules/mailer/mail.go @@ -17,6 +17,15 @@ import ( "github.com/gogits/gogs/modules/setting" ) +const ( + AUTH_ACTIVE base.TplName = "mail/auth/active" + AUTH_REGISTER_SUCCESS base.TplName = "mail/auth/register_success" + AUTH_RESET_PASSWORD base.TplName = "mail/auth/reset_passwd" + + NOTIFY_COLLABORATOR base.TplName = "mail/notify/collaborator" + NOTIFY_MENTION base.TplName = "mail/notify/mention" +) + // Create New mail message use MailFrom and MailUser func NewMailMessageFrom(To []string, from, subject, body string) Message { msg := NewHtmlMessage(To, from, subject, body) @@ -26,10 +35,10 @@ func NewMailMessageFrom(To []string, from, subject, body string) Message { // Create New mail message use MailFrom and MailUser func NewMailMessage(To []string, subject, body string) Message { - return NewMailMessageFrom(To, setting.MailService.User, subject, body) + return NewMailMessageFrom(To, setting.MailService.From, subject, body) } -func GetMailTmplData(user *models.User) map[interface{}]interface{} { +func GetMailTmplData(u *models.User) map[interface{}]interface{} { data := make(map[interface{}]interface{}, 10) data["AppName"] = setting.AppName data["AppVer"] = setting.AppVer @@ -37,84 +46,84 @@ func GetMailTmplData(user *models.User) map[interface{}]interface{} { data["AppLogo"] = setting.AppLogo data["ActiveCodeLives"] = setting.Service.ActiveCodeLives / 60 data["ResetPwdCodeLives"] = setting.Service.ResetPwdCodeLives / 60 - if user != nil { - data["User"] = user + if u != nil { + data["User"] = u } return data } // create a time limit code for user active -func CreateUserActiveCode(user *models.User, startInf interface{}) string { +func CreateUserActiveCode(u *models.User, startInf interface{}) string { minutes := setting.Service.ActiveCodeLives - data := base.ToStr(user.Id) + user.Email + user.LowerName + user.Passwd + user.Rands + data := base.ToStr(u.Id) + u.Email + u.LowerName + u.Passwd + u.Rands code := base.CreateTimeLimitCode(data, minutes, startInf) // add tail hex username - code += hex.EncodeToString([]byte(user.LowerName)) + code += hex.EncodeToString([]byte(u.LowerName)) return code } // Send user register mail with active code -func SendRegisterMail(r *middleware.Render, user *models.User) { - code := CreateUserActiveCode(user, nil) +func SendRegisterMail(r *middleware.Render, u *models.User) { + code := CreateUserActiveCode(u, nil) subject := "Register success, Welcome" - data := GetMailTmplData(user) + data := GetMailTmplData(u) data["Code"] = code - body, err := r.HTMLString("mail/auth/register_success", data) + body, err := r.HTMLString(string(AUTH_REGISTER_SUCCESS), data) if err != nil { log.Error("mail.SendRegisterMail(fail to render): %v", err) return } - msg := NewMailMessage([]string{user.Email}, subject, body) - msg.Info = fmt.Sprintf("UID: %d, send register mail", user.Id) + msg := NewMailMessage([]string{u.Email}, subject, body) + msg.Info = fmt.Sprintf("UID: %d, send register mail", u.Id) SendAsync(&msg) } // Send email verify active email. -func SendActiveMail(r *middleware.Render, user *models.User) { - code := CreateUserActiveCode(user, nil) +func SendActiveMail(r *middleware.Render, u *models.User) { + code := CreateUserActiveCode(u, nil) subject := "Verify your e-mail address" - data := GetMailTmplData(user) + data := GetMailTmplData(u) data["Code"] = code - body, err := r.HTMLString("mail/auth/active_email", data) + body, err := r.HTMLString(string(AUTH_ACTIVE), data) if err != nil { log.Error("mail.SendActiveMail(fail to render): %v", err) return } - msg := NewMailMessage([]string{user.Email}, subject, body) - msg.Info = fmt.Sprintf("UID: %d, send active mail", user.Id) + msg := NewMailMessage([]string{u.Email}, subject, body) + msg.Info = fmt.Sprintf("UID: %d, send active mail", u.Id) SendAsync(&msg) } // Send reset password email. -func SendResetPasswdMail(r *middleware.Render, user *models.User) { - code := CreateUserActiveCode(user, nil) +func SendResetPasswdMail(r *middleware.Render, u *models.User) { + code := CreateUserActiveCode(u, nil) subject := "Reset your password" - data := GetMailTmplData(user) + data := GetMailTmplData(u) data["Code"] = code - body, err := r.HTMLString("mail/auth/reset_passwd", data) + body, err := r.HTMLString(string(AUTH_RESET_PASSWORD), data) if err != nil { log.Error("mail.SendResetPasswdMail(fail to render): %v", err) return } - msg := NewMailMessage([]string{user.Email}, subject, body) - msg.Info = fmt.Sprintf("UID: %d, send reset password email", user.Id) + msg := NewMailMessage([]string{u.Email}, subject, body) + msg.Info = fmt.Sprintf("UID: %d, send reset password email", u.Id) SendAsync(&msg) } // SendIssueNotifyMail sends mail notification of all watchers of repository. -func SendIssueNotifyMail(user, owner *models.User, repo *models.Repository, issue *models.Issue) ([]string, error) { +func SendIssueNotifyMail(u, owner *models.User, repo *models.Repository, issue *models.Issue) ([]string, error) { ws, err := models.GetWatchers(repo.Id) if err != nil { return nil, errors.New("mail.NotifyWatchers(GetWatchers): " + err.Error()) @@ -123,7 +132,7 @@ func SendIssueNotifyMail(user, owner *models.User, repo *models.Repository, issu tos := make([]string, 0, len(ws)) for i := range ws { uid := ws[i].UserId - if user.Id == uid { + if u.Id == uid { continue } u, err := models.GetUserById(uid) @@ -141,14 +150,14 @@ func SendIssueNotifyMail(user, owner *models.User, repo *models.Repository, issu content := fmt.Sprintf("%s
-
View it on Gogs.", base.RenderSpecialLink([]byte(issue.Content), owner.Name+"/"+repo.Name), setting.AppUrl, owner.Name, repo.Name, issue.Index) - msg := NewMailMessageFrom(tos, user.Email, subject, content) + msg := NewMailMessageFrom(tos, u.Email, subject, content) msg.Info = fmt.Sprintf("Subject: %s, send issue notify emails", subject) SendAsync(&msg) return tos, nil } // SendIssueMentionMail sends mail notification for who are mentioned in issue. -func SendIssueMentionMail(r *middleware.Render, user, owner *models.User, +func SendIssueMentionMail(r *middleware.Render, u, owner *models.User, repo *models.Repository, issue *models.Issue, tos []string) error { if len(tos) == 0 { @@ -161,19 +170,19 @@ func SendIssueMentionMail(r *middleware.Render, user, owner *models.User, data["IssueLink"] = fmt.Sprintf("%s/%s/issues/%d", owner.Name, repo.Name, issue.Index) data["Subject"] = subject - body, err := r.HTMLString("mail/notify/mention", data) + body, err := r.HTMLString(string(NOTIFY_MENTION), data) if err != nil { return fmt.Errorf("mail.SendIssueMentionMail(fail to render): %v", err) } - msg := NewMailMessageFrom(tos, user.Email, subject, body) + msg := NewMailMessageFrom(tos, u.Email, subject, body) msg.Info = fmt.Sprintf("Subject: %s, send issue mention emails", subject) SendAsync(&msg) return nil } // SendCollaboratorMail sends mail notification to new collaborator. -func SendCollaboratorMail(r *middleware.Render, user, owner *models.User, +func SendCollaboratorMail(r *middleware.Render, u, owner *models.User, repo *models.Repository) error { subject := fmt.Sprintf("%s added you to %s", owner.Name, repo.Name) @@ -182,13 +191,13 @@ func SendCollaboratorMail(r *middleware.Render, user, owner *models.User, data["RepoLink"] = path.Join(owner.Name, repo.Name) data["Subject"] = subject - body, err := r.HTMLString("mail/notify/collaborator", data) + body, err := r.HTMLString(string(NOTIFY_COLLABORATOR), data) if err != nil { return fmt.Errorf("mail.SendCollaboratorMail(fail to render): %v", err) } - msg := NewMailMessage([]string{user.Email}, subject, body) - msg.Info = fmt.Sprintf("UID: %d, send register mail", user.Id) + msg := NewMailMessage([]string{u.Email}, subject, body) + msg.Info = fmt.Sprintf("UID: %d, send register mail", u.Id) SendAsync(&msg) return nil diff --git a/modules/middleware/context.go b/modules/middleware/context.go index 8c837d0852..45f0140a28 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -104,12 +104,12 @@ func (ctx *Context) HasError() bool { } // HTML calls render.HTML underlying but reduce one argument. -func (ctx *Context) HTML(status int, name string, htmlOpt ...HTMLOptions) { - ctx.Render.HTML(status, name, ctx.Data, htmlOpt...) +func (ctx *Context) HTML(status int, name base.TplName, htmlOpt ...HTMLOptions) { + ctx.Render.HTML(status, string(name), ctx.Data, htmlOpt...) } // RenderWithErr used for page has form validation but need to prompt error to users. -func (ctx *Context) RenderWithErr(msg, tpl string, form auth.Form) { +func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form auth.Form) { if form != nil { auth.AssignForm(form, ctx.Data) } @@ -133,7 +133,7 @@ func (ctx *Context) Handle(status int, title string, err error) { case 500: ctx.Data["Title"] = "Internal Server Error" } - ctx.HTML(status, fmt.Sprintf("status/%d", status)) + ctx.HTML(status, base.TplName(fmt.Sprintf("status/%d", status))) } func (ctx *Context) Debug(msg string, args ...interface{}) { @@ -358,7 +358,7 @@ func InitContext() martini.Handler { }) // Get user from session if logined. - user := auth.SignedInUser(ctx.Session) + user := auth.SignedInUser(ctx.req.Header, ctx.Session) ctx.User = user ctx.IsSigned = user != nil diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index c1acc827ee..0c64027552 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -21,21 +21,17 @@ import ( func RepoAssignment(redirect bool, args ...bool) martini.Handler { return func(ctx *Context, params martini.Params) { - log.Trace(fmt.Sprint(args)) // valid brachname var validBranch bool // display bare quick start if it is a bare repo var displayBare bool if len(args) >= 1 { - // Note: argument has wrong value in Go1.3 martini. - // validBranch = args[0] - validBranch = true + validBranch = args[0] } if len(args) >= 2 { - // displayBare = args[1] - displayBare = true + displayBare = args[1] } var ( @@ -48,9 +44,10 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler { repoName := params["reponame"] refName := params["branchname"] + // TODO: need more advanced onwership and access level check. // Collaborators who have write access can be seen as owners. if ctx.IsSigned { - ctx.Repo.IsOwner, err = models.HasAccess(ctx.User.Name, userName+"/"+repoName, models.AU_WRITABLE) + ctx.Repo.IsOwner, err = models.HasAccess(ctx.User.Name, userName+"/"+repoName, models.WRITABLE) if err != nil { ctx.Handle(500, "RepoAssignment(HasAccess)", err) return @@ -111,7 +108,7 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler { return } - hasAccess, err := models.HasAccess(ctx.User.Name, ctx.Repo.Owner.Name+"/"+repo.Name, models.AU_READABLE) + hasAccess, err := models.HasAccess(ctx.User.Name, ctx.Repo.Owner.Name+"/"+repo.Name, models.READABLE) if err != nil { ctx.Handle(500, "RepoAssignment(HasAccess)", err) return diff --git a/modules/process/manager.go b/modules/process/manager.go new file mode 100644 index 0000000000..173b2aa4ee --- /dev/null +++ b/modules/process/manager.go @@ -0,0 +1,89 @@ +// 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 process + +import ( + "bytes" + "fmt" + "os/exec" + "time" + + "github.com/gogits/gogs/modules/log" +) + +// Process represents a working process inherit from Gogs. +type Process struct { + Pid int64 // Process ID, not system one. + Description string + Start time.Time + Cmd *exec.Cmd +} + +// List of existing processes. +var ( + curPid int64 = 1 + Processes []*Process +) + +// Add adds a existing process and returns its PID. +func Add(desc string, cmd *exec.Cmd) int64 { + pid := curPid + Processes = append(Processes, &Process{ + Pid: pid, + Description: desc, + Start: time.Now(), + Cmd: cmd, + }) + curPid++ + return pid +} + +func ExecDir(dir, desc, cmdName string, args ...string) (string, string, error) { + bufOut := new(bytes.Buffer) + bufErr := new(bytes.Buffer) + + cmd := exec.Command(cmdName, args...) + cmd.Dir = dir + cmd.Stdout = bufOut + cmd.Stderr = bufErr + + pid := Add(desc, cmd) + err := cmd.Run() + if errKill := Kill(pid); errKill != nil { + log.Error("Exec: %v", pid, desc, errKill) + } + return bufOut.String(), bufErr.String(), err +} + +// Exec starts executing a command and record its process. +func Exec(desc, cmdName string, args ...string) (string, string, error) { + return ExecDir("", desc, cmdName, args...) +} + +// Remove removes a process from list. +func Remove(pid int64) { + for i, proc := range Processes { + if proc.Pid == pid { + Processes = append(Processes[:i], Processes[i+1:]...) + return + } + } +} + +// Kill kills and removes a process from list. +func Kill(pid int64) error { + for i, proc := range Processes { + if proc.Pid == pid { + if proc.Cmd.Process != nil && proc.Cmd.ProcessState != nil && !proc.Cmd.ProcessState.Exited() { + if err := proc.Cmd.Process.Kill(); err != nil { + return fmt.Errorf("fail to kill process(%d/%s): %v", proc.Pid, proc.Description, err) + } + } + Processes = append(Processes[:i], Processes[i+1:]...) + return nil + } + } + return nil +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 8cca57efed..f03aa8aeae 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -47,11 +47,16 @@ var ( StaticRootPath string // Security settings. - InstallLock bool - SecretKey string - LogInRememberDays int - CookieUserName string - CookieRememberName string + InstallLock bool + SecretKey string + LogInRememberDays int + CookieUserName string + CookieRememberName string + ReverseProxyAuthUser string + + // Webhook settings. + WebhookTaskInterval int + WebhookDeliverTimeout int // Repository settings. RepoRootPath string @@ -86,8 +91,7 @@ var ( RunUser string ) -// WorkDir returns absolute path of work directory. -func WorkDir() (string, error) { +func ExecPath() (string, error) { file, err := exec.LookPath(os.Args[0]) if err != nil { return "", err @@ -96,7 +100,13 @@ func WorkDir() (string, error) { if err != nil { return "", err } - return path.Dir(strings.Replace(p, "\\", "/", -1)), nil + return p, nil +} + +// WorkDir returns absolute path of work directory. +func WorkDir() (string, error) { + execPath, err := ExecPath() + return path.Dir(strings.Replace(execPath, "\\", "/", -1)), err } // NewConfigContext initializes configuration context. @@ -154,6 +164,7 @@ func NewConfigContext() { LogInRememberDays = Cfg.MustInt("security", "LOGIN_REMEMBER_DAYS") CookieUserName = Cfg.MustValue("security", "COOKIE_USERNAME") CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME") + ReverseProxyAuthUser = Cfg.MustValue("security", "REVERSE_PROXY_AUTHENTICATION_USER", "X-WEBAUTH-USER") RunUser = Cfg.MustValue("", "RUN_USER") curUser := os.Getenv("USER") @@ -171,6 +182,12 @@ func NewConfigContext() { log.Fatal("Fail to get home directory: %v", err) } RepoRootPath = Cfg.MustValue("repository", "ROOT", filepath.Join(homeDir, "gogs-repositories")) + if !filepath.IsAbs(RepoRootPath) { + RepoRootPath = filepath.Join(workDir, RepoRootPath) + } else { + RepoRootPath = filepath.Clean(RepoRootPath) + } + if err = os.MkdirAll(RepoRootPath, os.ModePerm); err != nil { log.Fatal("Fail to create repository root path(%s): %v", RepoRootPath, err) } @@ -182,14 +199,15 @@ func NewConfigContext() { } var Service struct { - RegisterEmailConfirm bool - DisableRegistration bool - RequireSignInView bool - EnableCacheAvatar bool - NotifyMail bool - ActiveCodeLives int - ResetPwdCodeLives int - LdapAuth bool + RegisterEmailConfirm bool + DisableRegistration bool + RequireSignInView bool + EnableCacheAvatar bool + EnableNotifyMail bool + EnableReverseProxyAuth bool + LdapAuth bool + ActiveCodeLives int + ResetPwdCodeLives int } func newService() { @@ -198,6 +216,7 @@ func newService() { Service.DisableRegistration = Cfg.MustBool("service", "DISABLE_REGISTRATION") Service.RequireSignInView = Cfg.MustBool("service", "REQUIRE_SIGNIN_VIEW") Service.EnableCacheAvatar = Cfg.MustBool("service", "ENABLE_CACHE_AVATAR") + Service.EnableReverseProxyAuth = Cfg.MustBool("service", "ENABLE_REVERSE_PROXY_AUTHENTICATION") } var logLevels = map[string]string{ @@ -246,20 +265,20 @@ func newLogService() { Cfg.MustBool(modeSec, "DAILY_ROTATE", true), Cfg.MustInt(modeSec, "MAX_DAYS", 7)) case "conn": - LogConfigs[i] = fmt.Sprintf(`{"level":"%s","reconnectOnMsg":%v,"reconnect":%v,"net":"%s","addr":"%s"}`, level, + LogConfigs[i] = fmt.Sprintf(`{"level":%s,"reconnectOnMsg":%v,"reconnect":%v,"net":"%s","addr":"%s"}`, level, Cfg.MustBool(modeSec, "RECONNECT_ON_MSG"), Cfg.MustBool(modeSec, "RECONNECT"), Cfg.MustValueRange(modeSec, "PROTOCOL", "tcp", []string{"tcp", "unix", "udp"}), Cfg.MustValue(modeSec, "ADDR", ":7020")) case "smtp": - LogConfigs[i] = fmt.Sprintf(`{"level":"%s","username":"%s","password":"%s","host":"%s","sendTos":"%s","subject":"%s"}`, level, + LogConfigs[i] = fmt.Sprintf(`{"level":%s,"username":"%s","password":"%s","host":"%s","sendTos":"%s","subject":"%s"}`, level, Cfg.MustValue(modeSec, "USER", "example@example.com"), Cfg.MustValue(modeSec, "PASSWD", "******"), Cfg.MustValue(modeSec, "HOST", "127.0.0.1:25"), Cfg.MustValue(modeSec, "RECEIVERS", "[]"), Cfg.MustValue(modeSec, "SUBJECT", "Diagnostic message from serve")) case "database": - LogConfigs[i] = fmt.Sprintf(`{"level":"%s","driver":"%s","conn":"%s"}`, level, + LogConfigs[i] = fmt.Sprintf(`{"level":%s,"driver":"%s","conn":"%s"}`, level, Cfg.MustValue(modeSec, "DRIVER"), Cfg.MustValue(modeSec, "CONN")) } @@ -330,6 +349,7 @@ func newSessionService() { type Mailer struct { Name string Host string + From string User, Passwd string } @@ -363,6 +383,7 @@ func newMailService() { User: Cfg.MustValue("mailer", "USER"), Passwd: Cfg.MustValue("mailer", "PASSWD"), } + MailService.From = Cfg.MustValue("mailer", "FROM", MailService.User) log.Info("Mail Service Enabled") } @@ -384,10 +405,15 @@ func newNotifyMailService() { log.Warn("Notify Mail Service: Mail Service is not enabled") return } - Service.NotifyMail = true + Service.EnableNotifyMail = true log.Info("Notify Mail Service Enabled") } +func newWebhookService() { + WebhookTaskInterval = Cfg.MustInt("webhook", "TASK_INTERVAL", 1) + WebhookDeliverTimeout = Cfg.MustInt("webhook", "DELIVER_TIMEOUT", 5) +} + func NewServices() { newService() newLogService() @@ -396,4 +422,5 @@ func NewServices() { newMailService() newRegisterMailService() newNotifyMailService() + newWebhookService() } diff --git a/modules/social/social.go b/modules/social/social.go index 62f4d51835..326a463fac 100644 --- a/modules/social/social.go +++ b/modules/social/social.go @@ -120,7 +120,7 @@ type SocialGithub struct { } func (s *SocialGithub) Type() int { - return models.OT_GITHUB + return int(models.GITHUB) } func newGitHubOauth(config *oauth.Config) { @@ -174,7 +174,7 @@ type SocialGoogle struct { } func (s *SocialGoogle) Type() int { - return models.OT_GOOGLE + return int(models.GOOGLE) } func newGoogleOauth(config *oauth.Config) { @@ -229,7 +229,7 @@ type SocialTencent struct { } func (s *SocialTencent) Type() int { - return models.OT_QQ + return int(models.QQ) } func newTencentOauth(config *oauth.Config) { @@ -295,7 +295,7 @@ type SocialTwitter struct { } func (s *SocialTwitter) Type() int { - return models.OT_TWITTER + return int(models.TWITTER) } func newTwitterOauth(config *oauth.Config) { @@ -351,7 +351,7 @@ type SocialWeibo struct { } func (s *SocialWeibo) Type() int { - return models.OT_WEIBO + return int(models.WEIBO) } func newWeiboOauth(config *oauth.Config) { diff --git a/public/css/font-awesome.min.css b/public/css/font-awesome.min.css index 449d6ac551..3d920fc87c 100644 --- a/public/css/font-awesome.min.css +++ b/public/css/font-awesome.min.css @@ -1,4 +1,4 @@ /*! - * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"} \ No newline at end of file + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.1.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1);-o-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-moz-transform:scale(1, -1);-ms-transform:scale(1, -1);-o-transform:scale(1, -1);transform:scale(1, -1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-square:before,.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"} \ No newline at end of file diff --git a/public/css/gogs.css b/public/css/gogs.css index 79fd4bf908..98cb5ee188 100755 --- a/public/css/gogs.css +++ b/public/css/gogs.css @@ -10,7 +10,7 @@ body { html, body { height: 100%; - font-family: Helvetica, Arial, sans-serif; + font-family: Arial, Helvetica, sans-serif; } /* override bs3 */ @@ -257,6 +257,9 @@ html, body { .card .btn { cursor: pointer; +} + +.card .btn-primary { margin-right: 1.2em; } @@ -372,7 +375,7 @@ html, body { /* gogits repo create */ -#repo-create { +#repo-create, #org-create, #org-teams-create, #org-teams-edit { width: 800px; } @@ -638,6 +641,55 @@ html, body { margin: 0 .5em; } +#dashboard-switch .btn, #repo-owner-switch .btn { + height: 40px; +} + +#dashboard-switch { + margin-top: 14px; + margin-right: 18px; +} + +#dashboard-switch .dropdown-menu,#repo-owner-switch .dropdown-menu { + padding: 0; +} + +#dashboard-switch-menu { + width: 180px; + margin-bottom: 0; + padding-bottom: 0; +} + +#dashboard-switch-menu > li > a { + display: block; + padding: .8em 1.2em; +} + +#dashboard-switch-menu > li > a:hover { + text-decoration: none; +} + +#dashboard-switch-menu > li > a img, #dashboard-switch button img { + margin-right: 6px; +} + +#dashboard-switch-menu > li { + border-bottom: 1px solid #eaeaea; +} + +#dashboard-switch-menu > li .fa { + opacity: 0; + margin-right: 16px; +} + +#dashboard-switch-menu > li.checked .fa { + opacity: 1; +} + +#dashboard-switch-menu > li:last-child { + border-bottom: none; +} + /* gogits repo single page */ #body-nav.repo-nav { @@ -1643,7 +1695,7 @@ html, body { vertical-align: top; } -#label-color-change-ipt2{ +#label-color-change-ipt2 { margin-top: 1px; } @@ -1814,4 +1866,196 @@ html, body { #release-preview { margin: 6px 0; +} + +/* organization */ + +#body-nav.org-nav { + height: 140px; + padding: 16px 0; +} + +#body-nav.org-nav.org-nav-auto { + height: auto; +} + +.org-nav > .container { + padding-left: 0; + padding-left: 0; +} + +.org-nav .org-logo { + margin-right: 16px; + width: 100px; + height: 100px; +} + +.org-nav .org-small-logo { + margin-right: 16px; + width: 50px; + height: 50px; +} + +.org-nav .org-name { + margin-top: 0; +} + +.org-nav-auto .org-name { + font-size: 1.4em; + line-height: 48px; +} + +#body-nav.org-nav-auto .nav { + margin-top: 6px; +} + +#body-nav.org-nav-auto .nav a:hover { + text-decoration: none; +} + +.org-description { + font-size: 16px; +} + +.org-meta li, .org-meta li a, .org-repo-update, .org-repo-status, .org-team-meta { + color: #888; +} + +.org-meta li { + margin-right: 12px; +} + +.org-meta li a:hover { + text-decoration: underline; +} + +.org-meta .fa { + margin-left: 0; +} + +.org-main { + padding-left: 0; +} + +.org-sidebar { + margin-top: -100px; +} + +.org-panel .panel-heading { + font-size: 18px; +} + +.org-repo-status { + font-family: Verdana, Arial, Helvetica, sans-serif; +} + +.org-repo-item { + border-bottom: 1px solid #DDD; + padding-bottom: 18px; +} + +.org-member img { + width: 60px; + height: 60px; + border-radius: 4px; +} + +.org-member { + display: inline-block; + padding: 2px; +} + +.org-team-name { + font-size: 15px; + margin-bottom: 0; + color: #444; +} + +.org-team { + border-bottom: 1px solid #DDD; + margin-bottom: 12px; +} + +.org-team:last-child { + border: none; +} + +.org-team a { + display: block; +} + +.org-team a:hover { + text-decoration: none; +} + +.org-team a:hover .org-team-name { + color: #0079bc !important; +} + +#org-members { + margin-right: 30px; +} + +#org-members .member .avatar img { + width: 50px; + height: 50px; +} + +#org-members .member { + padding-bottom: 20px; + margin-bottom: 20px; + border-bottom: 1px solid #DDD; + height: 70px; +} + +#org-members .member .name { + padding-top: 4px; +} + +#org-members .member .nick { + display: block; + color: #888; +} + +#org-members .member .name a { + color: #444; +} + +#org-members .member .name strong { + font-size: 1.2em; +} + +#org-members .status, #org-members .role { + line-height: 48px; + text-align: right; +} + +#org-teams .org-team .panel-heading { + margin-top: 0; +} + +#org-teams .org-team .panel-heading a { + color: #444; +} + +#org-teams .org-team-members { + margin-top: 18px; +} + +#org-teams .org-team-members img { + width: 40px; + height: 40px; + margin-right: 12px; +} + +#org-teams .org-team-members a { + display: inline-block; +} + +#org-teams .org-team .panel-footer { + height: 60px; +} + +#org-teams .org-team { + border-bottom: none; } \ No newline at end of file diff --git a/public/fonts/FontAwesome.otf b/public/fonts/FontAwesome.otf index 8b0f54e47e..3461e3fce6 100644 Binary files a/public/fonts/FontAwesome.otf and b/public/fonts/FontAwesome.otf differ diff --git a/public/fonts/fontawesome-webfont.eot b/public/fonts/fontawesome-webfont.eot index 7c79c6a6bc..6cfd566095 100755 Binary files a/public/fonts/fontawesome-webfont.eot and b/public/fonts/fontawesome-webfont.eot differ diff --git a/public/fonts/fontawesome-webfont.svg b/public/fonts/fontawesome-webfont.svg index 45fdf33830..a9f8469503 100755 --- a/public/fonts/fontawesome-webfont.svg +++ b/public/fonts/fontawesome-webfont.svg @@ -14,10 +14,11 @@ + - + - + @@ -30,385 +31,474 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/fonts/fontawesome-webfont.ttf b/public/fonts/fontawesome-webfont.ttf index e89738de5e..5cd6cff6d6 100755 Binary files a/public/fonts/fontawesome-webfont.ttf and b/public/fonts/fontawesome-webfont.ttf differ diff --git a/public/fonts/fontawesome-webfont.woff b/public/fonts/fontawesome-webfont.woff index 8c1748aab7..9eaecb3799 100755 Binary files a/public/fonts/fontawesome-webfont.woff and b/public/fonts/fontawesome-webfont.woff differ diff --git a/public/js/app.js b/public/js/app.js index f56718067e..6edade445b 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -705,8 +705,8 @@ function initRelease() { (function () { $('[data-ajax-name=release-preview]').on("click", function () { var $this = $(this); - $this.toggleAjax(function (json) { - $($this.data("preview")).html(json.ok ? json.content : "no content"); + $this.toggleAjax(function (resp) { + $($this.data("preview")).html(resp); }, function () { $($this.data("preview")).html("no content"); }) @@ -758,6 +758,27 @@ function initRepoSetting() { }); } +function initRepoCreating() { + // owner switch menu click + (function () { + $('#repo-owner-switch .dropdown-menu').on("click", "li", function () { + var uid = $(this).data('uid'); + // set to input + $('#repo-owner-id').val(uid); + // set checked class + if (!$(this).hasClass("checked")) { + $(this).parent().find(".checked").removeClass("checked"); + $(this).addClass("checked"); + } + // set button group to show clicked owner + $('#repo-owner-avatar').attr("src",$(this).find('img').attr("src")); + $('#repo-owner-name').text($(this).text().trim()); + console.log("set repo owner to uid :",uid,$(this).text().trim()); + }); + }()); + console.log("init repo-creating scripts"); +} + (function ($) { $(function () { initCore(); @@ -780,6 +801,9 @@ function initRepoSetting() { if ($('#repo-setting-container').length) { initRepoSetting(); } + if ($('#repo-create').length) { + initRepoCreating(); + } }); })(jQuery); diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 6f8868a659..50a3823a0f 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -14,10 +14,22 @@ import ( "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/cron" "github.com/gogits/gogs/modules/middleware" + "github.com/gogits/gogs/modules/process" "github.com/gogits/gogs/modules/setting" ) +const ( + DASHBOARD base.TplName = "admin/dashboard" + USERS base.TplName = "admin/users" + REPOS base.TplName = "admin/repos" + AUTHS base.TplName = "admin/auths" + CONFIG base.TplName = "admin/config" + MONITOR_PROCESS base.TplName = "admin/monitor/process" + MONITOR_CRON base.TplName = "admin/monitor/cron" +) + var startTime = time.Now() var sysStatus struct { @@ -100,8 +112,11 @@ func updateSystemStatus() { } // Operation types. +type AdminOperation int + const ( - OT_CLEAN_OAUTH = iota + 1 + CLEAN_UNBIND_OAUTH AdminOperation = iota + 1 + CLEAN_INACTIVATE_USER ) func Dashboard(ctx *middleware.Context) { @@ -114,10 +129,13 @@ func Dashboard(ctx *middleware.Context) { var err error var success string - switch op { - case OT_CLEAN_OAUTH: + switch AdminOperation(op) { + case CLEAN_UNBIND_OAUTH: success = "All unbind OAuthes have been deleted." err = models.CleanUnbindOauth() + case CLEAN_INACTIVATE_USER: + success = "All inactivate accounts have been deleted." + err = models.DeleteInactivateUsers() } if err != nil { @@ -132,7 +150,7 @@ func Dashboard(ctx *middleware.Context) { ctx.Data["Stats"] = models.GetStatistic() updateSystemStatus() ctx.Data["SysStatus"] = sysStatus - ctx.HTML(200, "admin/dashboard") + ctx.HTML(200, DASHBOARD) } func Users(ctx *middleware.Context) { @@ -142,10 +160,10 @@ func Users(ctx *middleware.Context) { var err error ctx.Data["Users"], err = models.GetUsers(200, 0) if err != nil { - ctx.Handle(500, "admin.Users", err) + ctx.Handle(500, "admin.Users(GetUsers)", err) return } - ctx.HTML(200, "admin/users") + ctx.HTML(200, USERS) } func Repositories(ctx *middleware.Context) { @@ -158,7 +176,7 @@ func Repositories(ctx *middleware.Context) { ctx.Handle(500, "admin.Repositories", err) return } - ctx.HTML(200, "admin/repos") + ctx.HTML(200, REPOS) } func Auths(ctx *middleware.Context) { @@ -171,7 +189,7 @@ func Auths(ctx *middleware.Context) { ctx.Handle(500, "admin.Auths", err) return } - ctx.HTML(200, "admin/auths") + ctx.HTML(200, AUTHS) } func Config(ctx *middleware.Context) { @@ -188,11 +206,15 @@ func Config(ctx *middleware.Context) { ctx.Data["StaticRootPath"] = setting.StaticRootPath ctx.Data["LogRootPath"] = setting.LogRootPath ctx.Data["ScriptType"] = setting.ScriptType + ctx.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser ctx.Data["Service"] = setting.Service ctx.Data["DbCfg"] = models.DbCfg + ctx.Data["WebhookTaskInterval"] = setting.WebhookTaskInterval + ctx.Data["WebhookDeliverTimeout"] = setting.WebhookDeliverTimeout + ctx.Data["MailerEnabled"] = false if setting.MailService != nil { ctx.Data["MailerEnabled"] = true @@ -223,5 +245,22 @@ func Config(ctx *middleware.Context) { } ctx.Data["Loggers"] = loggers - ctx.HTML(200, "admin/config") + ctx.HTML(200, CONFIG) +} + +func Monitor(ctx *middleware.Context) { + ctx.Data["Title"] = "Monitoring Center" + ctx.Data["PageIsMonitor"] = true + + tab := ctx.Query("tab") + switch tab { + case "process": + ctx.Data["PageIsMonitorProcess"] = true + ctx.Data["Processes"] = process.Processes + ctx.HTML(200, MONITOR_PROCESS) + default: + ctx.Data["PageIsMonitorCron"] = true + ctx.Data["Entries"] = cron.ListEntries() + ctx.HTML(200, MONITOR_CRON) + } } diff --git a/routers/admin/auths.go b/routers/admin/auth.go similarity index 84% rename from routers/admin/auths.go rename to routers/admin/auth.go index c4702afc81..ff6c0325b6 100644 --- a/routers/admin/auths.go +++ b/routers/admin/auth.go @@ -18,12 +18,17 @@ import ( "github.com/gogits/gogs/modules/middleware" ) +const ( + AUTH_NEW base.TplName = "admin/auth/new" + AUTH_EDIT base.TplName = "admin/auth/edit" +) + func NewAuthSource(ctx *middleware.Context) { ctx.Data["Title"] = "New Authentication" ctx.Data["PageIsAuths"] = true ctx.Data["LoginTypes"] = models.LoginTypes ctx.Data["SMTPAuths"] = models.SMTPAuths - ctx.HTML(200, "admin/auths/new") + ctx.HTML(200, AUTH_NEW) } func NewAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { @@ -33,13 +38,13 @@ func NewAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { ctx.Data["SMTPAuths"] = models.SMTPAuths if ctx.HasError() { - ctx.HTML(200, "admin/auths/new") + ctx.HTML(200, AUTH_NEW) return } var u core.Conversion - switch form.Type { - case models.LT_LDAP: + switch models.LoginType(form.Type) { + case models.LDAP: u = &models.LDAPConfig{ Ldapsource: ldap.Ldapsource{ Host: form.Host, @@ -53,7 +58,7 @@ func NewAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { Name: form.AuthName, }, } - case models.LT_SMTP: + case models.SMTP: u = &models.SMTPConfig{ Auth: form.SmtpAuth, Host: form.SmtpHost, @@ -66,15 +71,15 @@ func NewAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { } var source = &models.LoginSource{ - Type: form.Type, + Type: models.LoginType(form.Type), Name: form.AuthName, IsActived: true, AllowAutoRegister: form.AllowAutoRegister, Cfg: u, } - if err := models.AddSource(source); err != nil { - ctx.Handle(500, "admin.auths.NewAuth", err) + if err := models.CreateSource(source); err != nil { + ctx.Handle(500, "admin.auths.NewAuth(CreateSource)", err) return } @@ -97,11 +102,11 @@ func EditAuthSource(ctx *middleware.Context, params martini.Params) { } u, err := models.GetLoginSourceById(id) if err != nil { - ctx.Handle(500, "admin.user.EditUser", err) + ctx.Handle(500, "admin.user.EditUser(GetLoginSourceById)", err) return } ctx.Data["Source"] = u - ctx.HTML(200, "admin/auths/edit") + ctx.HTML(200, AUTH_EDIT) } func EditAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { @@ -111,13 +116,13 @@ func EditAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { ctx.Data["SMTPAuths"] = models.SMTPAuths if ctx.HasError() { - ctx.HTML(200, "admin/auths/edit") + ctx.HTML(200, AUTH_EDIT) return } var config core.Conversion - switch form.Type { - case models.LT_LDAP: + switch models.LoginType(form.Type) { + case models.LDAP: config = &models.LDAPConfig{ Ldapsource: ldap.Ldapsource{ Host: form.Host, @@ -131,7 +136,7 @@ func EditAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { Name: form.AuthName, }, } - case models.LT_SMTP: + case models.SMTP: config = &models.SMTPConfig{ Auth: form.SmtpAuth, Host: form.SmtpHost, @@ -147,13 +152,13 @@ func EditAuthSourcePost(ctx *middleware.Context, form auth.AuthenticationForm) { Id: form.Id, Name: form.AuthName, IsActived: form.IsActived, - Type: form.Type, + Type: models.LoginType(form.Type), AllowAutoRegister: form.AllowAutoRegister, Cfg: config, } if err := models.UpdateSource(&u); err != nil { - ctx.Handle(500, "admin.auths.EditAuth", err) + ctx.Handle(500, "admin.auths.EditAuth(UpdateSource)", err) return } @@ -175,7 +180,7 @@ func DeleteAuthSource(ctx *middleware.Context, params martini.Params) { a, err := models.GetLoginSourceById(id) if err != nil { - ctx.Handle(500, "admin.auths.DeleteAuth", err) + ctx.Handle(500, "admin.auths.DeleteAuth(GetLoginSourceById)", err) return } @@ -185,7 +190,7 @@ func DeleteAuthSource(ctx *middleware.Context, params martini.Params) { ctx.Flash.Error("This authentication still has used by some users, you should move them and then delete again.") ctx.Redirect("/admin/auths/" + params["authid"]) default: - ctx.Handle(500, "admin.auths.DeleteAuth", err) + ctx.Handle(500, "admin.auths.DeleteAuth(DelLoginSource)", err) } return } diff --git a/routers/admin/user.go b/routers/admin/user.go index fa98bd32ea..cf99db2bf7 100644 --- a/routers/admin/user.go +++ b/routers/admin/user.go @@ -5,8 +5,6 @@ package admin import ( - "fmt" - "strconv" "strings" "github.com/go-martini/martini" @@ -18,16 +16,21 @@ import ( "github.com/gogits/gogs/modules/middleware" ) +const ( + USER_NEW base.TplName = "admin/user/new" + USER_EDIT base.TplName = "admin/user/edit" +) + func NewUser(ctx *middleware.Context) { ctx.Data["Title"] = "New Account" ctx.Data["PageIsUsers"] = true auths, err := models.GetAuths() if err != nil { - ctx.Handle(500, "admin.user.NewUser", err) + ctx.Handle(500, "admin.user.NewUser(GetAuths)", err) return } ctx.Data["LoginSources"] = auths - ctx.HTML(200, "admin/users/new") + ctx.HTML(200, USER_NEW) } func NewUserPost(ctx *middleware.Context, form auth.RegisterForm) { @@ -35,7 +38,7 @@ func NewUserPost(ctx *middleware.Context, form auth.RegisterForm) { ctx.Data["PageIsUsers"] = true if ctx.HasError() { - ctx.HTML(200, "admin/users/new") + ctx.HTML(200, USER_NEW) return } @@ -51,28 +54,29 @@ func NewUserPost(ctx *middleware.Context, form auth.RegisterForm) { Email: form.Email, Passwd: form.Password, IsActive: true, - LoginType: models.LT_PLAIN, + LoginType: models.PLAIN, } if len(form.LoginType) > 0 { + // NOTE: need rewrite. fields := strings.Split(form.LoginType, "-") - u.LoginType, _ = strconv.Atoi(fields[0]) - u.LoginSource, _ = strconv.ParseInt(fields[1], 10, 64) + tp, _ := base.StrTo(fields[0]).Int() + u.LoginType = models.LoginType(tp) + u.LoginSource, _ = base.StrTo(fields[1]).Int64() u.LoginName = form.LoginName - fmt.Println(u.LoginType, u.LoginSource, u.LoginName) } var err error - if u, err = models.RegisterUser(u); err != nil { + if u, err = models.CreateUser(u); err != nil { switch err { case models.ErrUserAlreadyExist: - ctx.RenderWithErr("Username has been already taken", "admin/users/new", &form) + ctx.RenderWithErr("Username has been already taken", USER_NEW, &form) case models.ErrEmailAlreadyUsed: - ctx.RenderWithErr("E-mail address has been already used", "admin/users/new", &form) + ctx.RenderWithErr("E-mail address has been already used", USER_NEW, &form) case models.ErrUserNameIllegal: - ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "admin/users/new", &form) + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), USER_NEW, &form) default: - ctx.Handle(500, "admin.user.NewUser", err) + ctx.Handle(500, "admin.user.NewUser(CreateUser)", err) } return } @@ -95,18 +99,18 @@ func EditUser(ctx *middleware.Context, params martini.Params) { u, err := models.GetUserById(int64(uid)) if err != nil { - ctx.Handle(500, "admin.user.EditUser", err) + ctx.Handle(500, "admin.user.EditUser(GetUserById)", err) return } ctx.Data["User"] = u auths, err := models.GetAuths() if err != nil { - ctx.Handle(500, "admin.user.NewUser", err) + ctx.Handle(500, "admin.user.NewUser(GetAuths)", err) return } ctx.Data["LoginSources"] = auths - ctx.HTML(200, "admin/users/edit") + ctx.HTML(200, USER_EDIT) } func EditUserPost(ctx *middleware.Context, params martini.Params, form auth.AdminEditUserForm) { @@ -115,13 +119,18 @@ func EditUserPost(ctx *middleware.Context, params martini.Params, form auth.Admi uid, err := base.StrTo(params["userid"]).Int() if err != nil { - ctx.Handle(404, "admin.user.EditUser", err) + ctx.Handle(404, "admin.user.EditUserPost", err) return } u, err := models.GetUserById(int64(uid)) if err != nil { - ctx.Handle(500, "admin.user.EditUser", err) + ctx.Handle(500, "admin.user.EditUserPost(GetUserById)", err) + return + } + + if ctx.HasError() { + ctx.HTML(200, USER_EDIT) return } @@ -133,7 +142,7 @@ func EditUserPost(ctx *middleware.Context, params martini.Params, form auth.Admi u.IsActive = form.Active u.IsAdmin = form.Admin if err := models.UpdateUser(u); err != nil { - ctx.Handle(500, "admin.user.EditUser", err) + ctx.Handle(500, "admin.user.EditUserPost(UpdateUser)", err) return } log.Trace("%s User profile updated by admin(%s): %s", ctx.Req.RequestURI, @@ -151,13 +160,13 @@ func DeleteUser(ctx *middleware.Context, params martini.Params) { //log.Info("delete") uid, err := base.StrTo(params["userid"]).Int() if err != nil { - ctx.Handle(404, "admin.user.EditUser", err) + ctx.Handle(404, "admin.user.DeleteUser", err) return } u, err := models.GetUserById(int64(uid)) if err != nil { - ctx.Handle(500, "admin.user.EditUser", err) + ctx.Handle(500, "admin.user.DeleteUser(GetUserById)", err) return } diff --git a/routers/dashboard.go b/routers/dashboard.go index 438d03794b..4ef4e54f49 100644 --- a/routers/dashboard.go +++ b/routers/dashboard.go @@ -6,11 +6,16 @@ package routers import ( "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/routers/user" ) +const ( + HOME base.TplName = "home" +) + func Home(ctx *middleware.Context) { if ctx.IsSigned { user.Dashboard(ctx) @@ -26,7 +31,7 @@ func Home(ctx *middleware.Context) { ctx.Data["PageIsHome"] = true - // Show recent updated repositoires for new visiters. + // Show recent updated repositories for new visitors. repos, err := models.GetRecentUpdatedRepositories() if err != nil { ctx.Handle(500, "dashboard.Home(GetRecentUpdatedRepositories)", err) @@ -40,7 +45,7 @@ func Home(ctx *middleware.Context) { } } ctx.Data["Repos"] = repos - ctx.HTML(200, "home") + ctx.HTML(200, HOME) } func NotFound(ctx *middleware.Context) { diff --git a/routers/dev/template.go b/routers/dev/template.go index 5e84d76edb..da477c94c0 100644 --- a/routers/dev/template.go +++ b/routers/dev/template.go @@ -8,6 +8,7 @@ import ( "github.com/go-martini/martini" "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/setting" ) @@ -22,5 +23,5 @@ func TemplatePreview(ctx *middleware.Context, params martini.Params) { ctx.Data["ActiveCodeLives"] = setting.Service.ActiveCodeLives / 60 ctx.Data["ResetPwdCodeLives"] = setting.Service.ResetPwdCodeLives / 60 ctx.Data["CurDbValue"] = "" - ctx.HTML(200, params["_1"]) + ctx.HTML(200, base.TplName(params["_1"])) } diff --git a/routers/install.go b/routers/install.go index f44391a46c..bb3c16eae4 100644 --- a/routers/install.go +++ b/routers/install.go @@ -14,7 +14,6 @@ import ( "github.com/Unknwon/goconfig" "github.com/go-martini/martini" "github.com/go-xorm/xorm" - qlog "github.com/qiniu/log" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" @@ -27,6 +26,10 @@ import ( "github.com/gogits/gogs/modules/social" ) +const ( + INSTALL base.TplName = "install" +) + func checkRunMode() { switch setting.Cfg.MustValue("", "RUN_MODE") { case "prod": @@ -56,11 +59,12 @@ func GlobalInit() { if setting.InstallLock { if err := models.NewEngine(); err != nil { - qlog.Fatal(err) + log.Fatal("Fail to initialize ORM engine: %v", err) } models.HasEngine = true cron.NewCronContext() + log.NewGitLogger(path.Join(setting.LogRootPath, "http.log")) } if models.EnableSQLite3 { log.Info("SQLite3 Enabled") @@ -72,6 +76,7 @@ func renderDbOption(ctx *middleware.Context) { ctx.Data["DbOptions"] = []string{"MySQL", "PostgreSQL", "SQLite3"} } +// @router /install [get] func Install(ctx *middleware.Context, form auth.InstallForm) { if setting.InstallLock { ctx.Handle(404, "install.Install", errors.New("Installation is prohibited")) @@ -119,12 +124,12 @@ func Install(ctx *middleware.Context, form auth.InstallForm) { ctx.Data["CurDbOption"] = curDbOp auth.AssignForm(form, ctx.Data) - ctx.HTML(200, "install") + ctx.HTML(200, INSTALL) } func InstallPost(ctx *middleware.Context, form auth.InstallForm) { if setting.InstallLock { - ctx.Handle(404, "install.Install", errors.New("Installation is prohibited")) + ctx.Handle(404, "install.InstallPost", errors.New("Installation is prohibited")) return } @@ -135,12 +140,12 @@ func InstallPost(ctx *middleware.Context, form auth.InstallForm) { ctx.Data["CurDbOption"] = form.Database if ctx.HasError() { - ctx.HTML(200, "install") + ctx.HTML(200, INSTALL) return } if _, err := exec.LookPath("git"); err != nil { - ctx.RenderWithErr("Fail to test 'git' command: "+err.Error(), "install", &form) + ctx.RenderWithErr("Fail to test 'git' command: "+err.Error(), INSTALL, &form) return } @@ -158,18 +163,19 @@ func InstallPost(ctx *middleware.Context, form auth.InstallForm) { // Set test engine. var x *xorm.Engine if err := models.NewTestEngine(x); err != nil { + // NOTE: should use core.QueryDriver (github.com/go-xorm/core) if strings.Contains(err.Error(), `Unknown database type: sqlite3`) { ctx.RenderWithErr("Your release version does not support SQLite3, please download the official binary version "+ - "from http://gogs.io/docs/installation/install_from_binary.md, NOT the gobuild version.", "install", &form) + "from http://gogs.io/docs/installation/install_from_binary.md, NOT the gobuild version.", INSTALL, &form) } else { - ctx.RenderWithErr("Database setting is not correct: "+err.Error(), "install", &form) + 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) + ctx.RenderWithErr("Repository root path is invalid: "+err.Error(), INSTALL, &form) return } @@ -180,7 +186,7 @@ func InstallPost(ctx *middleware.Context, form auth.InstallForm) { } // Does not check run user when the install lock is off. if form.RunUser != curUser { - ctx.RenderWithErr("Run user isn't the current user: "+form.RunUser+" -> "+curUser, "install", &form) + ctx.RenderWithErr("Run user isn't the current user: "+form.RunUser+" -> "+curUser, INSTALL, &form) return } @@ -214,18 +220,18 @@ func InstallPost(ctx *middleware.Context, form auth.InstallForm) { os.MkdirAll("custom/conf", os.ModePerm) if err := goconfig.SaveConfigFile(setting.Cfg, path.Join(setting.CustomPath, "conf/app.ini")); err != nil { - ctx.RenderWithErr("Fail to save configuration: "+err.Error(), "install", &form) + ctx.RenderWithErr("Fail to save configuration: "+err.Error(), INSTALL, &form) return } GlobalInit() // Create admin account. - if _, err := models.RegisterUser(&models.User{Name: form.AdminName, Email: form.AdminEmail, Passwd: form.AdminPasswd, + if _, err := models.CreateUser(&models.User{Name: form.AdminName, Email: form.AdminEmail, Passwd: form.AdminPasswd, IsAdmin: true, IsActive: true}); err != nil { if err != models.ErrUserAlreadyExist { setting.InstallLock = false - ctx.RenderWithErr("Admin account setting is invalid: "+err.Error(), "install", &form) + ctx.RenderWithErr("Admin account setting is invalid: "+err.Error(), INSTALL, &form) return } log.Info("Admin account already exist") diff --git a/routers/org/org.go b/routers/org/org.go new file mode 100644 index 0000000000..7b2c4d7320 --- /dev/null +++ b/routers/org/org.go @@ -0,0 +1,205 @@ +// 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 org + +import ( + "github.com/go-martini/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/middleware" + "github.com/gogits/gogs/routers/user" +) + +const ( + NEW base.TplName = "org/new" + SETTINGS base.TplName = "org/settings" +) + +func Organization(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Organization " + params["org"] + ctx.HTML(200, "org/org") +} + +func Members(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Organization " + params["org"] + " Members" + ctx.HTML(200, "org/members") +} + +func New(ctx *middleware.Context) { + ctx.Data["Title"] = "Create An Organization" + ctx.HTML(200, NEW) +} + +func NewPost(ctx *middleware.Context, form auth.CreateOrgForm) { + ctx.Data["Title"] = "Create An Organization" + + if ctx.HasError() { + ctx.HTML(200, NEW) + return + } + + org := &models.User{ + Name: form.OrgName, + Email: form.Email, + IsActive: true, // NOTE: may need to set false when require e-mail confirmation. + Type: models.ORGANIZATION, + } + + var err error + if org, err = models.CreateOrganization(org, ctx.User); err != nil { + switch err { + case models.ErrUserAlreadyExist: + ctx.Data["Err_OrgName"] = true + ctx.RenderWithErr("Organization name has been already taken", NEW, &form) + case models.ErrEmailAlreadyUsed: + ctx.Data["Err_Email"] = true + ctx.RenderWithErr("E-mail address has been already used", NEW, &form) + case models.ErrUserNameIllegal: + ctx.Data["Err_OrgName"] = true + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), NEW, &form) + default: + ctx.Handle(500, "user.NewPost(CreateUser)", err) + } + return + } + log.Trace("%s Organization created: %s", ctx.Req.RequestURI, org.Name) + + ctx.Redirect("/org/" + form.OrgName + "/dashboard") +} + +func Dashboard(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Dashboard" + ctx.Data["PageIsUserDashboard"] = true + ctx.Data["PageIsOrgDashboard"] = true + + org, err := models.GetUserByName(params["org"]) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "org.Dashboard(GetUserByName)", err) + } else { + ctx.Handle(500, "org.Dashboard(GetUserByName)", err) + } + return + } + + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs + ctx.Data["ContextUser"] = org + + ctx.Data["MyRepos"], err = models.GetRepositories(org.Id, true) + if err != nil { + ctx.Handle(500, "org.Dashboard(GetRepositories)", err) + return + } + + actions, err := models.GetFeeds(org.Id, 0, false) + if err != nil { + ctx.Handle(500, "org.Dashboard(GetFeeds)", err) + return + } + ctx.Data["Feeds"] = actions + + ctx.HTML(200, user.DASHBOARD) +} + +func Settings(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Settings" + + org, err := models.GetUserByName(params["org"]) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "org.Settings(GetUserByName)", err) + } else { + ctx.Handle(500, "org.Settings(GetUserByName)", err) + } + return + } + ctx.Data["Org"] = org + + ctx.HTML(200, SETTINGS) +} + +func SettingsPost(ctx *middleware.Context, params martini.Params, form auth.OrgSettingForm) { + ctx.Data["Title"] = "Settings" + + org, err := models.GetUserByName(params["org"]) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "org.SettingsPost(GetUserByName)", err) + } else { + ctx.Handle(500, "org.SettingsPost(GetUserByName)", err) + } + return + } + ctx.Data["Org"] = org + + if ctx.HasError() { + ctx.HTML(200, SETTINGS) + return + } + + org.FullName = form.DisplayName + org.Email = form.Email + org.Description = form.Description + org.Website = form.Website + org.Location = form.Location + if err = models.UpdateUser(org); err != nil { + ctx.Handle(500, "org.SettingsPost(UpdateUser)", err) + return + } + log.Trace("%s Organization setting updated: %s", ctx.Req.RequestURI, org.LowerName) + ctx.Flash.Success("Organization profile has been successfully updated.") + ctx.Redirect("/org/" + org.Name + "/settings") +} + +func DeletePost(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Settings" + + org, err := models.GetUserByName(params["org"]) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "org.DeletePost(GetUserByName)", err) + } else { + ctx.Handle(500, "org.DeletePost(GetUserByName)", err) + } + return + } + ctx.Data["Org"] = org + + if !models.IsOrganizationOwner(org.Id, ctx.User.Id) { + ctx.Error(403) + return + } + + tmpUser := models.User{ + Passwd: ctx.Query("password"), + Salt: ctx.User.Salt, + } + tmpUser.EncodePasswd() + if tmpUser.Passwd != ctx.User.Passwd { + ctx.Flash.Error("Password is not correct. Make sure you are owner of this account.") + } else { + if err := models.DeleteOrganization(org); err != nil { + switch err { + case models.ErrUserOwnRepos: + ctx.Flash.Error("This organization still have ownership of repository, you have to delete or transfer them first.") + default: + ctx.Handle(500, "org.DeletePost(DeleteOrganization)", err) + return + } + } else { + ctx.Redirect("/") + return + } + } + + ctx.Redirect("/org/" + org.Name + "/settings") +} diff --git a/routers/org/teams.go b/routers/org/teams.go new file mode 100644 index 0000000000..9ca5185a94 --- /dev/null +++ b/routers/org/teams.go @@ -0,0 +1,21 @@ +package org + +import ( + "github.com/go-martini/martini" + "github.com/gogits/gogs/modules/middleware" +) + +func Teams(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Organization "+params["org"]+" Teams" + ctx.HTML(200, "org/teams") +} + +func NewTeam(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Organization "+params["org"]+" New Team" + ctx.HTML(200, "org/new_team") +} + +func EditTeam(ctx *middleware.Context, params martini.Params){ + ctx.Data["Title"] = "Organization "+params["org"]+" Edit Team" + ctx.HTML(200,"org/edit_team") +} diff --git a/routers/repo/branch.go b/routers/repo/branch.go index 2e2ae69254..9bad7289b9 100644 --- a/routers/repo/branch.go +++ b/routers/repo/branch.go @@ -7,22 +7,27 @@ package repo import ( "github.com/go-martini/martini" + "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/middleware" ) +const ( + BRANCH base.TplName = "repo/branch" +) + func Branches(ctx *middleware.Context, params martini.Params) { ctx.Data["Title"] = "Branches" ctx.Data["IsRepoToolbarBranches"] = true brs, err := ctx.Repo.GitRepo.GetBranches() if err != nil { - ctx.Handle(500, "repo.Branches", err) + ctx.Handle(500, "repo.Branches(GetBranches)", err) return } else if len(brs) == 0 { - ctx.Handle(404, "repo.Branches", nil) + ctx.Handle(404, "repo.Branches(GetBranches)", nil) return } ctx.Data["Branches"] = brs - ctx.HTML(200, "repo/branches") + ctx.HTML(200, BRANCH) } diff --git a/routers/repo/commit.go b/routers/repo/commit.go index 09dcaf5ead..aa5c22e417 100644 --- a/routers/repo/commit.go +++ b/routers/repo/commit.go @@ -14,6 +14,11 @@ import ( "github.com/gogits/gogs/modules/middleware" ) +const ( + COMMITS base.TplName = "repo/commits" + DIFF base.TplName = "repo/diff" +) + func Commits(ctx *middleware.Context, params martini.Params) { ctx.Data["IsRepoToolbarCommits"] = true @@ -22,10 +27,10 @@ func Commits(ctx *middleware.Context, params martini.Params) { brs, err := ctx.Repo.GitRepo.GetBranches() if err != nil { - ctx.Handle(500, "repo.Commits", err) + ctx.Handle(500, "repo.Commits(GetBranches)", err) return } else if len(brs) == 0 { - ctx.Handle(404, "repo.Commits", nil) + ctx.Handle(404, "repo.Commits(GetBranches)", nil) return } @@ -61,7 +66,43 @@ func Commits(ctx *middleware.Context, params martini.Params) { ctx.Data["CommitCount"] = commitsCount ctx.Data["LastPageNum"] = lastPage ctx.Data["NextPageNum"] = nextPage - ctx.HTML(200, "repo/commits") + ctx.HTML(200, COMMITS) +} + +func SearchCommits(ctx *middleware.Context, params martini.Params) { + ctx.Data["IsSearchPage"] = true + ctx.Data["IsRepoToolbarCommits"] = true + + keyword := ctx.Query("q") + if len(keyword) == 0 { + ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchName) + return + } + + userName := params["username"] + repoName := params["reponame"] + + brs, err := ctx.Repo.GitRepo.GetBranches() + if err != nil { + ctx.Handle(500, "repo.SearchCommits(GetBranches)", err) + return + } else if len(brs) == 0 { + ctx.Handle(404, "repo.SearchCommits(GetBranches)", nil) + return + } + + commits, err := ctx.Repo.Commit.SearchCommits(keyword) + if err != nil { + ctx.Handle(500, "repo.SearchCommits(SearchCommits)", err) + return + } + + ctx.Data["Keyword"] = keyword + ctx.Data["Username"] = userName + ctx.Data["Reponame"] = repoName + ctx.Data["CommitCount"] = commits.Len() + ctx.Data["Commits"] = commits + ctx.HTML(200, COMMITS) } func Diff(ctx *middleware.Context, params martini.Params) { @@ -75,7 +116,7 @@ func Diff(ctx *middleware.Context, params martini.Params) { diff, err := models.GetDiff(models.RepoPath(userName, repoName), commitId) if err != nil { - ctx.Handle(404, "repo.Diff", err) + ctx.Handle(404, "repo.Diff(GetDiff)", err) return } @@ -119,43 +160,7 @@ func Diff(ctx *middleware.Context, params martini.Params) { ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0 ctx.Data["SourcePath"] = "/" + path.Join(userName, repoName, "src", commitId) ctx.Data["RawPath"] = "/" + path.Join(userName, repoName, "raw", commitId) - ctx.HTML(200, "repo/diff") -} - -func SearchCommits(ctx *middleware.Context, params martini.Params) { - ctx.Data["IsSearchPage"] = true - ctx.Data["IsRepoToolbarCommits"] = true - - keyword := ctx.Query("q") - if len(keyword) == 0 { - ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchName) - return - } - - userName := params["username"] - repoName := params["reponame"] - - brs, err := ctx.Repo.GitRepo.GetBranches() - if err != nil { - ctx.Handle(500, "repo.SearchCommits(GetBranches)", err) - return - } else if len(brs) == 0 { - ctx.Handle(404, "repo.SearchCommits(GetBranches)", nil) - return - } - - commits, err := ctx.Repo.Commit.SearchCommits(keyword) - if err != nil { - ctx.Handle(500, "repo.SearchCommits(SearchCommits)", err) - return - } - - ctx.Data["Keyword"] = keyword - ctx.Data["Username"] = userName - ctx.Data["Reponame"] = repoName - ctx.Data["CommitCount"] = commits.Len() - ctx.Data["Commits"] = commits - ctx.HTML(200, "repo/commits") + ctx.HTML(200, DIFF) } func FileHistory(ctx *middleware.Context, params martini.Params) { @@ -184,8 +189,7 @@ func FileHistory(ctx *middleware.Context, params martini.Params) { if err != nil { ctx.Handle(500, "repo.FileHistory(GetCommitsCount)", err) return - } - if commitsCount == 0 { + } else if commitsCount == 0 { ctx.Handle(404, "repo.FileHistory", nil) return } @@ -217,5 +221,5 @@ func FileHistory(ctx *middleware.Context, params martini.Params) { ctx.Data["CommitCount"] = commitsCount ctx.Data["LastPageNum"] = lastPage ctx.Data["NextPageNum"] = nextPage - ctx.HTML(200, "repo/commits") + ctx.HTML(200, COMMITS) } diff --git a/routers/repo/http.go b/routers/repo/http.go index c5856d603c..7b89e9b05a 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -7,9 +7,7 @@ package repo import ( "bytes" "fmt" - "io" "io/ioutil" - "log" "net/http" "os" "os/exec" @@ -22,6 +20,7 @@ import ( "github.com/go-martini/martini" "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/setting" ) @@ -107,9 +106,9 @@ func Http(ctx *middleware.Context, params martini.Params) { } if !isPublicPull { - var tp = models.AU_WRITABLE + var tp = models.WRITABLE if isPull { - tp = models.AU_READABLE + tp = models.READABLE } has, err := models.HasAccess(authUsername, username+"/"+reponame, tp) @@ -117,8 +116,8 @@ func Http(ctx *middleware.Context, params martini.Params) { ctx.Handle(401, "no basic auth and digit auth", nil) return } else if !has { - if tp == models.AU_READABLE { - has, err = models.HasAccess(authUsername, username+"/"+reponame, models.AU_WRITABLE) + if tp == models.READABLE { + has, err = models.HasAccess(authUsername, username+"/"+reponame, models.WRITABLE) if err != nil || !has { ctx.Handle(401, "no basic auth and digit auth", nil) return @@ -141,7 +140,10 @@ func Http(ctx *middleware.Context, params martini.Params) { newCommitId := fields[1] refName := fields[2] - models.Update(refName, oldCommitId, newCommitId, authUsername, username, reponame, authUser.Id) + if err = models.Update(refName, oldCommitId, newCommitId, authUsername, username, reponame, authUser.Id); err != nil { + log.GitLogger.Error(err.Error()) + return + } } } } @@ -190,7 +192,6 @@ var routes = []route{ // Request handling function func HttpBackend(config *Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - //log.Printf("%s %s %s %s", r.RemoteAddr, r.Method, r.URL.Path, r.Proto) for _, route := range routes { if m := route.cr.FindStringSubmatch(r.URL.Path); m != nil { if route.method != r.Method { @@ -202,7 +203,7 @@ func HttpBackend(config *Config) http.HandlerFunc { dir, err := getGitDir(config, m[1]) if err != nil { - log.Print(err) + log.GitLogger.Error(err.Error()) renderNotFound(w) return } @@ -212,13 +213,13 @@ func HttpBackend(config *Config) http.HandlerFunc { return } } + renderNotFound(w) return } } // Actual command handling functions - func serviceUploadPack(hr handler) { serviceRpc("upload-pack", hr) } @@ -236,36 +237,24 @@ func serviceRpc(rpc string, hr handler) { return } - input, _ := ioutil.ReadAll(r.Body) - w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", rpc)) w.WriteHeader(http.StatusOK) + input, _ := ioutil.ReadAll(r.Body) + br := bytes.NewReader(input) + args := []string{rpc, "--stateless-rpc", dir} cmd := exec.Command(hr.Config.GitBinPath, args...) cmd.Dir = dir - in, err := cmd.StdinPipe() + cmd.Stdout = w + cmd.Stdin = br + + err := cmd.Run() if err != nil { - log.Print(err) + log.GitLogger.Error(err.Error()) return } - stdout, err := cmd.StdoutPipe() - if err != nil { - log.Print(err) - return - } - - err = cmd.Start() - if err != nil { - log.Print(err) - return - } - - in.Write(input) - io.Copy(w, stdout) - cmd.Wait() - if hr.Config.OnSucceed != nil { hr.Config.OnSucceed(rpc, input) } @@ -345,7 +334,7 @@ func getGitDir(config *Config, fPath string) (string, error) { cwd, err := os.Getwd() if err != nil { - log.Print(err) + log.GitLogger.Error(err.Error()) return "", err } @@ -422,7 +411,7 @@ func gitCommand(gitBinPath, dir string, args ...string) []byte { out, err := command.Output() if err != nil { - log.Print(err) + log.GitLogger.Error(err.Error()) } return out diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 808fb52b41..cf454bc8fb 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -22,6 +22,16 @@ import ( "github.com/gogits/gogs/modules/setting" ) +const ( + ISSUES base.TplName = "repo/issue/list" + ISSUE_CREATE base.TplName = "repo/issue/create" + ISSUE_VIEW base.TplName = "repo/issue/view" + + MILESTONE base.TplName = "repo/issue/milestone" + MILESTONE_NEW base.TplName = "repo/issue/milestone_new" + MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit" +) + func Issues(ctx *middleware.Context) { ctx.Data["Title"] = "Issues" ctx.Data["IsRepoToolbarIssues"] = true @@ -134,7 +144,7 @@ func Issues(ctx *middleware.Context) { } else { ctx.Data["ShowCount"] = issueStats.OpenCount } - ctx.HTML(200, "issue/list") + ctx.HTML(200, ISSUES) } func CreateIssue(ctx *middleware.Context, params martini.Params) { @@ -161,7 +171,7 @@ func CreateIssue(ctx *middleware.Context, params martini.Params) { return } ctx.Data["Collaborators"] = us - ctx.HTML(200, "issue/create") + ctx.HTML(200, ISSUE_CREATE) } func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { @@ -190,7 +200,7 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C ctx.Data["Collaborators"] = us if ctx.HasError() { - ctx.HTML(200, "issue/create") + ctx.HTML(200, ISSUE_CREATE) return } @@ -250,7 +260,7 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C } // Mail watchers and mentions. - if setting.Service.NotifyMail { + if setting.Service.EnableNotifyMail { tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue) if err != nil { ctx.Handle(500, "issue.CreateIssue(SendIssueNotifyMail)", err) @@ -392,7 +402,7 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { ctx.Data["IsIssueOwner"] = ctx.Repo.IsOwner || (ctx.IsSigned && issue.PosterId == ctx.User.Id) ctx.Data["IsRepoToolbarIssues"] = true ctx.Data["IsRepoToolbarIssuesList"] = false - ctx.HTML(200, "issue/view") + ctx.HTML(200, ISSUE_VIEW) } func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { @@ -685,7 +695,7 @@ func Comment(ctx *middleware.Context, params martini.Params) { } // Mail watchers and mentions. - if setting.Service.NotifyMail { + if setting.Service.EnableNotifyMail { issue.Content = content tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue) if err != nil { @@ -794,14 +804,14 @@ func Milestones(ctx *middleware.Context) { } else { ctx.Data["State"] = "open" } - ctx.HTML(200, "issue/milestone") + ctx.HTML(200, MILESTONE) } func NewMilestone(ctx *middleware.Context) { ctx.Data["Title"] = "New Milestone" ctx.Data["IsRepoToolbarIssues"] = true ctx.Data["IsRepoToolbarIssuesList"] = true - ctx.HTML(200, "issue/milestone_new") + ctx.HTML(200, MILESTONE_NEW) } func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) { @@ -809,6 +819,11 @@ func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) { ctx.Data["IsRepoToolbarIssues"] = true ctx.Data["IsRepoToolbarIssuesList"] = true + if ctx.HasError() { + ctx.HTML(200, MILESTONE_NEW) + return + } + var deadline time.Time var err error if len(form.Deadline) == 0 { @@ -890,7 +905,7 @@ func UpdateMilestone(ctx *middleware.Context, params martini.Params) { } ctx.Data["Milestone"] = mile - ctx.HTML(200, "issue/milestone_edit") + ctx.HTML(200, MILESTONE_EDIT) } func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form auth.CreateMilestoneForm) { @@ -914,6 +929,11 @@ func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form au return } + if ctx.HasError() { + ctx.HTML(200, MILESTONE_EDIT) + return + } + var deadline time.Time if len(form.Deadline) == 0 { form.Deadline = "12/31/9999" diff --git a/routers/repo/pull.go b/routers/repo/pull.go index 430c6a815f..db208f9fbc 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -7,10 +7,15 @@ package repo import ( "github.com/go-martini/martini" + "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/middleware" ) +const ( + PULLS base.TplName = "repo/pulls" +) + func Pulls(ctx *middleware.Context, params martini.Params) { ctx.Data["IsRepoToolbarPulls"] = true - ctx.HTML(200, "repo/pulls") + ctx.HTML(200, PULLS) } diff --git a/routers/repo/release.go b/routers/repo/release.go index 14a14656d4..a901436fc0 100644 --- a/routers/repo/release.go +++ b/routers/repo/release.go @@ -5,7 +5,7 @@ package repo import ( - "sort" + "github.com/go-martini/martini" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" @@ -14,21 +14,11 @@ import ( "github.com/gogits/gogs/modules/middleware" ) -type ReleaseSorter struct { - rels []*models.Release -} - -func (rs *ReleaseSorter) Len() int { - return len(rs.rels) -} - -func (rs *ReleaseSorter) Less(i, j int) bool { - return rs.rels[i].NumCommits > rs.rels[j].NumCommits -} - -func (rs *ReleaseSorter) Swap(i, j int) { - rs.rels[i], rs.rels[j] = rs.rels[j], rs.rels[i] -} +const ( + RELEASES base.TplName = "repo/release/list" + RELEASE_NEW base.TplName = "repo/release/new" + RELEASE_EDIT base.TplName = "repo/release/edit" +) func Releases(ctx *middleware.Context) { ctx.Data["Title"] = "Releases" @@ -52,65 +42,88 @@ func Releases(ctx *middleware.Context) { return } - var tags ReleaseSorter - tags.rels = make([]*models.Release, len(rawTags)) + // Temproray cache commits count of used branches to speed up. + countCache := make(map[string]int) + + tags := make([]*models.Release, len(rawTags)) for i, rawTag := range rawTags { for _, rel := range rels { + if rel.IsDraft && !ctx.Repo.IsOwner { + continue + } if rel.TagName == rawTag { rel.Publisher, err = models.GetUserById(rel.PublisherId) if err != nil { ctx.Handle(500, "release.Releases(GetUserById)", err) return } - rel.NumCommitsBehind = commitsCount - rel.NumCommits + // Get corresponding target if it's not the current branch. + if ctx.Repo.BranchName != rel.Target { + // Get count if not exists. + if _, ok := countCache[rel.Target]; !ok { + commit, err := ctx.Repo.GitRepo.GetCommitOfTag(rel.TagName) + if err != nil { + ctx.Handle(500, "release.Releases(GetCommitOfTag)", err) + return + } + countCache[rel.Target], err = commit.CommitsCount() + if err != nil { + ctx.Handle(500, "release.Releases(CommitsCount2)", err) + return + } + } + rel.NumCommitsBehind = countCache[rel.Target] - rel.NumCommits + } else { + rel.NumCommitsBehind = commitsCount - rel.NumCommits + } + rel.Note = base.RenderMarkdownString(rel.Note, ctx.Repo.RepoLink) - tags.rels[i] = rel + tags[i] = rel break } } - if tags.rels[i] == nil { + if tags[i] == nil { commit, err := ctx.Repo.GitRepo.GetCommitOfTag(rawTag) if err != nil { - ctx.Handle(500, "release.Releases(GetCommitOfTag)", err) + ctx.Handle(500, "release.Releases(GetCommitOfTag2)", err) return } - tags.rels[i] = &models.Release{ + tags[i] = &models.Release{ Title: rawTag, TagName: rawTag, - SHA1: commit.Id.String(), + Sha1: commit.Id.String(), } - tags.rels[i].NumCommits, err = ctx.Repo.GitRepo.CommitsCount(commit.Id.String()) + + tags[i].NumCommits, err = ctx.Repo.GitRepo.CommitsCount(commit.Id.String()) if err != nil { ctx.Handle(500, "release.Releases(CommitsCount)", err) return } - tags.rels[i].NumCommitsBehind = commitsCount - tags.rels[i].NumCommits + tags[i].NumCommitsBehind = commitsCount - tags[i].NumCommits } } - - sort.Sort(&tags) - - ctx.Data["Releases"] = tags.rels - ctx.HTML(200, "release/list") + models.SortReleases(tags) + ctx.Data["Releases"] = tags + ctx.HTML(200, RELEASES) } -func ReleasesNew(ctx *middleware.Context) { +func NewRelease(ctx *middleware.Context) { if !ctx.Repo.IsOwner { - ctx.Handle(404, "release.ReleasesNew", nil) + ctx.Handle(403, "release.ReleasesNew", nil) return } ctx.Data["Title"] = "New Release" ctx.Data["IsRepoToolbarReleases"] = true ctx.Data["IsRepoReleaseNew"] = true - ctx.HTML(200, "release/new") + ctx.HTML(200, RELEASE_NEW) } -func ReleasesNewPost(ctx *middleware.Context, form auth.NewReleaseForm) { +func NewReleasePost(ctx *middleware.Context, form auth.NewReleaseForm) { if !ctx.Repo.IsOwner { - ctx.Handle(404, "release.ReleasesNew", nil) + ctx.Handle(403, "release.ReleasesNew", nil) return } @@ -119,7 +132,7 @@ func ReleasesNewPost(ctx *middleware.Context, form auth.NewReleaseForm) { ctx.Data["IsRepoReleaseNew"] = true if ctx.HasError() { - ctx.HTML(200, "release/new") + ctx.HTML(200, RELEASE_NEW) return } @@ -129,14 +142,21 @@ func ReleasesNewPost(ctx *middleware.Context, form auth.NewReleaseForm) { return } + if !ctx.Repo.GitRepo.IsBranchExist(form.Target) { + ctx.RenderWithErr("Target branch does not exist", "release/new", &form) + return + } + rel := &models.Release{ RepoId: ctx.Repo.Repository.Id, PublisherId: ctx.User.Id, Title: form.Title, TagName: form.TagName, - SHA1: ctx.Repo.Commit.Id.String(), + Target: form.Target, + Sha1: ctx.Repo.Commit.Id.String(), NumCommits: commitsCount, Note: form.Content, + IsDraft: len(form.Draft) > 0, IsPrerelease: form.Prerelease, } @@ -152,3 +172,63 @@ func ReleasesNewPost(ctx *middleware.Context, form auth.NewReleaseForm) { ctx.Redirect(ctx.Repo.RepoLink + "/releases") } + +func EditRelease(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsOwner { + ctx.Handle(403, "release.ReleasesEdit", nil) + return + } + + tagName := params["tagname"] + rel, err := models.GetRelease(ctx.Repo.Repository.Id, tagName) + if err != nil { + if err == models.ErrReleaseNotExist { + ctx.Handle(404, "release.ReleasesEdit(GetRelease)", err) + } else { + ctx.Handle(500, "release.ReleasesEdit(GetRelease)", err) + } + return + } + ctx.Data["Release"] = rel + + ctx.Data["Title"] = "Edit Release" + ctx.Data["IsRepoToolbarReleases"] = true + ctx.HTML(200, RELEASE_EDIT) +} + +func EditReleasePost(ctx *middleware.Context, params martini.Params, form auth.EditReleaseForm) { + if !ctx.Repo.IsOwner { + ctx.Handle(403, "release.EditReleasePost", nil) + return + } + + tagName := params["tagname"] + rel, err := models.GetRelease(ctx.Repo.Repository.Id, tagName) + if err != nil { + if err == models.ErrReleaseNotExist { + ctx.Handle(404, "release.EditReleasePost(GetRelease)", err) + } else { + ctx.Handle(500, "release.EditReleasePost(GetRelease)", err) + } + return + } + ctx.Data["Release"] = rel + + if ctx.HasError() { + ctx.HTML(200, RELEASE_EDIT) + return + } + + ctx.Data["Title"] = "Edit Release" + ctx.Data["IsRepoToolbarReleases"] = true + + rel.Title = form.Title + rel.Note = form.Content + rel.IsDraft = len(form.Draft) > 0 + rel.IsPrerelease = form.Prerelease + if err = models.UpdateRelease(ctx.Repo.GitRepo, rel); err != nil { + ctx.Handle(500, "release.EditReleasePost(UpdateRelease)", err) + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/releases") +} diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 6db453007c..6cb6c0660e 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -25,12 +25,25 @@ import ( "github.com/gogits/gogs/modules/middleware" ) +const ( + CREATE base.TplName = "repo/create" + MIGRATE base.TplName = "repo/migrate" + SINGLE base.TplName = "repo/single" +) + func Create(ctx *middleware.Context) { ctx.Data["Title"] = "Create repository" ctx.Data["PageIsNewRepo"] = true ctx.Data["LanguageIgns"] = models.LanguageIgns ctx.Data["Licenses"] = models.Licenses - ctx.HTML(200, "repo/create") + + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs + + ctx.HTML(200, CREATE) } func CreatePost(ctx *middleware.Context, form auth.CreateRepoForm) { @@ -39,76 +52,125 @@ func CreatePost(ctx *middleware.Context, form auth.CreateRepoForm) { ctx.Data["LanguageIgns"] = models.LanguageIgns ctx.Data["Licenses"] = models.Licenses + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.CreatePost(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs + if ctx.HasError() { - ctx.HTML(200, "repo/create") + ctx.HTML(200, CREATE) return } - repo, err := models.CreateRepository(ctx.User, form.RepoName, form.Description, + u := ctx.User + // Not equal means current user is an organization. + if u.Id != form.Uid { + var err error + u, err = models.GetUserById(form.Uid) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "home.CreatePost(GetUserById)", err) + } else { + ctx.Handle(500, "home.CreatePost(GetUserById)", err) + } + return + } + } + + repo, err := models.CreateRepository(u, form.RepoName, form.Description, form.Language, form.License, form.Private, false, form.InitReadme) if err == nil { - log.Trace("%s Repository created: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, form.RepoName) - ctx.Redirect("/" + ctx.User.Name + "/" + form.RepoName) + log.Trace("%s Repository created: %s/%s", ctx.Req.RequestURI, u.LowerName, form.RepoName) + ctx.Redirect("/" + u.Name + "/" + form.RepoName) return } else if err == models.ErrRepoAlreadyExist { - ctx.RenderWithErr("Repository name has already been used", "repo/create", &form) + ctx.RenderWithErr("Repository name has already been used", CREATE, &form) return } else if err == models.ErrRepoNameIllegal { - ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "repo/create", &form) + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), CREATE, &form) return } if repo != nil { - if errDelete := models.DeleteRepository(ctx.User.Id, repo.Id, ctx.User.Name); errDelete != nil { - log.Error("repo.MigratePost(CreatePost): %v", errDelete) + if errDelete := models.DeleteRepository(u.Id, repo.Id, u.Name); errDelete != nil { + log.Error("repo.CreatePost(DeleteRepository): %v", errDelete) } } - ctx.Handle(500, "repo.Create", err) + ctx.Handle(500, "repo.CreatePost(CreateRepository)", err) } func Migrate(ctx *middleware.Context) { ctx.Data["Title"] = "Migrate repository" ctx.Data["PageIsNewRepo"] = true - ctx.HTML(200, "repo/migrate") + + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.Migrate(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs + + ctx.HTML(200, MIGRATE) } func MigratePost(ctx *middleware.Context, form auth.MigrateRepoForm) { ctx.Data["Title"] = "Migrate repository" ctx.Data["PageIsNewRepo"] = true - if ctx.HasError() { - ctx.HTML(200, "repo/migrate") + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.MigratePost(GetOrganizations)", err) return } + ctx.Data["Orgs"] = ctx.User.Orgs + + if ctx.HasError() { + ctx.HTML(200, MIGRATE) + return + } + + u := ctx.User + // Not equal means current user is an organization. + if u.Id != form.Uid { + var err error + u, err = models.GetUserById(form.Uid) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Handle(404, "home.MigratePost(GetUserById)", err) + } else { + ctx.Handle(500, "home.MigratePost(GetUserById)", err) + } + return + } + } authStr := strings.Replace(fmt.Sprintf("://%s:%s", form.AuthUserName, form.AuthPasswd), "@", "%40", -1) url := strings.Replace(form.Url, "://", authStr+"@", 1) - repo, err := models.MigrateRepository(ctx.User, form.RepoName, form.Description, form.Private, + repo, err := models.MigrateRepository(u, form.RepoName, form.Description, form.Private, form.Mirror, url) if err == nil { - log.Trace("%s Repository migrated: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, form.RepoName) - ctx.Redirect("/" + ctx.User.Name + "/" + form.RepoName) + log.Trace("%s Repository migrated: %s/%s", ctx.Req.RequestURI, u.LowerName, form.RepoName) + ctx.Redirect("/" + u.Name + "/" + form.RepoName) return } else if err == models.ErrRepoAlreadyExist { - ctx.RenderWithErr("Repository name has already been used", "repo/migrate", &form) + ctx.RenderWithErr("Repository name has already been used", MIGRATE, &form) return } else if err == models.ErrRepoNameIllegal { - ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "repo/migrate", &form) + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), MIGRATE, &form) return } if repo != nil { - if errDelete := models.DeleteRepository(ctx.User.Id, repo.Id, ctx.User.Name); errDelete != nil { + if errDelete := models.DeleteRepository(u.Id, repo.Id, u.Name); errDelete != nil { log.Error("repo.MigratePost(DeleteRepository): %v", errDelete) } } if strings.Contains(err.Error(), "Authentication failed") { - ctx.RenderWithErr(err.Error(), "repo/migrate", &form) + ctx.RenderWithErr(err.Error(), MIGRATE, &form) return } - ctx.Handle(500, "repo.Migrate", err) + ctx.Handle(500, "repo.Migrate(MigrateRepository)", err) } func Single(ctx *middleware.Context, params martini.Params) { @@ -291,7 +353,7 @@ func Single(ctx *middleware.Context, params martini.Params) { ctx.Data["Treenames"] = treenames ctx.Data["TreePath"] = treePath ctx.Data["BranchLink"] = branchLink - ctx.HTML(200, "repo/single") + ctx.HTML(200, SINGLE) } func basicEncode(username, password string) string { @@ -318,7 +380,7 @@ func basicDecode(encoded string) (user string, name string, err error) { func authRequired(ctx *middleware.Context) { ctx.ResponseWriter.Header().Set("WWW-Authenticate", "Basic realm=\".\"") ctx.Data["ErrorMsg"] = "no basic auth and digit auth" - ctx.HTML(401, fmt.Sprintf("status/401")) + ctx.HTML(401, base.TplName("status/401")) } func Action(ctx *middleware.Context, params martini.Params) { diff --git a/routers/repo/setting.go b/routers/repo/setting.go index fe2489923e..e97eca1239 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -7,6 +7,7 @@ package repo import ( "fmt" "strings" + "time" "github.com/go-martini/martini" @@ -19,10 +20,19 @@ import ( "github.com/gogits/gogs/modules/setting" ) +const ( + SETTING base.TplName = "repo/setting" + COLLABORATION base.TplName = "repo/collaboration" + + HOOKS base.TplName = "repo/hooks" + HOOK_ADD base.TplName = "repo/hook_add" + HOOK_EDIT base.TplName = "repo/hook_edit" +) + func Setting(ctx *middleware.Context) { ctx.Data["IsRepoToolbarSetting"] = true ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - settings" - ctx.HTML(200, "repo/setting") + ctx.HTML(200, SETTING) } func SettingPost(ctx *middleware.Context, form auth.RepoSettingForm) { @@ -31,7 +41,7 @@ func SettingPost(ctx *middleware.Context, form auth.RepoSettingForm) { switch ctx.Query("action") { case "update": if ctx.HasError() { - ctx.HTML(200, "repo/setting") + ctx.HTML(200, SETTING) return } @@ -43,7 +53,7 @@ func SettingPost(ctx *middleware.Context, form auth.RepoSettingForm) { ctx.Handle(500, "setting.SettingPost(update: check existence)", err) return } else if isExist { - ctx.RenderWithErr("Repository name has been taken in your repositories.", "repo/setting", nil) + ctx.RenderWithErr("Repository name has been taken in your repositories.", SETTING, nil) return } else if err = models.ChangeRepositoryName(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName); err != nil { ctx.Handle(500, "setting.SettingPost(change repository name)", err) @@ -72,6 +82,7 @@ func SettingPost(ctx *middleware.Context, form auth.RepoSettingForm) { if ctx.Repo.Repository.IsMirror { if form.Interval > 0 { ctx.Repo.Mirror.Interval = form.Interval + ctx.Repo.Mirror.NextUpdate = time.Now().Add(time.Duration(form.Interval) * time.Hour) if err := models.UpdateMirror(ctx.Repo.Mirror); err != nil { log.Error("setting.SettingPost(UpdateMirror): %v", err) } @@ -82,7 +93,7 @@ func SettingPost(ctx *middleware.Context, form auth.RepoSettingForm) { ctx.Redirect(fmt.Sprintf("/%s/%s/settings", ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)) case "transfer": if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { - ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil) + ctx.RenderWithErr("Please make sure you entered repository name is correct.", SETTING, nil) return } else if ctx.Repo.Repository.IsMirror { ctx.Error(404) @@ -96,7 +107,7 @@ func SettingPost(ctx *middleware.Context, form auth.RepoSettingForm) { ctx.Handle(500, "setting.SettingPost(transfer: check existence)", err) return } else if !isExist { - ctx.RenderWithErr("Please make sure you entered owner name is correct.", "repo/setting", nil) + ctx.RenderWithErr("Please make sure you entered owner name is correct.", SETTING, nil) return } else if err = models.TransferOwnership(ctx.User, newOwner, ctx.Repo.Repository); err != nil { ctx.Handle(500, "setting.SettingPost(transfer repository)", err) @@ -107,17 +118,27 @@ func SettingPost(ctx *middleware.Context, form auth.RepoSettingForm) { ctx.Redirect("/") case "delete": if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { - ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil) + ctx.RenderWithErr("Please make sure you entered repository name is correct.", SETTING, nil) return } - if err := models.DeleteRepository(ctx.User.Id, ctx.Repo.Repository.Id, ctx.User.LowerName); err != nil { - ctx.Handle(500, "setting.Delete", err) + if ctx.Repo.Owner.IsOrganization() && + !models.IsOrganizationOwner(ctx.Repo.Owner.Id, ctx.User.Id) { + ctx.Error(403) return } - log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) - ctx.Redirect("/") + if err := models.DeleteRepository(ctx.Repo.Owner.Id, ctx.Repo.Repository.Id, ctx.Repo.Owner.Name); err != nil { + ctx.Handle(500, "setting.Delete(DeleteRepository)", err) + return + } + log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.LowerName, ctx.Repo.Repository.LowerName) + + if ctx.Repo.Owner.IsOrganization() { + ctx.Redirect("/org/" + ctx.Repo.Owner.Name + "/dashboard") + } else { + ctx.Redirect("/") + } } } @@ -154,7 +175,7 @@ func Collaboration(ctx *middleware.Context) { } ctx.Data["Collaborators"] = us - ctx.HTML(200, "repo/collaboration") + ctx.HTML(200, COLLABORATION) } func CollaborationPost(ctx *middleware.Context) { @@ -164,7 +185,7 @@ func CollaborationPost(ctx *middleware.Context) { ctx.Redirect(ctx.Req.RequestURI) return } - has, err := models.HasAccess(name, repoLink, models.AU_WRITABLE) + has, err := models.HasAccess(name, repoLink, models.WRITABLE) if err != nil { ctx.Handle(500, "setting.CollaborationPost(HasAccess)", err) return @@ -185,12 +206,12 @@ func CollaborationPost(ctx *middleware.Context) { } if err = models.AddAccess(&models.Access{UserName: name, RepoName: repoLink, - Mode: models.AU_WRITABLE}); err != nil { + Mode: models.WRITABLE}); err != nil { ctx.Handle(500, "setting.CollaborationPost(AddAccess)", err) return } - if setting.Service.NotifyMail { + if setting.Service.EnableNotifyMail { if err = mailer.SendCollaboratorMail(ctx.Render, u, ctx.User, ctx.Repo.Repository); err != nil { ctx.Handle(500, "setting.CollaborationPost(SendCollaboratorMail)", err) return @@ -224,13 +245,13 @@ func WebHooks(ctx *middleware.Context) { } ctx.Data["Webhooks"] = ws - ctx.HTML(200, "repo/hooks") + ctx.HTML(200, HOOKS) } func WebHooksAdd(ctx *middleware.Context) { ctx.Data["IsRepoToolbarWebHooks"] = true ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Add Webhook" - ctx.HTML(200, "repo/hooks_add") + ctx.HTML(200, HOOK_ADD) } func WebHooksAddPost(ctx *middleware.Context, form auth.NewWebhookForm) { @@ -238,13 +259,13 @@ func WebHooksAddPost(ctx *middleware.Context, form auth.NewWebhookForm) { ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Add Webhook" if ctx.HasError() { - ctx.HTML(200, "repo/hooks_add") + ctx.HTML(200, HOOK_ADD) return } - ct := models.CT_JSON + ct := models.JSON if form.ContentType == "2" { - ct = models.CT_FORM + ct = models.FORM } w := &models.Webhook{ @@ -257,8 +278,8 @@ func WebHooksAddPost(ctx *middleware.Context, form auth.NewWebhookForm) { }, IsActive: form.Active, } - if err := w.SaveEvent(); err != nil { - ctx.Handle(500, "setting.WebHooksAddPost(SaveEvent)", err) + if err := w.UpdateEvent(); err != nil { + ctx.Handle(500, "setting.WebHooksAddPost(UpdateEvent)", err) return } else if err := models.CreateWebhook(w); err != nil { ctx.Handle(500, "setting.WebHooksAddPost(CreateWebhook)", err) @@ -291,42 +312,48 @@ func WebHooksEdit(ctx *middleware.Context, params martini.Params) { w.GetEvent() ctx.Data["Webhook"] = w - ctx.HTML(200, "repo/hooks_edit") + ctx.HTML(200, HOOK_EDIT) } func WebHooksEditPost(ctx *middleware.Context, params martini.Params, form auth.NewWebhookForm) { ctx.Data["IsRepoToolbarWebHooks"] = true ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - Webhook" - if ctx.HasError() { - ctx.HTML(200, "repo/hooks_add") - return - } - hookId, _ := base.StrTo(params["id"]).Int64() if hookId == 0 { ctx.Handle(404, "setting.WebHooksEditPost", nil) return } - ct := models.CT_JSON - if form.ContentType == "2" { - ct = models.CT_FORM + w, err := models.GetWebhookById(hookId) + if err != nil { + if err == models.ErrWebhookNotExist { + ctx.Handle(404, "setting.WebHooksEditPost(GetWebhookById)", nil) + } else { + ctx.Handle(500, "setting.WebHooksEditPost(GetWebhookById)", err) + } + return } - w := &models.Webhook{ - Id: hookId, - RepoId: ctx.Repo.Repository.Id, - Url: form.Url, - ContentType: ct, - Secret: form.Secret, - HookEvent: &models.HookEvent{ - PushOnly: form.PushOnly, - }, - IsActive: form.Active, + if ctx.HasError() { + ctx.HTML(200, HOOK_EDIT) + return } - if err := w.SaveEvent(); err != nil { - ctx.Handle(500, "setting.WebHooksEditPost(SaveEvent)", err) + + ct := models.JSON + if form.ContentType == "2" { + ct = models.FORM + } + + w.Url = form.Url + w.ContentType = ct + w.Secret = form.Secret + w.HookEvent = &models.HookEvent{ + PushOnly: form.PushOnly, + } + w.IsActive = form.Active + if err := w.UpdateEvent(); err != nil { + ctx.Handle(500, "setting.WebHooksEditPost(UpdateEvent)", err) return } else if err := models.UpdateWebhook(w); err != nil { ctx.Handle(500, "setting.WebHooksEditPost(WebHooksEditPost)", err) diff --git a/routers/user/home.go b/routers/user/home.go index a9674ac242..86907b5a90 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -17,10 +17,25 @@ import ( "github.com/gogits/gogs/modules/middleware" ) +const ( + DASHBOARD base.TplName = "user/dashboard" + PROFILE base.TplName = "user/profile" + ISSUES base.TplName = "user/issues" + PULLS base.TplName = "user/pulls" + STARS base.TplName = "user/stars" +) + func Dashboard(ctx *middleware.Context) { ctx.Data["Title"] = "Dashboard" ctx.Data["PageIsUserDashboard"] = true + if err := ctx.User.GetOrganizations(); err != nil { + ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) + return + } + ctx.Data["Orgs"] = ctx.User.Orgs + ctx.Data["ContextUser"] = ctx.User + var err error ctx.Data["MyRepos"], err = models.GetRepositories(ctx.User.Id, true) if err != nil { @@ -45,21 +60,21 @@ func Dashboard(ctx *middleware.Context) { for _, act := range actions { if act.IsPrivate { if has, _ := models.HasAccess(ctx.User.Name, act.RepoUserName+"/"+act.RepoName, - models.AU_READABLE); !has { + models.READABLE); !has { continue } } feeds = append(feeds, act) } ctx.Data["Feeds"] = feeds - ctx.HTML(200, "user/dashboard") + ctx.HTML(200, DASHBOARD) } func Profile(ctx *middleware.Context, params martini.Params) { ctx.Data["Title"] = "Profile" ctx.Data["PageIsUserProfile"] = true - user, err := models.GetUserByName(params["username"]) + u, err := models.GetUserByName(params["username"]) if err != nil { if err == models.ErrUserNotExist { ctx.Handle(404, "user.Profile(GetUserByName)", err) @@ -68,26 +83,30 @@ func Profile(ctx *middleware.Context, params martini.Params) { } return } - ctx.Data["Owner"] = user + // For security reason, hide e-mail address for anonymous visitors. + if !ctx.IsSigned { + u.Email = "" + } + ctx.Data["Owner"] = u tab := ctx.Query("tab") ctx.Data["TabName"] = tab switch tab { case "activity": - ctx.Data["Feeds"], err = models.GetFeeds(user.Id, 0, true) + ctx.Data["Feeds"], err = models.GetFeeds(u.Id, 0, true) if err != nil { ctx.Handle(500, "user.Profile(GetFeeds)", err) return } default: - ctx.Data["Repos"], err = models.GetRepositories(user.Id, ctx.IsSigned && ctx.User.Id == user.Id) + ctx.Data["Repos"], err = models.GetRepositories(u.Id, ctx.IsSigned && ctx.User.Id == u.Id) if err != nil { ctx.Handle(500, "user.Profile(GetRepositories)", err) return } } - ctx.HTML(200, "user/profile") + ctx.HTML(200, PROFILE) } func Email2User(ctx *middleware.Context) { @@ -119,7 +138,7 @@ func Feeds(ctx *middleware.Context, form auth.FeedsForm) { for _, act := range actions { if act.IsPrivate { if has, _ := models.HasAccess(ctx.User.Name, act.RepoUserName+"/"+act.RepoName, - models.AU_READABLE); !has { + models.READABLE); !has { continue } } @@ -254,13 +273,13 @@ func Issues(ctx *middleware.Context) { } else { ctx.Data["ShowCount"] = issueStats.OpenCount } - ctx.HTML(200, "user/issue") + ctx.HTML(200, ISSUES) } func Pulls(ctx *middleware.Context) { - ctx.HTML(200, "user/pulls") + ctx.HTML(200, PULLS) } func Stars(ctx *middleware.Context) { - ctx.HTML(200, "user/stars") + ctx.HTML(200, STARS) } diff --git a/routers/user/setting.go b/routers/user/setting.go index 1fae516a43..8e4b0840c7 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -14,12 +14,21 @@ import ( "github.com/gogits/gogs/modules/middleware" ) +const ( + SETTING base.TplName = "user/setting" + SOCIAL base.TplName = "user/social" + PASSWORD base.TplName = "user/password" + PUBLICKEY base.TplName = "user/publickey" + NOTIFICATION base.TplName = "user/notification" + SECURITY base.TplName = "user/security" +) + func Setting(ctx *middleware.Context) { ctx.Data["Title"] = "Setting" ctx.Data["PageIsUserSetting"] = true ctx.Data["IsUserPageSetting"] = true ctx.Data["Owner"] = ctx.User - ctx.HTML(200, "user/setting") + ctx.HTML(200, SETTING) } func SettingPost(ctx *middleware.Context, form auth.UpdateProfileForm) { @@ -28,7 +37,7 @@ func SettingPost(ctx *middleware.Context, form auth.UpdateProfileForm) { ctx.Data["IsUserPageSetting"] = true if ctx.HasError() { - ctx.HTML(200, "user/setting") + ctx.HTML(200, SETTING) return } @@ -59,7 +68,7 @@ func SettingPost(ctx *middleware.Context, form auth.UpdateProfileForm) { ctx.User.Avatar = base.EncodeMd5(form.Avatar) ctx.User.AvatarEmail = form.Avatar if err := models.UpdateUser(ctx.User); err != nil { - ctx.Handle(500, "setting.Setting", err) + ctx.Handle(500, "setting.Setting(UpdateUser)", err) return } log.Trace("%s User setting updated: %s", ctx.Req.RequestURI, ctx.User.LowerName) @@ -90,14 +99,14 @@ func SettingSocial(ctx *middleware.Context) { ctx.Handle(500, "user.SettingSocial(GetOauthByUserId)", err) return } - ctx.HTML(200, "user/social") + ctx.HTML(200, SOCIAL) } func SettingPassword(ctx *middleware.Context) { ctx.Data["Title"] = "Password" ctx.Data["PageIsUserSetting"] = true ctx.Data["IsUserPageSettingPasswd"] = true - ctx.HTML(200, "user/password") + ctx.HTML(200, PASSWORD) } func SettingPasswordPost(ctx *middleware.Context, form auth.UpdatePasswdForm) { @@ -106,7 +115,7 @@ func SettingPasswordPost(ctx *middleware.Context, form auth.UpdatePasswdForm) { ctx.Data["IsUserPageSettingPasswd"] = true if ctx.HasError() { - ctx.HTML(200, "user/password") + ctx.HTML(200, PASSWORD) return } @@ -207,7 +216,7 @@ func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) { } } - ctx.HTML(200, "user/publickey") + ctx.HTML(200, PUBLICKEY) } func SettingNotification(ctx *middleware.Context) { @@ -215,7 +224,7 @@ func SettingNotification(ctx *middleware.Context) { ctx.Data["Title"] = "Notification" ctx.Data["PageIsUserSetting"] = true ctx.Data["IsUserPageSettingNotify"] = true - ctx.HTML(200, "user/notification") + ctx.HTML(200, NOTIFICATION) } func SettingSecurity(ctx *middleware.Context) { @@ -223,5 +232,5 @@ func SettingSecurity(ctx *middleware.Context) { ctx.Data["Title"] = "Security" ctx.Data["PageIsUserSetting"] = true ctx.Data["IsUserPageSettingSecurity"] = true - ctx.HTML(200, "user/security") + ctx.HTML(200, SECURITY) } diff --git a/routers/user/user.go b/routers/user/user.go index a5b3e79253..561fe1c111 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -17,12 +17,21 @@ import ( "github.com/gogits/gogs/modules/setting" ) +const ( + SIGNIN base.TplName = "user/signin" + SIGNUP base.TplName = "user/signup" + DELETE base.TplName = "user/delete" + ACTIVATE base.TplName = "user/activate" + FORGOT_PASSWORD base.TplName = "user/forgot_passwd" + RESET_PASSWORD base.TplName = "user/reset_passwd" +) + func SignIn(ctx *middleware.Context) { ctx.Data["Title"] = "Log In" if _, ok := ctx.Session.Get("socialId").(int64); ok { ctx.Data["IsSocialLogin"] = true - ctx.HTML(200, "user/signin") + ctx.HTML(200, SIGNIN) return } @@ -32,23 +41,23 @@ func SignIn(ctx *middleware.Context) { } // Check auto-login. - userName := ctx.GetCookie(setting.CookieUserName) - if len(userName) == 0 { - ctx.HTML(200, "user/signin") + uname := ctx.GetCookie(setting.CookieUserName) + if len(uname) == 0 { + ctx.HTML(200, SIGNIN) return } isSucceed := false defer func() { if !isSucceed { - log.Trace("user.SignIn(auto-login cookie cleared): %s", userName) + log.Trace("user.SignIn(auto-login cookie cleared): %s", uname) ctx.SetCookie(setting.CookieUserName, "", -1) ctx.SetCookie(setting.CookieRememberName, "", -1) return } }() - user, err := models.GetUserByName(userName) + user, err := models.GetUserByName(uname) if err != nil { ctx.Handle(500, "user.SignIn(GetUserByName)", err) return @@ -57,7 +66,7 @@ func SignIn(ctx *middleware.Context) { secret := base.EncodeMd5(user.Rands + user.Passwd) value, _ := ctx.GetSecureCookie(secret, setting.CookieRememberName) if value != user.Name { - ctx.HTML(200, "user/signin") + ctx.HTML(200, SIGNIN) return } @@ -86,19 +95,19 @@ func SignInPost(ctx *middleware.Context, form auth.LogInForm) { } if ctx.HasError() { - ctx.HTML(200, "user/signin") + ctx.HTML(200, SIGNIN) return } - user, err := models.LoginUser(form.UserName, form.Password) + user, err := models.UserSignIn(form.UserName, form.Password) if err != nil { if err == models.ErrUserNotExist { log.Trace("%s Log in failed: %s", ctx.Req.RequestURI, form.UserName) - ctx.RenderWithErr("Username or password is not correct", "user/signin", &form) + ctx.RenderWithErr("Username or password is not correct", SIGNIN, &form) return } - ctx.Handle(500, "user.SignIn", err) + ctx.Handle(500, "user.SignInPost(UserSignIn)", err) return } @@ -151,7 +160,7 @@ func SignUp(ctx *middleware.Context) { if setting.Service.DisableRegistration { ctx.Data["DisableRegistration"] = true - ctx.HTML(200, "user/signup") + ctx.HTML(200, SIGNUP) return } @@ -160,7 +169,7 @@ func SignUp(ctx *middleware.Context) { return } - ctx.HTML(200, "user/signup") + ctx.HTML(200, SIGNUP) } func oauthSignUp(ctx *middleware.Context, sid int64) { @@ -180,7 +189,7 @@ func oauthSignUp(ctx *middleware.Context, sid int64) { ctx.Data["username"] = strings.Replace(ctx.Session.Get("socialName").(string), " ", "", -1) ctx.Data["email"] = ctx.Session.Get("socialEmail") log.Trace("user.oauthSignUp(social ID): %v", ctx.Session.Get("socialId")) - ctx.HTML(200, "user/signup") + ctx.HTML(200, SIGNUP) } func SignUpPost(ctx *middleware.Context, form auth.RegisterForm) { @@ -198,14 +207,14 @@ func SignUpPost(ctx *middleware.Context, form auth.RegisterForm) { } if ctx.HasError() { - ctx.HTML(200, "user/signup") + ctx.HTML(200, SIGNUP) return } if form.Password != form.RetypePasswd { ctx.Data["Err_Password"] = true ctx.Data["Err_RetypePasswd"] = true - ctx.RenderWithErr("Password and re-type password are not same.", "user/signup", &form) + ctx.RenderWithErr("Password and re-type password are not same.", SIGNUP, &form) return } @@ -217,21 +226,23 @@ func SignUpPost(ctx *middleware.Context, form auth.RegisterForm) { } var err error - if u, err = models.RegisterUser(u); err != nil { + if u, err = models.CreateUser(u); err != nil { switch err { case models.ErrUserAlreadyExist: - ctx.RenderWithErr("Username has been already taken", "user/signup", &form) + ctx.Data["Err_UserName"] = true + ctx.RenderWithErr("Username has been already taken", SIGNUP, &form) case models.ErrEmailAlreadyUsed: - ctx.RenderWithErr("E-mail address has been already used", "user/signup", &form) + ctx.Data["Err_Email"] = true + ctx.RenderWithErr("E-mail address has been already used", SIGNUP, &form) case models.ErrUserNameIllegal: - ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "user/signup", &form) + ctx.Data["Err_UserName"] = true + ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), SIGNUP, &form) default: - ctx.Handle(500, "user.SignUp(RegisterUser)", err) + ctx.Handle(500, "user.SignUpPost(CreateUser)", err) } return } - - log.Trace("%s User created: %s", ctx.Req.RequestURI, form.UserName) + log.Trace("%s User created: %s", ctx.Req.RequestURI, u.Name) // Bind social account. if isOauth { @@ -256,6 +267,7 @@ func SignUpPost(ctx *middleware.Context, form auth.RegisterForm) { } return } + ctx.Redirect("/user/login") } @@ -263,7 +275,7 @@ func Delete(ctx *middleware.Context) { ctx.Data["Title"] = "Delete Account" ctx.Data["PageIsUserSetting"] = true ctx.Data["IsUserPageSettingDelete"] = true - ctx.HTML(200, "user/delete") + ctx.HTML(200, DELETE) } func DeletePost(ctx *middleware.Context) { @@ -284,7 +296,7 @@ func DeletePost(ctx *middleware.Context) { case models.ErrUserOwnRepos: ctx.Flash.Error("Your account still have ownership of repository, you have to delete or transfer them first.") default: - ctx.Handle(500, "user.Delete", err) + ctx.Handle(500, "user.DeletePost(DeleteUser)", err) return } } else { @@ -319,7 +331,7 @@ func Activate(ctx *middleware.Context) { } else { ctx.Data["ServiceNotEnabled"] = true } - ctx.HTML(200, "user/activate") + ctx.HTML(200, ACTIVATE) return } @@ -341,7 +353,7 @@ func Activate(ctx *middleware.Context) { } ctx.Data["IsActivateFailed"] = true - ctx.HTML(200, "user/activate") + ctx.HTML(200, ACTIVATE) } func ForgotPasswd(ctx *middleware.Context) { @@ -349,12 +361,12 @@ func ForgotPasswd(ctx *middleware.Context) { if setting.MailService == nil { ctx.Data["IsResetDisable"] = true - ctx.HTML(200, "user/forgot_passwd") + ctx.HTML(200, FORGOT_PASSWORD) return } ctx.Data["IsResetRequest"] = true - ctx.HTML(200, "user/forgot_passwd") + ctx.HTML(200, FORGOT_PASSWORD) } func ForgotPasswdPost(ctx *middleware.Context) { @@ -379,7 +391,7 @@ func ForgotPasswdPost(ctx *middleware.Context) { if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) { ctx.Data["ResendLimited"] = true - ctx.HTML(200, "user/forgot_passwd") + ctx.HTML(200, FORGOT_PASSWORD) return } @@ -391,7 +403,7 @@ func ForgotPasswdPost(ctx *middleware.Context) { ctx.Data["Email"] = email ctx.Data["Hours"] = setting.Service.ActiveCodeLives / 60 ctx.Data["IsResetSent"] = true - ctx.HTML(200, "user/forgot_passwd") + ctx.HTML(200, FORGOT_PASSWORD) } func ResetPasswd(ctx *middleware.Context) { @@ -404,7 +416,7 @@ func ResetPasswd(ctx *middleware.Context) { } ctx.Data["Code"] = code ctx.Data["IsResetForm"] = true - ctx.HTML(200, "user/reset_passwd") + ctx.HTML(200, RESET_PASSWORD) } func ResetPasswdPost(ctx *middleware.Context) { @@ -441,5 +453,5 @@ func ResetPasswdPost(ctx *middleware.Context) { } ctx.Data["IsResetFailed"] = true - ctx.HTML(200, "user/reset_passwd") + ctx.HTML(200, RESET_PASSWORD) } diff --git a/templates/VERSION b/templates/VERSION index 81b6032124..09c5198045 100644 --- a/templates/VERSION +++ b/templates/VERSION @@ -1 +1 @@ -0.4.1.0601 Alpha \ No newline at end of file +0.4.5.0628 Alpha \ No newline at end of file diff --git a/templates/admin/auths/edit.tmpl b/templates/admin/auth/edit.tmpl similarity index 98% rename from templates/admin/auths/edit.tmpl rename to templates/admin/auth/edit.tmpl index deea447c89..a2c2ddc698 100644 --- a/templates/admin/auths/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -71,21 +71,21 @@
- +
- +
- +
{{else if eq $type 3}} diff --git a/templates/admin/auths/new.tmpl b/templates/admin/auth/new.tmpl similarity index 100% rename from templates/admin/auths/new.tmpl rename to templates/admin/auth/new.tmpl diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index a8e9d1ae99..10a53b5397 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -36,6 +36,8 @@
{{.LogRootPath}}
Script Type
{{.ScriptType}}
+
Reverse Authentication User
+
{{.ReverseProxyAuthUser}}
@@ -77,7 +79,7 @@
Require Sign In View
Mail Notification
-
+
Enable Cache Avatar

@@ -89,6 +91,21 @@ +
+
+ Webhook Configuration +
+ +
+
+
Task Interval
+
{{.WebhookTaskInterval}} minutes
+
Deliver Timeout
+
{{.WebhookDeliverTimeout}} seconds
+
+
+
+
Mailer Configuration diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index f709cb3f28..aa2080d83e 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -32,6 +32,10 @@ Clean unbind OAuthes Run + + Delete inactivate accounts + Run +
diff --git a/templates/admin/monitor/cron.tmpl b/templates/admin/monitor/cron.tmpl new file mode 100644 index 0000000000..a04c017e29 --- /dev/null +++ b/templates/admin/monitor/cron.tmpl @@ -0,0 +1,40 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +
+ {{template "admin/nav" .}} +
+ +
+
+ {{if .PageIsMonitorCron}} + + + + + + + + + + + + {{range .Entries}} + + + + + + + + {{end}} + +
NameScheduleNext TimePrevious TimeExecute Times
{{.Description}}{{.Spec}}{{.Next}}{{.Prev}}{{.ExecTimes}}
+ {{end}} +
+
+
+
+{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/admin/monitor/process.tmpl b/templates/admin/monitor/process.tmpl new file mode 100644 index 0000000000..2d60ff6895 --- /dev/null +++ b/templates/admin/monitor/process.tmpl @@ -0,0 +1,38 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +
+ {{template "admin/nav" .}} +
+ +
+
+ {{if .PageIsMonitorProcess}} + + + + + + + + + + + {{range .Processes}} + + + + + + + {{end}} + +
PidDescriptionStart TimeExecution Time
{{.Pid}}{{.Description}}{{.Start}}{{TimeSince .Start}}
+ {{end}} +
+
+
+
+{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/admin/nav.tmpl b/templates/admin/nav.tmpl index 5ba4495796..b78e0bd17d 100644 --- a/templates/admin/nav.tmpl +++ b/templates/admin/nav.tmpl @@ -5,5 +5,6 @@
  • Repositories
  • Authentication
  • Configuration
  • +
  • Monitoring
  • \ No newline at end of file diff --git a/templates/admin/users/edit.tmpl b/templates/admin/user/edit.tmpl similarity index 100% rename from templates/admin/users/edit.tmpl rename to templates/admin/user/edit.tmpl diff --git a/templates/admin/users/new.tmpl b/templates/admin/user/new.tmpl similarity index 100% rename from templates/admin/users/new.tmpl rename to templates/admin/user/new.tmpl diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index e7759c2985..a58299f8cf 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -14,7 +14,7 @@ {{if CdnMode}} - + diff --git a/templates/mail/auth/active_email.tmpl b/templates/mail/auth/active.tmpl similarity index 100% rename from templates/mail/auth/active_email.tmpl rename to templates/mail/auth/active.tmpl diff --git a/templates/org/edit_team.tmpl b/templates/org/edit_team.tmpl new file mode 100644 index 0000000000..4292575c87 --- /dev/null +++ b/templates/org/edit_team.tmpl @@ -0,0 +1,75 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +
    +
    +
    + + +
    +

    Organization Name

    +
    +
    +
    +
    +
    +
    +
    +

    Edit team

    +
    + +
    + + You'll use this name to mention this team in conversations. +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + +

    This team will be able to view and clone its repositories.

    +
    +
    + +

    This team will be able to read its repositories, as well as push to them.

    +
    +
    + +

    This team will be able to push/pull to its repositories, as well as add other collaborators to them.

    +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/org/members.tmpl b/templates/org/members.tmpl new file mode 100644 index 0000000000..ba14cb4cc9 --- /dev/null +++ b/templates/org/members.tmpl @@ -0,0 +1,56 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +
    +
    +
    + + +
    +

    Organization Name

    +
    +
    + +
    +
    +
    +
    +
    +
      +
    + +
    + +
    + Member +
    +
    + Public +
    +
    +
      +
    + +
    + +
    + Owner +
    +
    + Private +
    +
    +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/org/new.tmpl b/templates/org/new.tmpl new file mode 100644 index 0000000000..bb46db4ac3 --- /dev/null +++ b/templates/org/new.tmpl @@ -0,0 +1,32 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +
    +
    + {{.CsrfTokenHtml}} +

    Create New Organization

    + {{template "base/alert" .}} +
    + +
    + + Great organization names are short and memorable. +
    +
    + +
    + +
    + + Organization's Email receives all notifications and confirmations. +
    +
    + +
    +
    + + Cancel +
    +
    +
    +
    +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/org/new_team.tmpl b/templates/org/new_team.tmpl new file mode 100644 index 0000000000..752f37d2e0 --- /dev/null +++ b/templates/org/new_team.tmpl @@ -0,0 +1,74 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +
    +
    +
    + + +
    +

    Organization Name

    +
    +
    +
    +
    +
    +
    +
    +

    Create new team

    +
    + +
    + + You'll use this name to mention this team in conversations. +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + +

    This team will be able to view and clone its repositories.

    +
    +
    + +

    This team will be able to read its repositories, as well as push to them.

    +
    +
    + +

    This team will be able to push/pull to its repositories, as well as add other collaborators to them.

    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/org/org.tmpl b/templates/org/org.tmpl new file mode 100644 index 0000000000..872e50be86 --- /dev/null +++ b/templates/org/org.tmpl @@ -0,0 +1,85 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +
    +
    +
    + +
    +

    Organization Name

    +

    Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language.

    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
      +
    • Go
    • +
    • 6
    • +
    • 2
    • +
    +
    +

    gogs

    +

    Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language.

    +

    Updated 17 hours ago

    +
    +
    +
    +
      +
    • Go
    • +
    • 6
    • +
    • 2
    • +
    +
    +

    gogs

    +

    Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language.

    +

    Updated 17 hours ago

    +
    +
    +
    + +
    +
    +{{template "base/footer" .}} diff --git a/templates/org/settings.tmpl b/templates/org/settings.tmpl new file mode 100644 index 0000000000..fd0d6a1c14 --- /dev/null +++ b/templates/org/settings.tmpl @@ -0,0 +1,130 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +
    +
    +
    + +
    + +
    +
    + +
    +
    + +
    +
    + {{template "base/alert" .}} +
    +
    + Organization Options +
    + +
    +
    + {{.CsrfTokenHtml}} + + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + Danger Zone +
    +
    + +
    +
    Delete this organization
    +
    Once you delete this organization and all repositories in, there is no going back. Please be + certain. +
    + + + +
    +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/org/teams.tmpl b/templates/org/teams.tmpl new file mode 100644 index 0000000000..90aab94401 --- /dev/null +++ b/templates/org/teams.tmpl @@ -0,0 +1,71 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +
    +
    +
    + + +
    +

    Organization Name

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +

    Team Name

    +
    +

    4 members · 10 repositories

    +

    + + + + + + +

    +
    + +
    +
    +
    +
    +

    Team Name

    +
    +

    4 members · 10 repositories

    +

    + + + + + + +

    +
    + +
    +
    +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/repo/branches.tmpl b/templates/repo/branch.tmpl similarity index 100% rename from templates/repo/branches.tmpl rename to templates/repo/branch.tmpl diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 6da6a93d15..38b32a3bfc 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -8,9 +8,37 @@
    -

    {{.SignedUserName}}

    - +
    + + + +
    +
    diff --git a/templates/repo/hooks_add.tmpl b/templates/repo/hook_add.tmpl similarity index 100% rename from templates/repo/hooks_add.tmpl rename to templates/repo/hook_add.tmpl diff --git a/templates/repo/hooks_edit.tmpl b/templates/repo/hook_edit.tmpl similarity index 100% rename from templates/repo/hooks_edit.tmpl rename to templates/repo/hook_edit.tmpl diff --git a/templates/issue/create.tmpl b/templates/repo/issue/create.tmpl similarity index 100% rename from templates/issue/create.tmpl rename to templates/repo/issue/create.tmpl diff --git a/templates/issue/list.tmpl b/templates/repo/issue/list.tmpl similarity index 100% rename from templates/issue/list.tmpl rename to templates/repo/issue/list.tmpl diff --git a/templates/issue/milestone.tmpl b/templates/repo/issue/milestone.tmpl similarity index 100% rename from templates/issue/milestone.tmpl rename to templates/repo/issue/milestone.tmpl diff --git a/templates/issue/milestone_edit.tmpl b/templates/repo/issue/milestone_edit.tmpl similarity index 100% rename from templates/issue/milestone_edit.tmpl rename to templates/repo/issue/milestone_edit.tmpl diff --git a/templates/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl similarity index 100% rename from templates/issue/milestone_new.tmpl rename to templates/repo/issue/milestone_new.tmpl diff --git a/templates/issue/view.tmpl b/templates/repo/issue/view.tmpl similarity index 100% rename from templates/issue/view.tmpl rename to templates/repo/issue/view.tmpl diff --git a/templates/repo/migrate.tmpl b/templates/repo/migrate.tmpl index 34a4077eec..fff25e6de5 100644 --- a/templates/repo/migrate.tmpl +++ b/templates/repo/migrate.tmpl @@ -44,9 +44,37 @@
    -

    {{.SignedUserName}}

    - +
    + + + +
    +
    diff --git a/templates/repo/nav.tmpl b/templates/repo/nav.tmpl index 70e1745fff..ea7799b351 100644 --- a/templates/repo/nav.tmpl +++ b/templates/repo/nav.tmpl @@ -27,6 +27,7 @@
    diff --git a/templates/repo/release/edit.tmpl b/templates/repo/release/edit.tmpl new file mode 100644 index 0000000000..e437092c8c --- /dev/null +++ b/templates/repo/release/edit.tmpl @@ -0,0 +1,70 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +{{template "repo/nav" .}} +{{template "repo/toolbar" .}} +
    +
    +

    Edit Release

    + {{template "base/alert" .}} +
    + {{.CsrfTokenHtml}} +
    + {{.Release.TagName}} + @ +
    + + + + +
    +

    Choose an existing tag, or create a new tag on publish

    +
    +
    + +
    +
    +
    + Content with Markdown +
    + +
    +
    +
    + +
    +
    +
    loading...
    +
    +
    +
    +
    + +

    We’ll point out that this release is identified as non-production ready.

    +
    +
    + + +
    +
    +
    +
    +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/release/list.tmpl b/templates/repo/release/list.tmpl similarity index 78% rename from templates/release/list.tmpl rename to templates/repo/release/list.tmpl index edbc7467b4..0f02508fb9 100644 --- a/templates/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -11,20 +11,26 @@
      {{range .Releases}} -
    • +
    • {{if .PublisherId}}
      - {{if .IsPrerelease}}Pre-Release{{else}}Stable{{end}} + {{if .IsDraft}} + Draft + {{else if .IsPrerelease}} + Pre-Release + {{else}} + Stable + {{end}} {{.TagName}} - {{ShortSha .SHA1}} + {{ShortSha .Sha1}}
      -

      {{.Title}}

      +

      {{.Title}} (edit)

         {{.Publisher.Name}} {{if .Created}}{{TimeSince .Created}}{{end}} - {{.NumCommitsBehind}} commits since this release + {{.NumCommitsBehind}} commits to {{.Target}} since this release

      {{str2html .Note}} @@ -37,7 +43,7 @@
      {{else}}
      {{.TagName}}
      diff --git a/templates/release/new.tmpl b/templates/repo/release/new.tmpl similarity index 91% rename from templates/release/new.tmpl rename to templates/repo/release/new.tmpl index 6dfe4a5c2d..6c5cf40ceb 100644 --- a/templates/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -41,12 +41,12 @@
      - +
      loading...
      @@ -62,7 +62,7 @@
      - +
      diff --git a/templates/user/dashboard.tmpl b/templates/user/dashboard.tmpl index 5cda6722f0..2cb19cef16 100644 --- a/templates/user/dashboard.tmpl +++ b/templates/user/dashboard.tmpl @@ -2,15 +2,48 @@ {{template "base/navbar" .}}
      +
      + + + +
      -

      News Feed

      +
      {{if .HasInfo}}
      {{.InfoMsg}}
      {{end}}
      @@ -28,7 +61,7 @@
      -
      Your Repositories +
      {{if not .PageIsOrgDashboard}}Your {{end}}Repositories
      - + + {{if not .PageIsOrgDashboard}}
      Collaborative Repositories
      @@ -62,6 +96,7 @@
    + {{end}} {{template "base/footer" .}} diff --git a/templates/user/issue.tmpl b/templates/user/issues.tmpl similarity index 98% rename from templates/user/issue.tmpl rename to templates/user/issues.tmpl index d1c2bd9941..c4ad64a4cf 100644 --- a/templates/user/issue.tmpl +++ b/templates/user/issues.tmpl @@ -3,7 +3,7 @@
    +
    {{if .HasInfo}}
    {{.InfoMsg}}
    {{end}}
    diff --git a/templates/user/signin.tmpl b/templates/user/signin.tmpl index 09ce249f7f..9a8aa1992d 100644 --- a/templates/user/signin.tmpl +++ b/templates/user/signin.tmpl @@ -62,7 +62,7 @@ {{if .OauthService.GitHub}}GitHub{{end}} {{if .OauthService.Google}}Google{{end}} {{if .OauthService.Twitter}}Twitter{{end}} - {{if .OauthService.Tencent}}Tencent QQ{{end}} + {{if not .OauthService.Tencent}}Tencent QQ{{end}} {{if .OauthService.Weibo}}Weibo{{end}}
    {{end}}{{end}}