From 932f717adb1cb1738415fcddaf5eb3c0e4a60fc2 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 04:44:57 -0400 Subject: [PATCH 01/38] Fixing bug --- models/action.go | 10 ++++++++++ models/repo.go | 9 +++++++-- models/user.go | 4 +--- modules/middleware/repo.go | 2 +- serve.go | 2 +- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/models/action.go b/models/action.go index 107d4b1057..89751b9779 100644 --- a/models/action.go +++ b/models/action.go @@ -79,6 +79,16 @@ func CommitRepoAction(userId int64, userName string, }) return err } + + // Update repository last update time. + repo, err := GetRepositoryByName(userId, repoName) + if err != nil { + return err + } + repo.Updated = time.Now() + if err = UpdateRepository(repo); err != nil { + return err + } return nil } diff --git a/models/repo.go b/models/repo.go index 918e5dc84c..6a764e6c31 100644 --- a/models/repo.go +++ b/models/repo.go @@ -358,6 +358,11 @@ func RepoPath(userName, repoName string) string { return filepath.Join(UserPath(userName), repoName+".git") } +func UpdateRepository(repo *Repository) error { + _, err := orm.Id(repo.Id).UseBool().Update(repo) + return err +} + // DeleteRepository deletes a repository for a user or orgnaztion. func DeleteRepository(userId, repoId int64, userName string) (err error) { repo := &Repository{Id: repoId, OwnerId: userId} @@ -402,9 +407,9 @@ func DeleteRepository(userId, repoId int64, userName string) (err error) { } // GetRepositoryByName returns the repository by given name under user if exists. -func GetRepositoryByName(user *User, repoName string) (*Repository, error) { +func GetRepositoryByName(userId int64, repoName string) (*Repository, error) { repo := &Repository{ - OwnerId: user.Id, + OwnerId: userId, LowerName: strings.ToLower(repoName), } has, err := orm.Get(repo) diff --git a/models/user.go b/models/user.go index 3c11091285..d6dc041490 100644 --- a/models/user.go +++ b/models/user.go @@ -279,9 +279,7 @@ func GetUserByName(name string) (*User, error) { if len(name) == 0 { return nil, ErrUserNotExist } - user := &User{ - LowerName: strings.ToLower(name), - } + user := &User{LowerName: strings.ToLower(name)} has, err := orm.Get(user) if err != nil { return nil, err diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index a9a90e3ff5..3864caaf80 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -54,7 +54,7 @@ func RepoAssignment(redirect bool) martini.Handler { ctx.Repo.Owner = user // get repository - repo, err := models.GetRepositoryByName(user, params["reponame"]) + repo, err := models.GetRepositoryByName(user.Id, params["reponame"]) if err != nil { if redirect { ctx.Redirect("/") diff --git a/serve.go b/serve.go index 3ce8f9046c..be8dedc985 100644 --- a/serve.go +++ b/serve.go @@ -86,7 +86,7 @@ func runServ(*cli.Context) { os.Setenv("userName", user.Name) os.Setenv("userId", strconv.Itoa(int(user.Id))) - repo, err := models.GetRepositoryByName(user, repoName) + repo, err := models.GetRepositoryByName(user.Id, repoName) if err != nil { println("Unavilable repository", err) return From 76cd448e7925997b60a54e8d9431ffd0826cc24e Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 06:20:00 -0400 Subject: [PATCH 02/38] Add admin delete user --- gogs.go | 2 +- models/action.go | 6 ++++++ routers/admin/user.go | 35 +++++++++++++++++++++++++++++++++ templates/admin/users/edit.tmpl | 2 +- web.go | 1 + 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/gogs.go b/gogs.go index 41df79280a..8ec4fd42f1 100644 --- a/gogs.go +++ b/gogs.go @@ -20,7 +20,7 @@ import ( // Test that go1.2 tag above is included in builds. main.go refers to this definition. const go12tag = true -const APP_VER = "0.1.5.0321" +const APP_VER = "0.1.5.0322" func init() { base.AppVer = APP_VER diff --git a/models/action.go b/models/action.go index 89751b9779..12122ae240 100644 --- a/models/action.go +++ b/models/action.go @@ -7,6 +7,8 @@ package models import ( "encoding/json" "time" + + "github.com/gogits/gogs/modules/log" ) // Operation types of user action. @@ -89,6 +91,8 @@ func CommitRepoAction(userId int64, userName string, if err = UpdateRepository(repo); err != nil { return err } + + log.Trace("action.CommitRepoAction: %d/%s", userId, repo.LowerName) return nil } @@ -102,6 +106,8 @@ func NewRepoAction(user *User, repo *Repository) error { RepoId: repo.Id, RepoName: repo.Name, }) + + log.Trace("action.NewRepoAction: %s/%s", user.LowerName, repo.LowerName) return err } diff --git a/routers/admin/user.go b/routers/admin/user.go index d6f8523218..fa27d11664 100644 --- a/routers/admin/user.go +++ b/routers/admin/user.go @@ -107,3 +107,38 @@ func EditUser(ctx *middleware.Context, params martini.Params, form auth.AdminEdi log.Trace("%s User profile updated by admin(%s): %s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.User.LowerName) } + +func DeleteUser(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Edit Account" + ctx.Data["PageIsUsers"] = true + + uid, err := base.StrTo(params["userid"]).Int() + if err != nil { + ctx.Handle(200, "admin.user.EditUser", err) + return + } + + u, err := models.GetUserById(int64(uid)) + if err != nil { + ctx.Handle(200, "admin.user.EditUser", err) + return + } + + if err = models.DeleteUser(u); err != nil { + ctx.Data["HasError"] = true + switch err { + case models.ErrUserOwnRepos: + ctx.Data["ErrorMsg"] = "This account still has ownership of repository, owner has to delete or transfer them first." + ctx.Data["User"] = u + ctx.HTML(200, "admin/users/edit") + default: + ctx.Handle(200, "admin.user.DeleteUser", err) + } + return + } + + log.Trace("%s User deleted by admin(%s): %s", ctx.Req.RequestURI, + ctx.User.LowerName, ctx.User.LowerName) + + ctx.Redirect("/admin/users", 302) +} diff --git a/templates/admin/users/edit.tmpl b/templates/admin/users/edit.tmpl index 415bcedc92..2a9882423a 100644 --- a/templates/admin/users/edit.tmpl +++ b/templates/admin/users/edit.tmpl @@ -71,7 +71,7 @@ <div class="form-group"> <div class="col-md-offset-3 col-md-6"> <button type="submit" class="btn btn-lg btn-primary btn-block">Update account profile</button> - <!-- <a type="button" href="/admin/users/{{.User.Id}}/delete" class="btn btn-lg btn-danger btn-block">Delete this account</a> --> + <a type="button" href="/admin/users/{{.User.Id}}/delete" class="btn btn-lg btn-danger btn-block">Delete this account</a> </div> </div> </form> diff --git a/web.go b/web.go index bb316a6724..595b8f74ed 100644 --- a/web.go +++ b/web.go @@ -119,6 +119,7 @@ func runWeb(*cli.Context) { m.Get("/admin/users", reqSignIn, adminReq, admin.Users) m.Any("/admin/users/new", reqSignIn, adminReq, binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser) m.Any("/admin/users/:userid", reqSignIn, adminReq, binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser) + m.Any("/admin/users/:userid/delete", reqSignIn, adminReq, admin.DeleteUser) m.Get("/admin/repos", reqSignIn, adminReq, admin.Repositories) m.Get("/admin/config", reqSignIn, adminReq, admin.Config) From 7a1ff8636c01844a501dd9cdca2c436d1b7826b7 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 06:42:19 -0400 Subject: [PATCH 03/38] Add config option: Picture cache path --- conf/app.ini | 8 ++++++++ modules/base/conf.go | 8 ++++++++ routers/admin/admin.go | 3 +++ templates/admin/config.tmpl | 12 ++++++++++++ 4 files changed, 31 insertions(+) diff --git a/conf/app.ini b/conf/app.ini index ecb0d2511f..cf99c9da09 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -44,6 +44,8 @@ REGISTER_EMAIL_CONFIRM = false DISENABLE_REGISTERATION = false ; User must sign in to view anything. REQUIRE_SIGNIN_VIEW = false +; Cache avatar as picture +ENABLE_CACHE_AVATAR = false [mailer] ENABLED = false @@ -70,6 +72,12 @@ INTERVAL = 60 ; memcache: "127.0.0.1:11211" HOST = +[picture] +; The place to picture data, either "server" or "qiniu", default is "server" +SERVICE = server +; For "server" only, root path of picture data, default is "data/pictures" +PATH = data/pictures + [log] ; Either "console", "file", "conn" or "smtp", default is "console" MODE = console diff --git a/modules/base/conf.go b/modules/base/conf.go index 863daca644..8c6ee62818 100644 --- a/modules/base/conf.go +++ b/modules/base/conf.go @@ -44,6 +44,9 @@ var ( CacheAdapter string CacheConfig string + PictureService string + PictureRootPath string + LogMode string LogConfig string ) @@ -52,6 +55,7 @@ var Service struct { RegisterEmailConfirm bool DisenableRegisteration bool RequireSignInView bool + EnableCacheAvatar bool ActiveCodeLives int ResetPwdCodeLives int } @@ -82,6 +86,7 @@ func newService() { Service.ResetPwdCodeLives = Cfg.MustInt("service", "RESET_PASSWD_CODE_LIVE_MINUTES", 180) Service.DisenableRegisteration = Cfg.MustBool("service", "DISENABLE_REGISTERATION", false) Service.RequireSignInView = Cfg.MustBool("service", "REQUIRE_SIGNIN_VIEW", false) + Service.EnableCacheAvatar = Cfg.MustBool("service", "ENABLE_CACHE_AVATAR", false) } func newLogService() { @@ -214,6 +219,9 @@ func NewConfigContext() { SecretKey = Cfg.MustValue("security", "SECRET_KEY") RunUser = Cfg.MustValue("", "RUN_USER") + PictureService = Cfg.MustValue("picture", "SERVICE") + PictureRootPath = Cfg.MustValue("picture", "PATH") + // Determine and create root git reposiroty path. RepoRootPath = Cfg.MustValue("repository", "ROOT") if err = os.MkdirAll(RepoRootPath, os.ModePerm); err != nil { diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 2e19b99c10..25ed8981e0 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -70,6 +70,9 @@ func Config(ctx *middleware.Context) { ctx.Data["CacheAdapter"] = base.CacheAdapter ctx.Data["CacheConfig"] = base.CacheConfig + ctx.Data["PictureService"] = base.PictureService + ctx.Data["PictureRootPath"] = base.PictureRootPath + ctx.Data["LogMode"] = base.LogMode ctx.Data["LogConfig"] = base.LogConfig diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 6906f2409d..e3f69ee6ea 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -45,6 +45,7 @@ <div><b>Register Email Confirmation:</b> <i class="fa fa{{if .Service.RegisterEmailConfirm}}-check{{end}}-square-o"></i></div> <div><b>Disenable Registeration:</b> <i class="fa fa{{if .Service.DisenableRegisteration}}-check{{end}}-square-o"></i></div> <div><b>Require Sign In View:</b> <i class="fa fa{{if .Service.RequireSignInView}}-check{{end}}-square-o"></i></div> + <div><b>Enable Cache Avatar:</b> <i class="fa fa{{if .Service.EnableCacheAvatar}}-check{{end}}-square-o"></i></div> <hr/> <div><b>Active Code Lives:</b> {{.Service.ActiveCodeLives}} minutes</div> <div><b>Reset Password Code Lives:</b> {{.Service.ResetPwdCodeLives}} minutes</div> @@ -76,6 +77,17 @@ </div> </div> + <div class="panel panel-default"> + <div class="panel-heading"> + Picture Configuration + </div> + + <div class="panel-body"> + <div><b>Picture Service:</b> {{.PictureService}}</div> + <div><b>Picture Root Path:</b> {{.PictureRootPath}}</div> + </div> + </div> + <div class="panel panel-default"> <div class="panel-heading"> Log Configuration From 0d1872ebe3f11c14f31f454ed8d719a22c0884d0 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 07:42:24 -0400 Subject: [PATCH 04/38] Add admin memStatus panel --- README.md | 3 +- routers/admin/admin.go | 81 ++++++++++++++++++++++++++++++++++ templates/admin/dashboard.tmpl | 33 +++++++++++++- 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cbd1f588df..a9ab7fe498 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ There are two ways to install Gogs: ## Acknowledgments -- Mail service is based on [WeTalk](https://github.com/beego/wetalk). - Logo is inspired by [martini](https://github.com/martini-contrib). +- Mail service is based on [WeTalk](https://github.com/beego/wetalk). +- System Monitor Status is based on [GoBlog](https://github.com/fuxiaohei/goblog). ## Contributors diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 25ed8981e0..57a46d1dfe 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -5,7 +5,10 @@ package admin import ( + "fmt" + "runtime" "strings" + "time" "github.com/codegangsta/martini" @@ -14,10 +17,88 @@ import ( "github.com/gogits/gogs/modules/middleware" ) +var sysStatus struct { + NumGoroutine int + + // General statistics. + MemAllocated string // bytes allocated and still in use + MemTotal string // bytes allocated (even if freed) + MemSys string // bytes obtained from system (sum of XxxSys below) + Lookups uint64 // number of pointer lookups + MemMallocs uint64 // number of mallocs + MemFrees uint64 // number of frees + + // Main allocation heap statistics. + HeapAlloc string // bytes allocated and still in use + HeapSys string // bytes obtained from system + HeapIdle string // bytes in idle spans + HeapInuse string // bytes in non-idle span + HeapReleased string // bytes released to the OS + HeapObjects uint64 // total number of allocated objects + + // Low-level fixed-size structure allocator statistics. + // Inuse is bytes used now. + // Sys is bytes obtained from system. + StackInuse string // bootstrap stacks + StackSys string + MSpanInuse string // mspan structures + MSpanSys string + MCacheInuse string // mcache structures + MCacheSys string + BuckHashSys string // profiling bucket hash table + GCSys string // GC metadata + OtherSys string // other system allocations + + // Garbage collector statistics. + NextGC string // next run in HeapAlloc time (bytes) + LastGC string // last run in absolute time (ns) + PauseTotalNs string + PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256] + NumGC uint32 +} + +func updateSystemStatus() { + m := new(runtime.MemStats) + runtime.ReadMemStats(m) + sysStatus.NumGoroutine = runtime.NumGoroutine() + + sysStatus.MemAllocated = base.FileSize(int64(m.Alloc)) + sysStatus.MemTotal = base.FileSize(int64(m.TotalAlloc)) + sysStatus.MemSys = base.FileSize(int64(m.Sys)) + sysStatus.Lookups = m.Lookups + sysStatus.MemMallocs = m.Mallocs + sysStatus.MemFrees = m.Frees + + sysStatus.HeapAlloc = base.FileSize(int64(m.HeapAlloc)) + sysStatus.HeapSys = base.FileSize(int64(m.HeapSys)) + sysStatus.HeapIdle = base.FileSize(int64(m.HeapIdle)) + sysStatus.HeapInuse = base.FileSize(int64(m.HeapInuse)) + sysStatus.HeapReleased = base.FileSize(int64(m.HeapReleased)) + sysStatus.HeapObjects = m.HeapObjects + + sysStatus.StackInuse = base.FileSize(int64(m.StackInuse)) + sysStatus.StackSys = base.FileSize(int64(m.StackSys)) + sysStatus.MSpanInuse = base.FileSize(int64(m.MSpanInuse)) + sysStatus.MSpanSys = base.FileSize(int64(m.MSpanSys)) + sysStatus.MCacheInuse = base.FileSize(int64(m.MCacheInuse)) + sysStatus.MCacheSys = base.FileSize(int64(m.MCacheSys)) + sysStatus.BuckHashSys = base.FileSize(int64(m.BuckHashSys)) + sysStatus.GCSys = base.FileSize(int64(m.GCSys)) + sysStatus.OtherSys = base.FileSize(int64(m.OtherSys)) + + sysStatus.NextGC = base.FileSize(int64(m.NextGC)) + sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000) + sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs/1000/1000/1000)) + sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256]/1000/1000/1000)) + sysStatus.NumGC = m.NumGC +} + func Dashboard(ctx *middleware.Context) { ctx.Data["Title"] = "Admin Dashboard" ctx.Data["PageIsDashboard"] = true ctx.Data["Stats"] = models.GetStatistic() + updateSystemStatus() + ctx.Data["SysStatus"] = sysStatus ctx.HTML(200, "admin/dashboard") } diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 6088487d62..0bebf8318f 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -15,10 +15,41 @@ <div class="panel panel-default"> <div class="panel-heading"> - System Status + System Monitor Status </div> <div class="panel-body"> + <div>Current Goroutines: <b>{{.SysStatus.NumGoroutine}}</b></div> + <hr/> + <div>Current Memory Usage: <b>{{.SysStatus.MemAllocated}}</b></div> + <div>Total Memory Allocated: <b>{{.SysStatus.MemTotal}}</b></div> + <div>Memory Obtained: <b>{{.SysStatus.MemSys}}</b></div> + <div>Pointer Lookup Times: <b>{{.SysStatus.Lookups}}</b></div> + <div>Memory Allocate Times: <b>{{.SysStatus.MemMallocs}}</b></div> + <div>Memory Free Times: <b>{{.SysStatus.MemFrees}}</b></div> + <hr/> + <div>Current Heap Usage: <b>{{.SysStatus.HeapAlloc}}</b></div> + <div>Heap Memory Obtained: <b>{{.SysStatus.HeapSys}}</b></div> + <div>Heap Memory Idle: <b>{{.SysStatus.HeapIdle}}</b></div> + <div>Heap Memory In Use: <b>{{.SysStatus.HeapInuse}}</b></div> + <div>Heap Memory Released: <b>{{.SysStatus.HeapReleased}}</b></div> + <div>Heap Objects: <b>{{.SysStatus.HeapObjects}}</b></div> + <hr/> + <div>Bootstrap Stack Usage: <b>{{.SysStatus.StackInuse}}</b></div> + <div>Stack Memory Obtained: <b>{{.SysStatus.StackSys}}</b></div> + <div>MSpan Structures Usage: <b>{{.SysStatus.MSpanInuse}}</b></div> + <div>MSpan Structures Obtained: <b>{{.SysStatus.HeapSys}}</b></div> + <div>MCache Structures Usage: <b>{{.SysStatus.MCacheInuse}}</b></div> + <div>MCache Structures Obtained: <b>{{.SysStatus.MCacheSys}}</b></div> + <div>Profiling Bucket Hash Table Obtained: <b>{{.SysStatus.BuckHashSys}}</b></div> + <div>GC Metadada Obtained: <b>{{.SysStatus.GCSys}}</b></div> + <div>Other System Allocation Obtained: <b>{{.SysStatus.OtherSys}}</b></div> + <hr/> + <div>Next GC Recycle: <b>{{.SysStatus.NextGC}}</b></div> + <div>Last GC Time: <b>{{.SysStatus.LastGC}} ago</b></div> + <div>Total GC Pause: <b>{{.SysStatus.PauseTotalNs}}</b></div> + <div>Last GC Pause: <b>{{.SysStatus.PauseNs}}</b></div> + <div>GC Times: <b>{{.SysStatus.NumGC}}</b></div> </div> </div> </div> From f9c07c4186b61a1548d9a908fe6228bd130f4f92 Mon Sep 17 00:00:00 2001 From: slene <vslene@gmail.com> Date: Sat, 22 Mar 2014 20:49:53 +0800 Subject: [PATCH 05/38] update session --- .gitignore | 1 + conf/app.ini | 27 +++++++++++++++++++++++++++ modules/auth/user.go | 11 ++++++----- modules/base/conf.go | 30 ++++++++++++++++++++++++++++++ modules/middleware/context.go | 24 ++++++++++++++---------- routers/user/user.go | 2 +- web.go | 5 ----- 7 files changed, 79 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index ad27cc8be8..d201223ef9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ gogs *.db *.log custom/ +data/ .vendor/ .idea/ *.iml \ No newline at end of file diff --git a/conf/app.ini b/conf/app.ini index cf99c9da09..cf2ae31d83 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -72,6 +72,33 @@ INTERVAL = 60 ; memcache: "127.0.0.1:11211" HOST = +[session] +; Either "memory", "file", "redis" or "mysql", default is "memory" +PROVIDER = file +; provider config +; memory: not have any config yet +; file: session file path +; e.g. tmp/sessions +; redis: config like redis server addr,poolSize,password +; e.g. 127.0.0.1:6379,100,astaxie +; mysql: go-sql-driver/mysql dsn config string +; e.g. root:password@/session_table +PROVIDER_CONFIG = data/sessions +; session cookie name +COOKIE_NAME = i_like_gogits +; if you use session in https only, default is false +COOKIE_SECURE = false +; enable set cookie, default is true +ENABLE_SET_COOKIE = true +; session gc time interval, default is 86400 +GC_INTERVAL_TIME = 86400 +; session life time, default is 86400 +SESSION_LIFE_TIME = 86400 +; session id hash func, default is sha1 +SESSION_ID_HASHFUNC = sha1 +; session hash key, default is use random string +SESSION_ID_HASHKEY = + [picture] ; The place to picture data, either "server" or "qiniu", default is "server" SERVICE = server diff --git a/modules/auth/user.go b/modules/auth/user.go index f8d8f66149..cb8db1b29a 100644 --- a/modules/auth/user.go +++ b/modules/auth/user.go @@ -9,7 +9,8 @@ import ( "reflect" "github.com/codegangsta/martini" - "github.com/martini-contrib/sessions" + + "github.com/gogits/session" "github.com/gogits/binding" @@ -19,7 +20,7 @@ import ( ) // SignedInId returns the id of signed in user. -func SignedInId(session sessions.Session) int64 { +func SignedInId(session session.SessionStore) int64 { userId := session.Get("userId") if userId == nil { return 0 @@ -34,7 +35,7 @@ func SignedInId(session sessions.Session) int64 { } // SignedInName returns the name of signed in user. -func SignedInName(session sessions.Session) string { +func SignedInName(session session.SessionStore) string { userName := session.Get("userName") if userName == nil { return "" @@ -46,7 +47,7 @@ func SignedInName(session sessions.Session) string { } // SignedInUser returns the user object of signed user. -func SignedInUser(session sessions.Session) *models.User { +func SignedInUser(session session.SessionStore) *models.User { id := SignedInId(session) if id <= 0 { return nil @@ -61,7 +62,7 @@ func SignedInUser(session sessions.Session) *models.User { } // IsSignedIn check if any user has signed in. -func IsSignedIn(session sessions.Session) bool { +func IsSignedIn(session session.SessionStore) bool { return SignedInId(session) > 0 } diff --git a/modules/base/conf.go b/modules/base/conf.go index 8c6ee62818..d5e27d043b 100644 --- a/modules/base/conf.go +++ b/modules/base/conf.go @@ -16,6 +16,7 @@ import ( "github.com/Unknwon/goconfig" "github.com/gogits/cache" + "github.com/gogits/session" "github.com/gogits/gogs/modules/log" ) @@ -49,6 +50,10 @@ var ( LogMode string LogConfig string + + SessionProvider string + SessionConfig *session.Config + SessionManager *session.Manager ) var Service struct { @@ -164,6 +169,30 @@ func newCacheService() { log.Info("Cache Service Enabled") } +func newSessionService() { + SessionProvider = Cfg.MustValue("session", "PROVIDER", "memory") + + SessionConfig = new(session.Config) + SessionConfig.ProviderConfig = Cfg.MustValue("session", "PROVIDER_CONFIG") + SessionConfig.CookieName = Cfg.MustValue("session", "COOKIE_NAME", "i_like_gogits") + SessionConfig.CookieSecure = Cfg.MustBool("session", "COOKIE_SECURE") + SessionConfig.EnableSetCookie = Cfg.MustBool("session", "ENABLE_SET_COOKIE", true) + SessionConfig.GcIntervalTime = Cfg.MustInt64("session", "GC_INTERVAL_TIME", 86400) + SessionConfig.SessionLifeTime = Cfg.MustInt64("session", "SESSION_LIFE_TIME", 86400) + SessionConfig.SessionIDHashFunc = Cfg.MustValue("session", "SESSION_ID_HASHFUNC", "sha1") + SessionConfig.SessionIDHashKey = Cfg.MustValue("session", "SESSION_ID_HASHKEY") + + var err error + SessionManager, err = session.NewManager(SessionProvider, *SessionConfig) + if err != nil { + fmt.Printf("Init session system failed, provider: %s, %v\n", + SessionProvider, err) + os.Exit(2) + } + + log.Info("Session Service Enabled") +} + func newMailService() { // Check mailer setting. if Cfg.MustBool("mailer", "ENABLED") { @@ -234,6 +263,7 @@ func NewServices() { newService() newLogService() newCacheService() + newSessionService() newMailService() newRegisterMailService() } diff --git a/modules/middleware/context.go b/modules/middleware/context.go index a25a3dbbeb..c958c1d6cd 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -10,9 +10,9 @@ import ( "time" "github.com/codegangsta/martini" - "github.com/martini-contrib/sessions" "github.com/gogits/cache" + "github.com/gogits/session" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" @@ -27,7 +27,7 @@ type Context struct { p martini.Params Req *http.Request Res http.ResponseWriter - Session sessions.Session + Session session.SessionStore Cache cache.Cache User *models.User IsSigned bool @@ -92,21 +92,25 @@ func (ctx *Context) Handle(status int, title string, err error) { // InitContext initializes a classic context for a request. func InitContext() martini.Handler { - return func(res http.ResponseWriter, r *http.Request, c martini.Context, - session sessions.Session, rd *Render) { + return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) { ctx := &Context{ c: c, // p: p, - Req: r, - Res: res, - Session: session, - Cache: base.Cache, - Render: rd, + Req: r, + Res: res, + Cache: base.Cache, + Render: rd, } + // start session + ctx.Session = base.SessionManager.SessionStart(res, r) + defer func() { + ctx.Session.SessionRelease(res) + }() + // Get user from session if logined. - user := auth.SignedInUser(session) + user := auth.SignedInUser(ctx.Session) ctx.User = user ctx.IsSigned = user != nil diff --git a/routers/user/user.go b/routers/user/user.go index d38eb1ceb3..2244697714 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -88,7 +88,7 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { user, err := models.LoginUserPlain(form.UserName, form.Password) if err != nil { - if err.Error() == models.ErrUserNotExist.Error() { + if err == models.ErrUserNotExist { ctx.RenderWithErr("Username or password is not correct", "user/signin", &form) return } diff --git a/web.go b/web.go index 595b8f74ed..ac5761d720 100644 --- a/web.go +++ b/web.go @@ -12,7 +12,6 @@ import ( "github.com/codegangsta/cli" "github.com/codegangsta/martini" - "github.com/martini-contrib/sessions" "github.com/gogits/binding" @@ -81,10 +80,6 @@ func runWeb(*cli.Context) { // Middlewares. m.Use(middleware.Renderer(middleware.RenderOptions{Funcs: []template.FuncMap{base.TemplateFuncs}})) - // TODO: should use other store because cookie store is not secure. - store := sessions.NewCookieStore([]byte("secret123")) - m.Use(sessions.Sessions("my_session", store)) - m.Use(middleware.InitContext()) reqSignIn := middleware.SignInRequire(true) From 01e781dedb3c6d48349516de0eee5cea41c077e1 Mon Sep 17 00:00:00 2001 From: slene <vslene@gmail.com> Date: Sat, 22 Mar 2014 21:19:27 +0800 Subject: [PATCH 06/38] add comment --- conf/app.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/app.ini b/conf/app.ini index cf2ae31d83..6b3ce8d240 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -94,7 +94,7 @@ ENABLE_SET_COOKIE = true GC_INTERVAL_TIME = 86400 ; session life time, default is 86400 SESSION_LIFE_TIME = 86400 -; session id hash func, default is sha1 +; session id hash func, Either "sha1", "sha256" or "md5" default is sha1 SESSION_ID_HASHFUNC = sha1 ; session hash key, default is use random string SESSION_ID_HASHKEY = From fd1831052c3a79492643b89512282fc66f34dd8d Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 09:21:57 -0400 Subject: [PATCH 07/38] Update session --- .gopmfile | 1 - README.md | 2 +- conf/app.ini | 25 +++++----- gogs.go | 2 +- modules/base/conf.go | 16 ++++--- modules/base/tool.go | 84 +++++++++++++++++++++++++++++++++- routers/admin/admin.go | 12 ++++- templates/admin/config.tmpl | 19 ++++++++ templates/admin/dashboard.tmpl | 1 + 9 files changed, 135 insertions(+), 27 deletions(-) diff --git a/.gopmfile b/.gopmfile index 5b690a06a7..6e6b59c620 100644 --- a/.gopmfile +++ b/.gopmfile @@ -4,7 +4,6 @@ path=github.com/gogits/gogs [deps] github.com/codegangsta/cli= github.com/codegangsta/martini= -github.com/martini-contrib/sessions= github.com/Unknwon/com= github.com/Unknwon/cae= github.com/Unknwon/goconfig= diff --git a/README.md b/README.md index a9ab7fe498..35044927ff 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ There are two ways to install Gogs: ## Acknowledgments - Logo is inspired by [martini](https://github.com/martini-contrib). -- Mail service is based on [WeTalk](https://github.com/beego/wetalk). +- Mail Service is based on [WeTalk](https://github.com/beego/wetalk). - System Monitor Status is based on [GoBlog](https://github.com/fuxiaohei/goblog). ## Contributors diff --git a/conf/app.ini b/conf/app.ini index cf2ae31d83..30d6c7d483 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -75,28 +75,25 @@ HOST = [session] ; Either "memory", "file", "redis" or "mysql", default is "memory" PROVIDER = file -; provider config +; Provider config options ; memory: not have any config yet -; file: session file path -; e.g. tmp/sessions -; redis: config like redis server addr,poolSize,password -; e.g. 127.0.0.1:6379,100,astaxie -; mysql: go-sql-driver/mysql dsn config string -; e.g. root:password@/session_table +; file: session file path, e.g. data/sessions +; redis: config like redis server addr, poolSize, password, e.g. 127.0.0.1:6379,100,astaxie +; mysql: go-sql-driver/mysql dsn config string, e.g. root:password@/session_table PROVIDER_CONFIG = data/sessions -; session cookie name +; Session cookie name COOKIE_NAME = i_like_gogits -; if you use session in https only, default is false +; If you use session in https only, default is false COOKIE_SECURE = false -; enable set cookie, default is true +; Enable set cookie, default is true ENABLE_SET_COOKIE = true -; session gc time interval, default is 86400 +; Session GC time interval, default is 86400 GC_INTERVAL_TIME = 86400 -; session life time, default is 86400 +; Session life time, default is 86400 SESSION_LIFE_TIME = 86400 -; session id hash func, default is sha1 +; Session id hash func, default is sha1 SESSION_ID_HASHFUNC = sha1 -; session hash key, default is use random string +; Session hash key, default is use random string SESSION_ID_HASHKEY = [picture] diff --git a/gogs.go b/gogs.go index 8ec4fd42f1..a609032093 100644 --- a/gogs.go +++ b/gogs.go @@ -20,7 +20,7 @@ import ( // Test that go1.2 tag above is included in builds. main.go refers to this definition. const go12tag = true -const APP_VER = "0.1.5.0322" +const APP_VER = "0.1.5.0322.2" func init() { base.AppVer = APP_VER diff --git a/modules/base/conf.go b/modules/base/conf.go index d5e27d043b..7c8ed93654 100644 --- a/modules/base/conf.go +++ b/modules/base/conf.go @@ -41,19 +41,19 @@ var ( Cfg *goconfig.ConfigFile MailService *Mailer + LogMode string + LogConfig string + Cache cache.Cache CacheAdapter string CacheConfig string - PictureService string - PictureRootPath string - - LogMode string - LogConfig string - SessionProvider string SessionConfig *session.Config SessionManager *session.Manager + + PictureService string + PictureRootPath string ) var Service struct { @@ -182,6 +182,10 @@ func newSessionService() { SessionConfig.SessionIDHashFunc = Cfg.MustValue("session", "SESSION_ID_HASHFUNC", "sha1") SessionConfig.SessionIDHashKey = Cfg.MustValue("session", "SESSION_ID_HASHKEY") + if SessionProvider == "file" { + os.MkdirAll(path.Dir(SessionConfig.ProviderConfig), os.ModePerm) + } + var err error SessionManager, err = session.NewManager(SessionProvider, *SessionConfig) if err != nil { diff --git a/modules/base/tool.go b/modules/base/tool.go index 8fabb8c531..4f368aa58c 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -111,6 +111,85 @@ const ( Year = 12 * Month ) +func computeTimeDiff(diff int64) (int64, string) { + diffStr := "" + switch { + case diff <= 0: + diff = 0 + diffStr = "now" + case diff < 2: + diff = 0 + diffStr = "1 second" + case diff < 1*Minute: + diffStr = fmt.Sprintf("%d seconds", diff) + diff = 0 + + case diff < 2*Minute: + diff -= 1 * Minute + diffStr = "1 minute" + case diff < 1*Hour: + diffStr = fmt.Sprintf("%d minutes", diff/Minute) + diff -= diff / Minute * Minute + + case diff < 2*Hour: + diff -= 1 * Hour + diffStr = "1 hour" + case diff < 1*Day: + diffStr = fmt.Sprintf("%d hours", diff/Hour) + diff -= diff / Hour * Hour + + case diff < 2*Day: + diff -= 1 * Day + diffStr = "1 day" + case diff < 1*Week: + diffStr = fmt.Sprintf("%d days", diff/Day) + diff -= diff / Day * Day + + case diff < 2*Week: + diff -= 1 * Week + diffStr = "1 week" + case diff < 1*Month: + diffStr = fmt.Sprintf("%d weeks", diff/Week) + diff -= diff / Week * Week + + case diff < 2*Month: + diff -= 1 * Month + diffStr = "1 month" + case diff < 1*Year: + diffStr = fmt.Sprintf("%d months", diff/Month) + diff -= diff / Month * Month + + case diff < 2*Year: + diff -= 1 * Year + diffStr = "1 year" + default: + diffStr = fmt.Sprintf("%d years", diff/Year) + diff = 0 + } + return diff, diffStr +} + +// TimeSincePro calculates the time interval and generate full user-friendly string. +func TimeSincePro(then time.Time) string { + now := time.Now() + diff := now.Unix() - then.Unix() + + if then.After(now) { + return "future" + } + + var timeStr, diffStr string + for { + if diff == 0 { + break + } + + diff, diffStr = computeTimeDiff(diff) + timeStr += ", " + diffStr + } + return strings.TrimPrefix(timeStr, ", ") +} + // TimeSince calculates the time interval and generate user-friendly string. func TimeSince(then time.Time) string { now := time.Now() @@ -123,7 +202,6 @@ func TimeSince(then time.Time) string { } switch { - case diff <= 0: return "now" case diff <= 2: @@ -156,8 +234,10 @@ func TimeSince(then time.Time) string { case diff < 1*Year: return fmt.Sprintf("%d months %s", diff/Month, lbl) - case diff < 18*Month: + case diff < 2*Year: return fmt.Sprintf("1 year %s", lbl) + default: + return fmt.Sprintf("%d years %s", diff/Year, lbl) } return then.String() } diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 57a46d1dfe..c0f39f7159 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -17,7 +17,10 @@ import ( "github.com/gogits/gogs/modules/middleware" ) +var startTime = time.Now() + var sysStatus struct { + Uptime string NumGoroutine int // General statistics. @@ -58,6 +61,8 @@ var sysStatus struct { } func updateSystemStatus() { + sysStatus.Uptime = base.TimeSincePro(startTime) + m := new(runtime.MemStats) runtime.ReadMemStats(m) sysStatus.NumGoroutine = runtime.NumGoroutine() @@ -88,8 +93,8 @@ func updateSystemStatus() { sysStatus.NextGC = base.FileSize(int64(m.NextGC)) sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000) - sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs/1000/1000/1000)) - sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256]/1000/1000/1000)) + sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000) + sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000) sysStatus.NumGC = m.NumGC } @@ -151,6 +156,9 @@ func Config(ctx *middleware.Context) { ctx.Data["CacheAdapter"] = base.CacheAdapter ctx.Data["CacheConfig"] = base.CacheConfig + ctx.Data["SessionProvider"] = base.SessionProvider + ctx.Data["SessionConfig"] = base.SessionConfig + ctx.Data["PictureService"] = base.PictureService ctx.Data["PictureRootPath"] = base.PictureRootPath diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index e3f69ee6ea..048740e617 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -77,6 +77,25 @@ </div> </div> + <div class="panel panel-default"> + <div class="panel-heading"> + Session Configuration + </div> + + <div class="panel-body"> + <div><b>Session Provider:</b> {{.SessionProvider}}</div> + <div><b>Cookie Name:</b> {{.SessionConfig.CookieName}}</div> + <div><b>Enable Set Cookie:</b> <i class="fa fa{{if .SessionConfig.EnableSetCookie}}-check{{end}}-square-o"></i></div> + <div><b>GC Interval Time:</b> {{.SessionConfig.GcIntervalTime}} seconds</div> + <div><b>Session Life Time:</b> {{.SessionConfig.SessionLifeTime}} seconds</div> + <div><b>HTTPS Only:</b> <i class="fa fa{{if .SessionConfig.CookieSecure}}-check{{end}}-square-o"></i></div> + <div><b>Cookie Life Time:</b> {{.SessionConfig.CookieLifeTime}} seconds</div> + <div><b>Session ID Hash Function:</b> {{.SessionConfig.SessionIDHashFunc}}</div> + <div><b>Session ID Hash Key:</b> {{.SessionConfig.SessionIDHashKey}}</div> + <div><b>Provider Config:</b> {{.SessionConfig.ProviderConfig}}</div> + </div> + </div> + <div class="panel panel-default"> <div class="panel-heading"> Picture Configuration diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 0bebf8318f..2a5a161e03 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -19,6 +19,7 @@ </div> <div class="panel-body"> + <div>Server Uptime: <b>{{.SysStatus.Uptime}}</b></div> <div>Current Goroutines: <b>{{.SysStatus.NumGoroutine}}</b></div> <hr/> <div>Current Memory Usage: <b>{{.SysStatus.MemAllocated}}</b></div> From e3f55ca0fb0c8aee84f2935b76353ef8ce66384f Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 11:59:14 -0400 Subject: [PATCH 08/38] Need a field to specify if repository is bare --- models/action.go | 1 + models/repo.go | 6 ++++-- routers/repo/single.go | 10 ++-------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/models/action.go b/models/action.go index 12122ae240..4e1107f891 100644 --- a/models/action.go +++ b/models/action.go @@ -87,6 +87,7 @@ func CommitRepoAction(userId int64, userName string, if err != nil { return err } + repo.IsBare = false repo.Updated = time.Now() if err = UpdateRepository(repo); err != nil { return err diff --git a/models/repo.go b/models/repo.go index 1961b31e94..fb115de590 100644 --- a/models/repo.go +++ b/models/repo.go @@ -83,10 +83,11 @@ type Repository struct { Name string `xorm:"index not null"` Description string Website string - Private bool NumWatches int NumStars int NumForks int + IsPrivate bool + IsBare bool Created time.Time `xorm:"created"` Updated time.Time `xorm:"updated"` } @@ -139,7 +140,8 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv Name: repoName, LowerName: strings.ToLower(repoName), Description: desc, - Private: private, + IsPrivate: private, + IsBare: repoLang == "" && license == "" && !initReadme, } repoPath := RepoPath(user.Name, repoName) diff --git a/routers/repo/single.go b/routers/repo/single.go index 37c0fabd79..5906e64fb9 100644 --- a/routers/repo/single.go +++ b/routers/repo/single.go @@ -69,7 +69,7 @@ func Single(ctx *middleware.Context, params martini.Params) { log.Error("repo.Single(GetBranches): %v", err) ctx.Error(404) return - } else if len(brs) == 0 { + } else if ctx.Repo.Repository.IsBare { ctx.Data["IsBareRepo"] = true ctx.HTML(200, "repo/single") return @@ -224,13 +224,7 @@ func Setting(ctx *middleware.Context, params martini.Params) { ctx.Data["IsRepoToolbarSetting"] = true - // Branches. - brs, err := models.GetBranches(params["username"], params["reponame"]) - if err != nil { - log.Error("repo.Setting(GetBranches): %v", err) - ctx.Error(404) - return - } else if len(brs) == 0 { + if ctx.Repo.Repository.IsBare { ctx.Data["IsBareRepo"] = true ctx.HTML(200, "repo/setting") return From 076fc98d981aea3533eea363ca1c7e43f77b9802 Mon Sep 17 00:00:00 2001 From: slene <vslene@gmail.com> Date: Sun, 23 Mar 2014 01:44:02 +0800 Subject: [PATCH 09/38] add csrf check --- modules/base/tool.go | 10 ++- modules/middleware/auth.go | 58 +++++++++-------- modules/middleware/context.go | 107 +++++++++++++++++++++++++++++++- modules/middleware/render.go | 5 +- public/js/app.js | 33 ++++++++++ templates/admin/users/edit.tmpl | 1 + templates/admin/users/new.tmpl | 1 + templates/base/head.tmpl | 1 + templates/repo/create.tmpl | 1 + templates/repo/setting.tmpl | 1 + templates/user/active.tmpl | 3 +- templates/user/delete.tmpl | 1 + templates/user/password.tmpl | 4 +- templates/user/publickey.tmpl | 1 + templates/user/setting.tmpl | 1 + templates/user/signin.tmpl | 1 + templates/user/signup.tmpl | 1 + web.go | 24 +++---- 18 files changed, 208 insertions(+), 46 deletions(-) diff --git a/modules/base/tool.go b/modules/base/tool.go index 8fabb8c531..a2aeebf1b8 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -25,13 +25,17 @@ func EncodeMd5(str string) string { return hex.EncodeToString(m.Sum(nil)) } -// Random generate string -func GetRandomString(n int) string { +// GetRandomString generate random string by specify chars. +func GetRandomString(n int, alphabets ...byte) string { const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" var bytes = make([]byte, n) rand.Read(bytes) for i, b := range bytes { - bytes[i] = alphanum[b%byte(len(alphanum))] + if len(alphabets) == 0 { + bytes[i] = alphanum[b%byte(len(alphanum))] + } else { + bytes[i] = alphabets[b%byte(len(alphabets))] + } } return string(bytes) } diff --git a/modules/middleware/auth.go b/modules/middleware/auth.go index f211de32b9..b557188ee9 100644 --- a/modules/middleware/auth.go +++ b/modules/middleware/auth.go @@ -10,39 +10,45 @@ import ( "github.com/gogits/gogs/modules/base" ) -// SignInRequire requires user to sign in. -func SignInRequire(redirect bool) martini.Handler { - return func(ctx *Context) { - if !ctx.IsSigned { - if redirect { - ctx.Redirect("/user/login") - } - return - } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { - ctx.Data["Title"] = "Activate Your Account" - ctx.HTML(200, "user/active") - return - } - } +type ToggleOptions struct { + SignInRequire bool + SignOutRequire bool + AdminRequire bool + DisableCsrf bool } -// SignOutRequire requires user to sign out. -func SignOutRequire() martini.Handler { +func Toggle(options *ToggleOptions) martini.Handler { return func(ctx *Context) { - if ctx.IsSigned { + if options.SignOutRequire && ctx.IsSigned { ctx.Redirect("/") return } - } -} -// AdminRequire requires user signed in as administor. -func AdminRequire() martini.Handler { - return func(ctx *Context) { - if !ctx.User.IsAdmin { - ctx.Error(403) - return + if !options.DisableCsrf { + if ctx.Req.Method == "POST" { + if !ctx.CsrfTokenValid() { + ctx.Error(403, "CSRF token does not match") + return + } + } + } + + if options.SignInRequire { + if !ctx.IsSigned { + ctx.Redirect("/user/login") + return + } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { + ctx.Data["Title"] = "Activate Your Account" + ctx.HTML(200, "user/active") + return + } + } + + if options.AdminRequire { + if !ctx.User.IsAdmin { + ctx.Error(403) + return + } } - ctx.Data["PageIsAdmin"] = true } } diff --git a/modules/middleware/context.go b/modules/middleware/context.go index c958c1d6cd..b28953fc0e 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -6,6 +6,7 @@ package middleware import ( "fmt" + "html/template" "net/http" "time" @@ -32,6 +33,8 @@ type Context struct { User *models.User IsSigned bool + csrfToken string + Repo struct { IsValid bool IsOwner bool @@ -90,6 +93,95 @@ func (ctx *Context) Handle(status int, title string, err error) { ctx.HTML(status, fmt.Sprintf("status/%d", status)) } +func (ctx *Context) GetCookie(name string) string { + cookie, err := ctx.Req.Cookie(name) + if err != nil { + return "" + } + return cookie.Value +} + +func (ctx *Context) SetCookie(name string, value string, others ...interface{}) { + cookie := http.Cookie{} + cookie.Name = name + cookie.Value = value + + if len(others) > 0 { + switch v := others[0].(type) { + case int: + cookie.MaxAge = v + case int64: + cookie.MaxAge = int(v) + case int32: + cookie.MaxAge = int(v) + } + } + + // default "/" + if len(others) > 1 { + if v, ok := others[1].(string); ok && len(v) > 0 { + cookie.Path = v + } + } else { + cookie.Path = "/" + } + + // default empty + if len(others) > 2 { + if v, ok := others[2].(string); ok && len(v) > 0 { + cookie.Domain = v + } + } + + // default empty + if len(others) > 3 { + switch v := others[3].(type) { + case bool: + cookie.Secure = v + default: + if others[3] != nil { + cookie.Secure = true + } + } + } + + // default false. for session cookie default true + if len(others) > 4 { + if v, ok := others[4].(bool); ok && v { + cookie.HttpOnly = true + } + } + + ctx.Res.Header().Add("Set-Cookie", cookie.String()) +} + +func (ctx *Context) CsrfToken() string { + if len(ctx.csrfToken) > 0 { + return ctx.csrfToken + } + + token := ctx.GetCookie("_csrf") + if len(token) == 0 { + token = base.GetRandomString(30) + ctx.SetCookie("_csrf", token) + } + ctx.csrfToken = token + return token +} + +func (ctx *Context) CsrfTokenValid() bool { + token := ctx.Query("_csrf") + if token == "" { + token = ctx.Req.Header.Get("X-Csrf-Token") + } + if token == "" { + return false + } else if ctx.csrfToken != token { + return false + } + return true +} + // InitContext initializes a classic context for a request. func InitContext() martini.Handler { return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) { @@ -103,11 +195,14 @@ func InitContext() martini.Handler { Render: rd, } + ctx.Data["PageStartTime"] = time.Now() + // start session ctx.Session = base.SessionManager.SessionStart(res, r) - defer func() { + rw := res.(martini.ResponseWriter) + rw.Before(func(martini.ResponseWriter) { ctx.Session.SessionRelease(res) - }() + }) // Get user from session if logined. user := auth.SignedInUser(ctx.Session) @@ -121,9 +216,15 @@ func InitContext() martini.Handler { ctx.Data["SignedUserId"] = user.Id ctx.Data["SignedUserName"] = user.LowerName ctx.Data["IsAdmin"] = ctx.User.IsAdmin + + if ctx.User.IsAdmin { + ctx.Data["PageIsAdmin"] = true + } } - ctx.Data["PageStartTime"] = time.Now() + // get or create csrf token + ctx.Data["CsrfToken"] = ctx.CsrfToken() + ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.csrfToken + `">`) c.Map(ctx) diff --git a/modules/middleware/render.go b/modules/middleware/render.go index 8a54183135..869ef9abaa 100644 --- a/modules/middleware/render.go +++ b/modules/middleware/render.go @@ -242,8 +242,11 @@ func (r *Render) HTMLString(name string, binding interface{}, htmlOpt ...HTMLOpt } } -func (r *Render) Error(status int) { +func (r *Render) Error(status int, message ...string) { r.WriteHeader(status) + if len(message) > 0 { + r.Write([]byte(message[0])) + } } func (r *Render) Redirect(location string, status ...int) { diff --git a/public/js/app.js b/public/js/app.js index f179342f4b..df755727b5 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -2,6 +2,39 @@ var Gogits = { "PageIsSignup": false }; +(function($){ + // extend jQuery ajax, set csrf token value + var ajax = $.ajax; + $.extend({ + ajax: function(url, options) { + if (typeof url === 'object') { + options = url; + url = undefined; + } + options = options || {}; + url = options.url; + var csrftoken = $('meta[name=_csrf]').attr('content'); + var headers = options.headers || {}; + var domain = document.domain.replace(/\./ig, '\\.'); + if (!/^(http:|https:).*/.test(url) || eval('/^(http:|https:)\\/\\/(.+\\.)*' + domain + '.*/').test(url)) { + headers = $.extend(headers, {'X-Csrf-Token':csrftoken}); + } + options.headers = headers; + var callback = options.success; + options.success = function(data){ + if(data.once){ + // change all _once value if ajax data.once exist + $('[name=_once]').val(data.once); + } + if(callback){ + callback.apply(this, arguments); + } + }; + return ajax(url, options); + } + }); +}(jQuery)); + (function ($) { Gogits.showTab = function (selector, index) { diff --git a/templates/admin/users/edit.tmpl b/templates/admin/users/edit.tmpl index 2a9882423a..08f11fcb12 100644 --- a/templates/admin/users/edit.tmpl +++ b/templates/admin/users/edit.tmpl @@ -12,6 +12,7 @@ <br/> <form action="/admin/users/{{.User.Id}}" method="post" class="form-horizontal"> {{if .IsSuccess}}<p class="alert alert-success">Account profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}} + {{.CsrfTokenHtml}} <input type="hidden" value="{{.User.Id}}" name="userId"/> <div class="form-group"> <label class="col-md-3 control-label">Username: </label> diff --git a/templates/admin/users/new.tmpl b/templates/admin/users/new.tmpl index 01d976caa0..7b41ae43a7 100644 --- a/templates/admin/users/new.tmpl +++ b/templates/admin/users/new.tmpl @@ -11,6 +11,7 @@ <div class="panel-body"> <br/> <form action="/admin/users/new" method="post" class="form-horizontal"> + {{.CsrfTokenHtml}} <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div> <div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}"> <label class="col-md-3 control-label">Username: </label> diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index f02ea095ca..7f56ed7080 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -8,6 +8,7 @@ <meta name="author" content="Gogs - Go Git Service" /> <meta name="description" content="Gogs(Go Git Service) is a GitHub-like clone in the Go Programming Language" /> <meta name="keywords" content="go, git"> + <meta name="_csrf" content="{{.CsrfToken}}" /> <!-- Stylesheets --> <link href="/css/bootstrap.min.css" rel="stylesheet" /> diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 2de92f515f..a43f510484 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -2,6 +2,7 @@ {{template "base/navbar" .}} <div class="container" id="gogs-body"> <form action="/repo/create" method="post" class="form-horizontal gogs-card" id="gogs-repo-create"> + {{.CsrfTokenHtml}} <h3>Create New Repository</h3> <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div> <div class="form-group"> diff --git a/templates/repo/setting.tmpl b/templates/repo/setting.tmpl index a2fb1771d4..38c3fd3bcc 100644 --- a/templates/repo/setting.tmpl +++ b/templates/repo/setting.tmpl @@ -40,6 +40,7 @@ <div class="modal fade" id="delete-repository-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal-dialog"> <form action="/{{.Owner.Name}}/{{.Repository.Name}}/settings" method="post" class="modal-content"> + {{.CsrfTokenHtml}} <input type="hidden" name="action" value="delete"> <div class="modal-header"> diff --git a/templates/user/active.tmpl b/templates/user/active.tmpl index 47c87a591c..0df116cb40 100644 --- a/templates/user/active.tmpl +++ b/templates/user/active.tmpl @@ -1,7 +1,8 @@ {{template "base/head" .}} {{template "base/navbar" .}} <div id="gogs-body" class="container"> - <form action="/user/activate" method="get" class="form-horizontal gogs-card" id="gogs-login-card"> + <form action="/user/activate" method="post" class="form-horizontal gogs-card" id="gogs-login-card"> + {{.CsrfTokenHtml}} <h3>Activate Your Account</h3> {{if .IsActivatePage}} {{if .ServiceNotEnabled}} diff --git a/templates/user/delete.tmpl b/templates/user/delete.tmpl index 397ea8cc09..46376672d4 100644 --- a/templates/user/delete.tmpl +++ b/templates/user/delete.tmpl @@ -22,6 +22,7 @@ <div class="modal fade" id="delete-account-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal-dialog"> <form action="/user/delete" method="post" class="modal-content" id="gogs-user-delete"> + {{.CsrfTokenHtml}} <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title" id="myModalLabel">Delete Account</h4> diff --git a/templates/user/password.tmpl b/templates/user/password.tmpl index 2ee178a3fc..936ec4b124 100644 --- a/templates/user/password.tmpl +++ b/templates/user/password.tmpl @@ -5,7 +5,9 @@ <div id="gogs-user-setting-container" class="col-md-9"> <div id="gogs-setting-pwd"> <h4>Password</h4> - <form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting/password">{{if .IsSuccess}} + <form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting/password"> + {{.CsrfTokenHtml}} + {{if .IsSuccess}} <p class="alert alert-success">Password is changed successfully. You can now sign in via new password.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}} <div class="form-group"> <label class="col-md-3 control-label">Old Password<strong class="text-danger">*</strong></label> diff --git a/templates/user/publickey.tmpl b/templates/user/publickey.tmpl index 72467659be..e645e1a84b 100644 --- a/templates/user/publickey.tmpl +++ b/templates/user/publickey.tmpl @@ -22,6 +22,7 @@ <div class="modal fade" id="ssh-add-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal-dialog"> <form class="modal-content form-horizontal" id="gogs-ssh-form" method="post" action="/user/setting/ssh/"> + {{.CsrfTokenHtml}} <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title" id="myModalLabel">Add SSH Key</h4> diff --git a/templates/user/setting.tmpl b/templates/user/setting.tmpl index 222ddd895b..30c9529b12 100644 --- a/templates/user/setting.tmpl +++ b/templates/user/setting.tmpl @@ -6,6 +6,7 @@ <div id="gogs-setting-pwd"> <h4>Account Profile</h4> <form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting"> + {{.CsrfTokenHtml}} {{if .IsSuccess}}<p class="alert alert-success">Your profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}} <p>Your Email will be public and used for Account related notifications and any web based operations made via the web.</p> <div class="form-group"> diff --git a/templates/user/signin.tmpl b/templates/user/signin.tmpl index a49bf11405..8dc7292ff8 100644 --- a/templates/user/signin.tmpl +++ b/templates/user/signin.tmpl @@ -2,6 +2,7 @@ {{template "base/navbar" .}} <div class="container" id="gogs-body" data-page="user-signin"> <form action="/user/login" method="post" class="form-horizontal gogs-card" id="gogs-login-card"> + {{.CsrfTokenHtml}} <h3>Log in</h3> <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div> <div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}"> diff --git a/templates/user/signup.tmpl b/templates/user/signup.tmpl index 069d34a5b2..fbfc4cadcc 100644 --- a/templates/user/signup.tmpl +++ b/templates/user/signup.tmpl @@ -2,6 +2,7 @@ {{template "base/navbar" .}} <div class="container" id="gogs-body" data-page="user-signup"> <form action="/user/sign_up" method="post" class="form-horizontal gogs-card" id="gogs-login-card"> + {{.CsrfTokenHtml}} {{if .DisenableRegisteration}} Sorry, registeration has been disenabled, you can only get account from administrator. {{else}} diff --git a/web.go b/web.go index ac5761d720..0da2d129d0 100644 --- a/web.go +++ b/web.go @@ -82,9 +82,10 @@ func runWeb(*cli.Context) { m.Use(middleware.InitContext()) - reqSignIn := middleware.SignInRequire(true) - ignSignIn := middleware.SignInRequire(base.Service.RequireSignInView) - reqSignOut := middleware.SignOutRequire() + reqSignIn := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true}) + ignSignIn := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: base.Service.RequireSignInView}) + reqSignOut := middleware.Toggle(&middleware.ToggleOptions{SignOutRequire: true}) + // Routers. m.Get("/", ignSignIn, routers.Home) m.Get("/issues", reqSignIn, user.Issues) @@ -109,14 +110,15 @@ func runWeb(*cli.Context) { m.Get("/help", routers.Help) - adminReq := middleware.AdminRequire() - m.Get("/admin", reqSignIn, adminReq, admin.Dashboard) - m.Get("/admin/users", reqSignIn, adminReq, admin.Users) - m.Any("/admin/users/new", reqSignIn, adminReq, binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser) - m.Any("/admin/users/:userid", reqSignIn, adminReq, binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser) - m.Any("/admin/users/:userid/delete", reqSignIn, adminReq, admin.DeleteUser) - m.Get("/admin/repos", reqSignIn, adminReq, admin.Repositories) - m.Get("/admin/config", reqSignIn, adminReq, admin.Config) + adminReq := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true, AdminRequire: true}) + + m.Get("/admin", adminReq, admin.Dashboard) + m.Get("/admin/users", adminReq, admin.Users) + m.Any("/admin/users/new", adminReq, binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser) + m.Any("/admin/users/:userid", adminReq, binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser) + m.Any("/admin/users/:userid/delete", adminReq, admin.DeleteUser) + m.Get("/admin/repos", adminReq, admin.Repositories) + m.Get("/admin/config", adminReq, admin.Config) m.Post("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.SettingPost) m.Get("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.Setting) From 61e29226015fad6451281035948c3d8d1364880c Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 13:50:50 -0400 Subject: [PATCH 10/38] Working on issues --- gogs.go | 2 +- models/action.go | 1 - models/issue.go | 123 ++++++++++++++++- models/models.go | 2 +- routers/repo/issue.go | 30 ++++ routers/repo/repo.go | 294 ++++++++++++++++++++++++++++++++++++++++ routers/repo/single.go | 301 ----------------------------------------- 7 files changed, 443 insertions(+), 310 deletions(-) create mode 100644 routers/repo/issue.go delete mode 100644 routers/repo/single.go diff --git a/gogs.go b/gogs.go index a609032093..2bb80a6644 100644 --- a/gogs.go +++ b/gogs.go @@ -20,7 +20,7 @@ import ( // Test that go1.2 tag above is included in builds. main.go refers to this definition. const go12tag = true -const APP_VER = "0.1.5.0322.2" +const APP_VER = "0.1.6.0323.1" func init() { base.AppVer = APP_VER diff --git a/models/action.go b/models/action.go index 4e1107f891..a996e16aa8 100644 --- a/models/action.go +++ b/models/action.go @@ -88,7 +88,6 @@ func CommitRepoAction(userId int64, userName string, return err } repo.IsBare = false - repo.Updated = time.Now() if err = UpdateRepository(repo); err != nil { return err } diff --git a/models/issue.go b/models/issue.go index c669d201f6..0b6ca4c323 100644 --- a/models/issue.go +++ b/models/issue.go @@ -4,16 +4,127 @@ package models +import ( + "strings" + "time" + + "github.com/gogits/gogs/modules/base" +) + +// Issue represents an issue or pull request of repository. type Issue struct { - Id int64 - RepoId int64 `xorm:"index"` - PosterId int64 + Id int64 + Index int64 // Index in one repository. + Name string + RepoId int64 `xorm:"index"` + PosterId int64 + MilestoneId int64 + AssigneeId int64 + IsPull bool // Indicates whether is a pull request or not. + IsClosed bool + Labels string + Mentions string + Content string + NumComments int + Created time.Time `xorm:"created"` + Updated time.Time `xorm:"updated"` } -type PullRequest struct { - Id int64 +// CreateIssue creates new issue for repository. +func CreateIssue(userId, repoId, milestoneId, assigneeId int64, name, labels, mentions, content string, isPull bool) error { + count, err := GetIssueCount(repoId) + if err != nil { + return err + } + + _, err = orm.Insert(&Issue{ + Index: count + 1, + Name: name, + RepoId: repoId, + PosterId: userId, + MilestoneId: milestoneId, + AssigneeId: assigneeId, + IsPull: isPull, + Labels: labels, + Mentions: mentions, + Content: content, + }) + return err } +// GetIssueCount returns count of issues in the repository. +func GetIssueCount(repoId int64) (int64, error) { + return orm.Count(&Issue{RepoId: repoId}) +} + +// GetIssues returns a list of issues by given conditions. +func GetIssues(userId, repoId, posterId, milestoneId int64, page int, isClosed, isMention bool, labels, sortType string) ([]Issue, error) { + sess := orm.Limit(20, (page-1)*20).Where("repo_id=?", repoId).And("is_closed=?", isClosed) + if userId > 0 { + sess = sess.And("assignee_id=?", userId) + } else if posterId > 0 { + sess = sess.And("poster_id=?", posterId) + } else if isMention { + sess = sess.And("mentions like '%$" + base.ToStr(userId) + "|%'") + } + + if milestoneId > 0 { + sess = sess.And("milestone_id=?", milestoneId) + } + + if len(labels) > 0 { + for _, label := range strings.Split(labels, ",") { + sess = sess.And("mentions like '%$" + label + "|%'") + } + } + + switch sortType { + case "oldest": + sess = sess.Asc("created") + case "recentupdate": + sess = sess.Desc("updated") + case "leastupdate": + sess = sess.Asc("updated") + case "mostcomment": + sess = sess.Desc("num_comments") + case "leastcomment": + sess = sess.Asc("num_comments") + default: + sess = sess.Desc("created") + } + + var issues []Issue + err := sess.Find(&issues) + return issues, err +} + +// Label represents a list of labels of repository for issues. +type Label struct { + Id int64 + RepoId int64 `xorm:"index"` + Names string + Colors string +} + +// Milestone represents a milestone of repository. +type Milestone struct { + Id int64 + Name string + RepoId int64 `xorm:"index"` + IsClosed bool + Content string + NumIssues int + DueDate time.Time + Created time.Time `xorm:"created"` +} + +// Comment represents a comment in commit and issue page. type Comment struct { - Id int64 + Id int64 + PosterId int64 + IssueId int64 + CommitId int64 + Line int + Content string + Created time.Time `xorm:"created"` } diff --git a/models/models.go b/models/models.go index 8713ff2896..fb749c5d8a 100644 --- a/models/models.go +++ b/models/models.go @@ -72,7 +72,7 @@ func setEngine() { func NewEngine() { setEngine() if err := orm.Sync(new(User), new(PublicKey), new(Repository), new(Watch), - new(Action), new(Access)); err != nil { + new(Action), new(Access), new(Issue)); err != nil { fmt.Printf("sync database struct error: %v\n", err) os.Exit(2) } diff --git a/routers/repo/issue.go b/routers/repo/issue.go new file mode 100644 index 0000000000..c6af8ca0bc --- /dev/null +++ b/routers/repo/issue.go @@ -0,0 +1,30 @@ +// 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 repo + +import ( + "github.com/codegangsta/martini" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/middleware" +) + +func Issues(ctx *middleware.Context, params martini.Params) { + ctx.Data["IsRepoToolbarIssues"] = true + + milestoneId, _ := base.StrTo(params["milestone"]).Int() + page, _ := base.StrTo(params["page"]).Int() + + var err error + ctx.Data["Issues"], err = models.GetIssues(0, ctx.Repo.Repository.Id, 0, + int64(milestoneId), page, params["state"] == "closed", false, params["labels"], params["sortType"]) + if err != nil { + ctx.Handle(200, "issue.Issues: %v", err) + return + } + + ctx.HTML(200, "repo/issues") +} diff --git a/routers/repo/repo.go b/routers/repo/repo.go index c83a6df522..ff0fa85dde 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -5,8 +5,17 @@ package repo import ( + "path" + "strings" + + "github.com/codegangsta/martini" + + "github.com/gogits/git" + "github.com/gogits/webdav" + "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" ) @@ -61,3 +70,288 @@ func SettingPost(ctx *middleware.Context) { log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) ctx.Redirect("/", 302) } + +func Branches(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsValid { + return + } + + brs, err := models.GetBranches(params["username"], params["reponame"]) + if err != nil { + ctx.Handle(200, "repo.Branches", err) + return + } else if len(brs) == 0 { + ctx.Error(404) + return + } + + ctx.Data["Username"] = params["username"] + ctx.Data["Reponame"] = params["reponame"] + + ctx.Data["Branchname"] = brs[0] + ctx.Data["Branches"] = brs + ctx.Data["IsRepoToolbarBranches"] = true + + ctx.HTML(200, "repo/branches") +} + +func Single(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsValid { + return + } + + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + + // Get tree path + treename := params["_1"] + + if len(treename) > 0 && treename[len(treename)-1] == '/' { + ctx.Redirect("/"+ctx.Repo.Owner.LowerName+"/"+ + ctx.Repo.Repository.Name+"/src/"+params["branchname"]+"/"+treename[:len(treename)-1], 302) + return + } + + ctx.Data["IsRepoToolbarSource"] = true + + // Branches. + brs, err := models.GetBranches(params["username"], params["reponame"]) + if err != nil { + log.Error("repo.Single(GetBranches): %v", err) + ctx.Error(404) + return + } else if ctx.Repo.Repository.IsBare { + ctx.Data["IsBareRepo"] = true + ctx.HTML(200, "repo/single") + return + } + + ctx.Data["Branches"] = brs + + repoFile, err := models.GetTargetFile(params["username"], params["reponame"], + params["branchname"], params["commitid"], treename) + + if err != nil && err != models.ErrRepoFileNotExist { + log.Error("repo.Single(GetTargetFile): %v", err) + ctx.Error(404) + return + } + + branchLink := "/" + ctx.Repo.Owner.LowerName + "/" + ctx.Repo.Repository.Name + "/src/" + params["branchname"] + + if len(treename) != 0 && repoFile == nil { + ctx.Error(404) + return + } + + if repoFile != nil && repoFile.IsFile() { + if repoFile.Size > 1024*1024 || repoFile.Filemode != git.FileModeBlob { + ctx.Data["FileIsLarge"] = true + } else if blob, err := repoFile.LookupBlob(); err != nil { + log.Error("repo.Single(repoFile.LookupBlob): %v", err) + ctx.Error(404) + } else { + ctx.Data["IsFile"] = true + ctx.Data["FileName"] = repoFile.Name + ext := path.Ext(repoFile.Name) + if len(ext) > 0 { + ext = ext[1:] + } + ctx.Data["FileExt"] = ext + + readmeExist := base.IsMarkdownFile(repoFile.Name) || base.IsReadmeFile(repoFile.Name) + ctx.Data["ReadmeExist"] = readmeExist + if readmeExist { + ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), "")) + } else { + ctx.Data["FileContent"] = string(blob.Contents()) + } + } + + } else { + // Directory and file list. + files, err := models.GetReposFiles(params["username"], params["reponame"], + params["branchname"], params["commitid"], treename) + if err != nil { + log.Error("repo.Single(GetReposFiles): %v", err) + ctx.Error(404) + return + } + + ctx.Data["Files"] = files + + var readmeFile *models.RepoFile + + for _, f := range files { + if !f.IsFile() || !base.IsReadmeFile(f.Name) { + continue + } else { + readmeFile = f + break + } + } + + if readmeFile != nil { + ctx.Data["ReadmeExist"] = true + // if file large than 1M not show it + if readmeFile.Size > 1024*1024 || readmeFile.Filemode != git.FileModeBlob { + ctx.Data["FileIsLarge"] = true + } else if blob, err := readmeFile.LookupBlob(); err != nil { + log.Error("repo.Single(readmeFile.LookupBlob): %v", err) + ctx.Error(404) + return + } else { + // current repo branch link + + ctx.Data["FileName"] = readmeFile.Name + ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), branchLink)) + } + } + } + + ctx.Data["Username"] = params["username"] + ctx.Data["Reponame"] = params["reponame"] + ctx.Data["Branchname"] = params["branchname"] + + var treenames []string + Paths := make([]string, 0) + + if len(treename) > 0 { + treenames = strings.Split(treename, "/") + for i, _ := range treenames { + Paths = append(Paths, strings.Join(treenames[0:i+1], "/")) + } + + ctx.Data["HasParentPath"] = true + if len(Paths)-2 >= 0 { + ctx.Data["ParentPath"] = "/" + Paths[len(Paths)-2] + } + } + + // Get latest commit according username and repo name + commit, err := models.GetCommit(params["username"], params["reponame"], + params["branchname"], params["commitid"]) + if err != nil { + log.Error("repo.Single(GetCommit): %v", err) + ctx.Error(404) + return + } + ctx.Data["LastCommit"] = commit + + ctx.Data["Paths"] = Paths + ctx.Data["Treenames"] = treenames + ctx.Data["BranchLink"] = branchLink + ctx.HTML(200, "repo/single") +} + +func Http(ctx *middleware.Context, params martini.Params) { + /*if !ctx.Repo.IsValid { + return + }*/ + + // TODO: access check + + username := params["username"] + reponame := params["reponame"] + if strings.HasSuffix(reponame, ".git") { + reponame = reponame[:len(reponame)-4] + } + + prefix := path.Join("/", username, params["reponame"]) + server := &webdav.Server{ + Fs: webdav.Dir(models.RepoPath(username, reponame)), + TrimPrefix: prefix, + Listings: true, + } + + server.ServeHTTP(ctx.ResponseWriter, ctx.Req) +} + +func Setting(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + + ctx.Data["IsRepoToolbarSetting"] = true + + if ctx.Repo.Repository.IsBare { + ctx.Data["IsBareRepo"] = true + ctx.HTML(200, "repo/setting") + return + } + + var title string + if t, ok := ctx.Data["Title"].(string); ok { + title = t + } + + if len(params["branchname"]) == 0 { + params["branchname"] = "master" + } + + ctx.Data["Branchname"] = params["branchname"] + ctx.Data["Title"] = title + " - settings" + ctx.HTML(200, "repo/setting") +} + +func Commits(ctx *middleware.Context, params martini.Params) { + brs, err := models.GetBranches(params["username"], params["reponame"]) + if err != nil { + ctx.Handle(200, "repo.Commits", err) + return + } else if len(brs) == 0 { + ctx.Error(404) + return + } + + ctx.Data["IsRepoToolbarCommits"] = true + commits, err := models.GetCommits(params["username"], + params["reponame"], params["branchname"]) + if err != nil { + ctx.Error(404) + return + } + ctx.Data["Username"] = params["username"] + ctx.Data["Reponame"] = params["reponame"] + ctx.Data["CommitCount"] = commits.Len() + ctx.Data["Commits"] = commits + ctx.HTML(200, "repo/commits") +} + +func Pulls(ctx *middleware.Context) { + ctx.Data["IsRepoToolbarPulls"] = true + ctx.HTML(200, "repo/pulls") +} + +func Action(ctx *middleware.Context, params martini.Params) { + var err error + switch params["action"] { + case "watch": + err = models.WatchRepo(ctx.User.Id, ctx.Repo.Repository.Id, true) + case "unwatch": + err = models.WatchRepo(ctx.User.Id, ctx.Repo.Repository.Id, false) + case "desc": + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + + ctx.Repo.Repository.Description = ctx.Query("desc") + ctx.Repo.Repository.Website = ctx.Query("site") + err = models.UpdateRepository(ctx.Repo.Repository) + } + + if err != nil { + log.Error("repo.Action(%s): %v", params["action"], err) + ctx.JSON(200, map[string]interface{}{ + "ok": false, + "err": err.Error(), + }) + return + } + ctx.JSON(200, map[string]interface{}{ + "ok": true, + }) +} diff --git a/routers/repo/single.go b/routers/repo/single.go deleted file mode 100644 index 5906e64fb9..0000000000 --- a/routers/repo/single.go +++ /dev/null @@ -1,301 +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 repo - -import ( - "path" - "strings" - - "github.com/codegangsta/martini" - - "github.com/gogits/git" - "github.com/gogits/webdav" - - "github.com/gogits/gogs/models" - "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/log" - "github.com/gogits/gogs/modules/middleware" -) - -func Branches(ctx *middleware.Context, params martini.Params) { - if !ctx.Repo.IsValid { - return - } - - brs, err := models.GetBranches(params["username"], params["reponame"]) - if err != nil { - ctx.Handle(200, "repo.Branches", err) - return - } else if len(brs) == 0 { - ctx.Error(404) - return - } - - ctx.Data["Username"] = params["username"] - ctx.Data["Reponame"] = params["reponame"] - - ctx.Data["Branchname"] = brs[0] - ctx.Data["Branches"] = brs - ctx.Data["IsRepoToolbarBranches"] = true - - ctx.HTML(200, "repo/branches") -} - -func Single(ctx *middleware.Context, params martini.Params) { - if !ctx.Repo.IsValid { - return - } - - if len(params["branchname"]) == 0 { - params["branchname"] = "master" - } - - // Get tree path - treename := params["_1"] - - if len(treename) > 0 && treename[len(treename)-1] == '/' { - ctx.Redirect("/"+ctx.Repo.Owner.LowerName+"/"+ - ctx.Repo.Repository.Name+"/src/"+params["branchname"]+"/"+treename[:len(treename)-1], 302) - return - } - - ctx.Data["IsRepoToolbarSource"] = true - - // Branches. - brs, err := models.GetBranches(params["username"], params["reponame"]) - if err != nil { - log.Error("repo.Single(GetBranches): %v", err) - ctx.Error(404) - return - } else if ctx.Repo.Repository.IsBare { - ctx.Data["IsBareRepo"] = true - ctx.HTML(200, "repo/single") - return - } - - ctx.Data["Branches"] = brs - - repoFile, err := models.GetTargetFile(params["username"], params["reponame"], - params["branchname"], params["commitid"], treename) - - if err != nil && err != models.ErrRepoFileNotExist { - log.Error("repo.Single(GetTargetFile): %v", err) - ctx.Error(404) - return - } - - branchLink := "/" + ctx.Repo.Owner.LowerName + "/" + ctx.Repo.Repository.Name + "/src/" + params["branchname"] - - if len(treename) != 0 && repoFile == nil { - ctx.Error(404) - return - } - - if repoFile != nil && repoFile.IsFile() { - if repoFile.Size > 1024*1024 || repoFile.Filemode != git.FileModeBlob { - ctx.Data["FileIsLarge"] = true - } else if blob, err := repoFile.LookupBlob(); err != nil { - log.Error("repo.Single(repoFile.LookupBlob): %v", err) - ctx.Error(404) - } else { - ctx.Data["IsFile"] = true - ctx.Data["FileName"] = repoFile.Name - ext := path.Ext(repoFile.Name) - if len(ext) > 0 { - ext = ext[1:] - } - ctx.Data["FileExt"] = ext - - readmeExist := base.IsMarkdownFile(repoFile.Name) || base.IsReadmeFile(repoFile.Name) - ctx.Data["ReadmeExist"] = readmeExist - if readmeExist { - ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), "")) - } else { - ctx.Data["FileContent"] = string(blob.Contents()) - } - } - - } else { - // Directory and file list. - files, err := models.GetReposFiles(params["username"], params["reponame"], - params["branchname"], params["commitid"], treename) - if err != nil { - log.Error("repo.Single(GetReposFiles): %v", err) - ctx.Error(404) - return - } - - ctx.Data["Files"] = files - - var readmeFile *models.RepoFile - - for _, f := range files { - if !f.IsFile() || !base.IsReadmeFile(f.Name) { - continue - } else { - readmeFile = f - break - } - } - - if readmeFile != nil { - ctx.Data["ReadmeExist"] = true - // if file large than 1M not show it - if readmeFile.Size > 1024*1024 || readmeFile.Filemode != git.FileModeBlob { - ctx.Data["FileIsLarge"] = true - } else if blob, err := readmeFile.LookupBlob(); err != nil { - log.Error("repo.Single(readmeFile.LookupBlob): %v", err) - ctx.Error(404) - return - } else { - // current repo branch link - - ctx.Data["FileName"] = readmeFile.Name - ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), branchLink)) - } - } - } - - ctx.Data["Username"] = params["username"] - ctx.Data["Reponame"] = params["reponame"] - ctx.Data["Branchname"] = params["branchname"] - - var treenames []string - Paths := make([]string, 0) - - if len(treename) > 0 { - treenames = strings.Split(treename, "/") - for i, _ := range treenames { - Paths = append(Paths, strings.Join(treenames[0:i+1], "/")) - } - - ctx.Data["HasParentPath"] = true - if len(Paths)-2 >= 0 { - ctx.Data["ParentPath"] = "/" + Paths[len(Paths)-2] - } - } - - // Get latest commit according username and repo name - commit, err := models.GetCommit(params["username"], params["reponame"], - params["branchname"], params["commitid"]) - if err != nil { - log.Error("repo.Single(GetCommit): %v", err) - ctx.Error(404) - return - } - ctx.Data["LastCommit"] = commit - - ctx.Data["Paths"] = Paths - ctx.Data["Treenames"] = treenames - ctx.Data["BranchLink"] = branchLink - ctx.HTML(200, "repo/single") -} - -func Http(ctx *middleware.Context, params martini.Params) { - /*if !ctx.Repo.IsValid { - return - }*/ - - // TODO: access check - - username := params["username"] - reponame := params["reponame"] - if strings.HasSuffix(reponame, ".git") { - reponame = reponame[:len(reponame)-4] - } - - prefix := path.Join("/", username, params["reponame"]) - server := &webdav.Server{ - Fs: webdav.Dir(models.RepoPath(username, reponame)), - TrimPrefix: prefix, - Listings: true, - } - - server.ServeHTTP(ctx.ResponseWriter, ctx.Req) -} - -func Setting(ctx *middleware.Context, params martini.Params) { - if !ctx.Repo.IsOwner { - ctx.Error(404) - return - } - - ctx.Data["IsRepoToolbarSetting"] = true - - if ctx.Repo.Repository.IsBare { - ctx.Data["IsBareRepo"] = true - ctx.HTML(200, "repo/setting") - return - } - - var title string - if t, ok := ctx.Data["Title"].(string); ok { - title = t - } - - if len(params["branchname"]) == 0 { - params["branchname"] = "master" - } - - ctx.Data["Branchname"] = params["branchname"] - ctx.Data["Title"] = title + " - settings" - ctx.HTML(200, "repo/setting") -} - -func Commits(ctx *middleware.Context, params martini.Params) { - brs, err := models.GetBranches(params["username"], params["reponame"]) - if err != nil { - ctx.Handle(200, "repo.Commits", err) - return - } else if len(brs) == 0 { - ctx.Error(404) - return - } - - ctx.Data["IsRepoToolbarCommits"] = true - commits, err := models.GetCommits(params["username"], - params["reponame"], params["branchname"]) - if err != nil { - ctx.Error(404) - return - } - ctx.Data["Username"] = params["username"] - ctx.Data["Reponame"] = params["reponame"] - ctx.Data["CommitCount"] = commits.Len() - ctx.Data["Commits"] = commits - ctx.HTML(200, "repo/commits") -} - -func Issues(ctx *middleware.Context) { - ctx.Data["IsRepoToolbarIssues"] = true - ctx.HTML(200, "repo/issues") -} - -func Pulls(ctx *middleware.Context) { - ctx.Data["IsRepoToolbarPulls"] = true - ctx.HTML(200, "repo/pulls") -} - -func Action(ctx *middleware.Context, params martini.Params) { - var err error - switch params["action"] { - case "watch": - err = models.WatchRepo(ctx.User.Id, ctx.Repo.Repository.Id, true) - case "unwatch": - err = models.WatchRepo(ctx.User.Id, ctx.Repo.Repository.Id, false) - } - - if err != nil { - log.Error("repo.Action(%s): %v", params["action"], err) - ctx.JSON(200, map[string]interface{}{ - "ok": false, - "err": err.Error(), - }) - return - } - ctx.JSON(200, map[string]interface{}{ - "ok": true, - }) -} From b3cfd9fe0c293ba9d84d38ec140db2c01b1e3109 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 14:27:03 -0400 Subject: [PATCH 11/38] Fix SSH key bug in windows --- models/publickey.go | 45 +++++++++++++++++++++-------------- models/repo.go | 2 +- models/user.go | 2 +- modules/middleware/auth.go | 1 + modules/middleware/context.go | 4 ---- routers/repo/issue.go | 10 ++++++++ 6 files changed, 40 insertions(+), 24 deletions(-) diff --git a/models/publickey.go b/models/publickey.go index c69bca681d..9e7cc6f740 100644 --- a/models/publickey.go +++ b/models/publickey.go @@ -19,6 +19,8 @@ import ( "time" "github.com/Unknwon/com" + + "github.com/gogits/gogs/modules/log" ) const ( @@ -99,8 +101,8 @@ func AddPublicKey(key *PublicKey) (err error) { } // Calculate fingerprint. - tmpPath := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()), - "id_rsa.pub") + tmpPath := strings.Replace(filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()), + "id_rsa.pub"), "\\", "/", -1) os.MkdirAll(path.Dir(tmpPath), os.ModePerm) if err = ioutil.WriteFile(tmpPath, []byte(key.Content), os.ModePerm); err != nil { return err @@ -127,25 +129,11 @@ func AddPublicKey(key *PublicKey) (err error) { return nil } -// DeletePublicKey deletes SSH key information both in database and authorized_keys file. -func DeletePublicKey(key *PublicKey) (err error) { - // Delete SSH key in database. - has, err := orm.Id(key.Id).Get(key) - if err != nil { - return err - } else if !has { - return errors.New("Public key does not exist") - } - if _, err = orm.Delete(key); err != nil { - return err - } - +func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { // Delete SSH key in SSH key file. sshOpLocker.Lock() defer sshOpLocker.Unlock() - p := filepath.Join(sshPath, "authorized_keys") - tmpP := filepath.Join(sshPath, "authorized_keys.tmp") fr, err := os.Open(p) if err != nil { return err @@ -188,8 +176,29 @@ func DeletePublicKey(key *PublicKey) (err error) { break } } + return nil +} - if err = os.Remove(p); err != nil { +// DeletePublicKey deletes SSH key information both in database and authorized_keys file. +func DeletePublicKey(key *PublicKey) (err error) { + // Delete SSH key in database. + has, err := orm.Id(key.Id).Get(key) + if err != nil { + return err + } else if !has { + return errors.New("Public key does not exist") + } + if _, err = orm.Delete(key); err != nil { + return err + } + + p := filepath.Join(sshPath, "authorized_keys") + tmpP := filepath.Join(sshPath, "authorized_keys.tmp") + log.Trace("ssh.DeletePublicKey(authorized_keys): %s", p) + + if err = rewriteAuthorizedKeys(key, p, tmpP); err != nil { + return err + } else if err = os.Remove(p); err != nil { return err } return os.Rename(tmpP, p) diff --git a/models/repo.go b/models/repo.go index fb115de590..317f936ece 100644 --- a/models/repo.go +++ b/models/repo.go @@ -372,7 +372,7 @@ func RepoPath(userName, repoName string) string { } func UpdateRepository(repo *Repository) error { - _, err := orm.Id(repo.Id).UseBool().Update(repo) + _, err := orm.Id(repo.Id).UseBool().Cols("description", "website").Update(repo) return err } diff --git a/models/user.go b/models/user.go index d6dc041490..88c29ae43e 100644 --- a/models/user.go +++ b/models/user.go @@ -201,7 +201,7 @@ func VerifyUserActiveCode(code string) (user *User) { // UpdateUser updates user's information. func UpdateUser(user *User) (err error) { - _, err = orm.Id(user.Id).UseBool().Update(user) + _, err = orm.Id(user.Id).UseBool().Cols("website", "location").Update(user) return err } diff --git a/modules/middleware/auth.go b/modules/middleware/auth.go index b557188ee9..3224b3df91 100644 --- a/modules/middleware/auth.go +++ b/modules/middleware/auth.go @@ -49,6 +49,7 @@ func Toggle(options *ToggleOptions) martini.Handler { ctx.Error(403) return } + ctx.Data["PageIsAdmin"] = true } } } diff --git a/modules/middleware/context.go b/modules/middleware/context.go index b28953fc0e..5727b4f094 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -216,10 +216,6 @@ func InitContext() martini.Handler { ctx.Data["SignedUserId"] = user.Id ctx.Data["SignedUserName"] = user.LowerName ctx.Data["IsAdmin"] = ctx.User.IsAdmin - - if ctx.User.IsAdmin { - ctx.Data["PageIsAdmin"] = true - } } // get or create csrf token diff --git a/routers/repo/issue.go b/routers/repo/issue.go index c6af8ca0bc..eee55c6fda 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -28,3 +28,13 @@ func Issues(ctx *middleware.Context, params martini.Params) { ctx.HTML(200, "repo/issues") } + +func CreateIssue(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + // else if err = models.CreateIssue(userId, repoId, milestoneId, assigneeId, name, labels, mentions, content, isPull); err != nil { + + // } +} From 59ffdbf6f80328f9b9074930444dedd936aeae51 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 16:00:46 -0400 Subject: [PATCH 12/38] Add create, list, view issue --- README.md | 2 +- models/action.go | 2 +- models/issue.go | 46 +++++++++++++++---- models/publickey.go | 2 +- models/repo.go | 7 +++ models/user.go | 7 +++ modules/auth/issue.go | 54 +++++++++++++++++++++++ routers/repo/issue.go | 51 +++++++++++++++++++-- routers/repo/repo.go | 5 +++ templates/admin/repos.tmpl | 2 +- web.go | 90 +++++++++++++++++++++++--------------- 11 files changed, 217 insertions(+), 51 deletions(-) create mode 100644 modules/auth/issue.go diff --git a/README.md b/README.md index 35044927ff..89a346d602 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Gogs(Go Git Service) is a GitHub-like clone in the Go Programming Language. Since we choose to use pure Go implementation of Git manipulation, Gogs certainly supports **ALL platforms** that Go supports, including Linux, Max OS X, and Windows with **ZERO** dependency. -##### Current version: 0.1.5 Alpha +##### Current version: 0.1.6 Alpha ## Purpose diff --git a/models/action.go b/models/action.go index a996e16aa8..cfb124363c 100644 --- a/models/action.go +++ b/models/action.go @@ -30,7 +30,7 @@ type Action struct { ActUserName string // Action user name. RepoId int64 RepoName string - Content string + Content string `xorm:"TEXT"` Created time.Time `xorm:"created"` } diff --git a/models/issue.go b/models/issue.go index 0b6ca4c323..f78c240cbc 100644 --- a/models/issue.go +++ b/models/issue.go @@ -5,12 +5,17 @@ package models import ( + "errors" "strings" "time" "github.com/gogits/gogs/modules/base" ) +var ( + ErrIssueNotExist = errors.New("Issue does not exist") +) + // Issue represents an issue or pull request of repository. type Issue struct { Id int64 @@ -22,22 +27,25 @@ type Issue struct { AssigneeId int64 IsPull bool // Indicates whether is a pull request or not. IsClosed bool - Labels string - Mentions string - Content string + Labels string `xorm:"TEXT"` + Mentions string `xorm:"TEXT"` + Content string `xorm:"TEXT"` NumComments int Created time.Time `xorm:"created"` Updated time.Time `xorm:"updated"` } // CreateIssue creates new issue for repository. -func CreateIssue(userId, repoId, milestoneId, assigneeId int64, name, labels, mentions, content string, isPull bool) error { +func CreateIssue(userId, repoId, milestoneId, assigneeId int64, name, labels, content string, isPull bool) (*Issue, error) { count, err := GetIssueCount(repoId) if err != nil { - return err + return nil, err } - _, err = orm.Insert(&Issue{ + // TODO: find out mentions + mentions := "" + + issue := &Issue{ Index: count + 1, Name: name, RepoId: repoId, @@ -48,8 +56,9 @@ func CreateIssue(userId, repoId, milestoneId, assigneeId int64, name, labels, me Labels: labels, Mentions: mentions, Content: content, - }) - return err + } + _, err = orm.Insert(issue) + return issue, err } // GetIssueCount returns count of issues in the repository. @@ -57,9 +66,28 @@ func GetIssueCount(repoId int64) (int64, error) { return orm.Count(&Issue{RepoId: repoId}) } +// GetIssueById returns issue object by given id. +func GetIssueById(id int64) (*Issue, error) { + issue := new(Issue) + has, err := orm.Id(id).Get(issue) + if err != nil { + return nil, err + } else if !has { + return nil, ErrIssueNotExist + } + return issue, nil +} + // GetIssues returns a list of issues by given conditions. func GetIssues(userId, repoId, posterId, milestoneId int64, page int, isClosed, isMention bool, labels, sortType string) ([]Issue, error) { - sess := orm.Limit(20, (page-1)*20).Where("repo_id=?", repoId).And("is_closed=?", isClosed) + sess := orm.Limit(20, (page-1)*20) + + if repoId > 0 { + sess = sess.Where("repo_id=?", repoId).And("is_closed=?", isClosed) + } else { + sess = sess.Where("is_closed=?", isClosed) + } + if userId > 0 { sess = sess.And("assignee_id=?", userId) } else if posterId > 0 { diff --git a/models/publickey.go b/models/publickey.go index 9e7cc6f740..3f2fcabd3b 100644 --- a/models/publickey.go +++ b/models/publickey.go @@ -80,7 +80,7 @@ type PublicKey struct { OwnerId int64 `xorm:"index"` Name string `xorm:"unique not null"` Fingerprint string - Content string `xorm:"text not null"` + Content string `xorm:"TEXT not null"` Created time.Time `xorm:"created"` Updated time.Time `xorm:"updated"` } diff --git a/models/repo.go b/models/repo.go index 317f936ece..a37923c8b1 100644 --- a/models/repo.go +++ b/models/repo.go @@ -372,6 +372,13 @@ func RepoPath(userName, repoName string) string { } func UpdateRepository(repo *Repository) error { + if len(repo.Description) > 255 { + repo.Description = repo.Description[:255] + } + if len(repo.Website) > 255 { + repo.Website = repo.Website[:255] + } + _, err := orm.Id(repo.Id).UseBool().Cols("description", "website").Update(repo) return err } diff --git a/models/user.go b/models/user.go index 88c29ae43e..9333d1ee67 100644 --- a/models/user.go +++ b/models/user.go @@ -201,6 +201,13 @@ func VerifyUserActiveCode(code string) (user *User) { // UpdateUser updates user's information. func UpdateUser(user *User) (err error) { + if len(user.Location) > 255 { + user.Location = user.Location[:255] + } + if len(user.Website) > 255 { + user.Website = user.Website[:255] + } + _, err = orm.Id(user.Id).UseBool().Cols("website", "location").Update(user) return err } diff --git a/modules/auth/issue.go b/modules/auth/issue.go new file mode 100644 index 0000000000..e2b1f9f2a7 --- /dev/null +++ b/modules/auth/issue.go @@ -0,0 +1,54 @@ +// 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/codegangsta/martini" + + "github.com/gogits/binding" + + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" +) + +type CreateIssueForm struct { + IssueName string `form:"name" binding:"Required;MaxSize(50)"` + RepoId int64 `form:"repoid" binding:"Required"` + MilestoneId int64 `form:"milestoneid" binding:"Required"` + AssigneeId int64 `form:"assigneeid"` + Labels string `form:"labels"` + Content string `form:"content"` +} + +func (f *CreateIssueForm) Name(field string) string { + names := map[string]string{ + "IssueName": "Issue name", + "RepoId": "Repository ID", + "MilestoneId": "Milestone ID", + } + return names[field] +} + +func (f *CreateIssueForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { + if req.Method == "GET" || errors.Count() == 0 { + return + } + + data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) + data["HasError"] = true + AssignForm(f, data) + + if len(errors.Overall) > 0 { + for _, err := range errors.Overall { + log.Error("CreateIssueForm.Validate: %v", err) + } + return + } + + validate(errors, data, f) +} diff --git a/routers/repo/issue.go b/routers/repo/issue.go index eee55c6fda..154e8308ab 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -5,14 +5,19 @@ package repo import ( + "fmt" + "github.com/codegangsta/martini" "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/middleware" ) func Issues(ctx *middleware.Context, params martini.Params) { + ctx.Data["Title"] = "Issues" ctx.Data["IsRepoToolbarIssues"] = true milestoneId, _ := base.StrTo(params["milestone"]).Int() @@ -29,12 +34,52 @@ func Issues(ctx *middleware.Context, params martini.Params) { ctx.HTML(200, "repo/issues") } -func CreateIssue(ctx *middleware.Context, params martini.Params) { +func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { if !ctx.Repo.IsOwner { ctx.Error(404) return } - // else if err = models.CreateIssue(userId, repoId, milestoneId, assigneeId, name, labels, mentions, content, isPull); err != nil { - // } + ctx.Data["Title"] = "Create issue" + + if ctx.Req.Method == "GET" { + ctx.HTML(200, "issue/create") + return + } + + if ctx.HasError() { + ctx.HTML(200, "issue/create") + return + } + + issue, err := models.CreateIssue(ctx.User.Id, form.RepoId, form.MilestoneId, form.AssigneeId, + form.IssueName, form.Labels, form.Content, false) + if err == nil { + log.Trace("%s Issue created: %d", form.RepoId, issue.Id) + ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index), 302) + return + } + ctx.Handle(200, "issue.CreateIssue", err) +} + +func ViewIssue(ctx *middleware.Context, params martini.Params) { + issueid, err := base.StrTo(params["issueid"]).Int() + if err != nil { + ctx.Error(404) + return + } + + issue, err := models.GetIssueById(int64(issueid)) + if err != nil { + if err == models.ErrIssueNotExist { + ctx.Error(404) + } else { + ctx.Handle(200, "issue.ViewIssue", err) + } + return + } + + ctx.Data["Title"] = issue.Name + ctx.Data["Issue"] = issue + ctx.HTML(200, "issue/view") } diff --git a/routers/repo/repo.go b/routers/repo/repo.go index ff0fa85dde..c436d38714 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -31,6 +31,11 @@ func Create(ctx *middleware.Context, form auth.CreateRepoForm) { return } + if ctx.HasError() { + ctx.HTML(200, "repo/create") + return + } + _, err := models.CreateRepository(ctx.User, form.RepoName, form.Description, form.Language, form.License, form.Visibility == "private", form.InitReadme == "on") if err == nil { diff --git a/templates/admin/repos.tmpl b/templates/admin/repos.tmpl index a1f41d8365..2c91ccc096 100644 --- a/templates/admin/repos.tmpl +++ b/templates/admin/repos.tmpl @@ -27,7 +27,7 @@ <td>{{.Id}}</td> <th>{{.UserName}}</th> <td><a href="/{{.UserName}}/{{.Name}}">{{.Name}}</a></td> - <td><i class="fa fa{{if .Private}}-check{{end}}-square-o"></i></td> + <td><i class="fa fa{{if .IsPrivate}}-check{{end}}-square-o"></i></td> <td>{{.NumWatches}}</td> <td>{{.NumForks}}</td> <td>{{DateFormat .Created "M d, Y"}}</td> diff --git a/web.go b/web.go index 0da2d129d0..bf654aace2 100644 --- a/web.go +++ b/web.go @@ -91,53 +91,73 @@ func runWeb(*cli.Context) { m.Get("/issues", reqSignIn, user.Issues) m.Get("/pulls", reqSignIn, user.Pulls) m.Get("/stars", reqSignIn, user.Stars) - m.Any("/user/login", reqSignOut, binding.BindIgnErr(auth.LogInForm{}), user.SignIn) - m.Any("/user/logout", reqSignIn, user.SignOut) - m.Any("/user/sign_up", reqSignOut, binding.BindIgnErr(auth.RegisterForm{}), user.SignUp) - m.Any("/user/delete", reqSignIn, user.Delete) - m.Get("/user/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds) - m.Get("/user/activate", user.Activate) + m.Get("/help", routers.Help) - m.Any("/user/setting", reqSignIn, binding.BindIgnErr(auth.UpdateProfileForm{}), user.Setting) - m.Any("/user/setting/password", reqSignIn, binding.BindIgnErr(auth.UpdatePasswdForm{}), user.SettingPassword) - m.Any("/user/setting/ssh", reqSignIn, binding.BindIgnErr(auth.AddSSHKeyForm{}), user.SettingSSHKeys) - m.Any("/user/setting/notification", reqSignIn, user.SettingNotification) - m.Any("/user/setting/security", reqSignIn, user.SettingSecurity) + m.Group("/user", func(r martini.Router) { + r.Any("/login", binding.BindIgnErr(auth.LogInForm{}), user.SignIn) + r.Any("/sign_up", reqSignOut, binding.BindIgnErr(auth.RegisterForm{}), user.SignUp) + }, reqSignOut) + m.Group("/user", func(r martini.Router) { + r.Any("/logout", user.SignOut) + r.Any("/delete", user.Delete) + r.Any("/setting", binding.BindIgnErr(auth.UpdateProfileForm{}), user.Setting) + }, reqSignIn) + m.Group("/user", func(r martini.Router) { + r.Get("/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds) + r.Get("/activate", user.Activate) + }) + + m.Group("/user/setting", func(r martini.Router) { + r.Any("/password", binding.BindIgnErr(auth.UpdatePasswdForm{}), user.SettingPassword) + r.Any("/ssh", binding.BindIgnErr(auth.AddSSHKeyForm{}), user.SettingSSHKeys) + r.Any("/notification", user.SettingNotification) + r.Any("/security", user.SettingSecurity) + }, reqSignIn) m.Get("/user/:username", ignSignIn, user.Profile) m.Any("/repo/create", reqSignIn, binding.BindIgnErr(auth.CreateRepoForm{}), repo.Create) - m.Get("/help", routers.Help) - adminReq := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true, AdminRequire: true}) m.Get("/admin", adminReq, admin.Dashboard) - m.Get("/admin/users", adminReq, admin.Users) - m.Any("/admin/users/new", adminReq, binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser) - m.Any("/admin/users/:userid", adminReq, binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser) - m.Any("/admin/users/:userid/delete", adminReq, admin.DeleteUser) - m.Get("/admin/repos", adminReq, admin.Repositories) - m.Get("/admin/config", adminReq, admin.Config) + m.Group("/admin", func(r martini.Router) { + r.Get("/users", admin.Users) + r.Get("/repos", admin.Repositories) + r.Get("/config", admin.Config) + }, adminReq) + m.Group("/admin/users", func(r martini.Router) { + r.Any("/new", binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser) + r.Any("/:userid", binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser) + r.Any("/:userid/delete", admin.DeleteUser) + }, adminReq) - m.Post("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.SettingPost) - m.Get("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.Setting) + m.Group("/:username/:reponame", func(r martini.Router) { + r.Post("/settings", repo.SettingPost) + r.Get("/settings", repo.Setting) + r.Get("/action/:action", repo.Action) + }, reqSignIn, middleware.RepoAssignment(true)) + m.Group("/:username/:reponame", func(r martini.Router) { + r.Get("/commits/:branchname", repo.Commits) + r.Get("/issues", repo.Issues) + r.Any("/issues/new", binding.BindIgnErr(auth.CreateIssueForm{}), repo.CreateIssue) + r.Get("/issues/:issueid", repo.ViewIssue) + r.Get("/pulls", repo.Pulls) + r.Get("/branches", repo.Branches) + r.Get("/src/:branchname", repo.Single) + r.Get("/src/:branchname/**", repo.Single) + r.Get("/commits/:branchname", repo.Commits) + r.Get("/commits/:branchname", repo.Commits) + }, ignSignIn, middleware.RepoAssignment(true)) - m.Get("/:username/:reponame/commits/:branchname", ignSignIn, middleware.RepoAssignment(true), repo.Commits) - m.Get("/:username/:reponame/issues", ignSignIn, middleware.RepoAssignment(true), repo.Issues) - m.Get("/:username/:reponame/pulls", ignSignIn, middleware.RepoAssignment(true), repo.Pulls) - m.Get("/:username/:reponame/branches", ignSignIn, middleware.RepoAssignment(true), repo.Branches) - m.Get("/:username/:reponame/action/:action", reqSignIn, middleware.RepoAssignment(true), repo.Action) - m.Get("/:username/:reponame/src/:branchname/**", - ignSignIn, middleware.RepoAssignment(true), repo.Single) - m.Get("/:username/:reponame/src/:branchname", - ignSignIn, middleware.RepoAssignment(true), repo.Single) - m.Get("/:username/:reponame/commit/:commitid/**", ignSignIn, middleware.RepoAssignment(true), repo.Single) - m.Get("/:username/:reponame/commit/:commitid", ignSignIn, middleware.RepoAssignment(true), repo.Single) + // TODO: implement single commit page + // m.Get("/:username/:reponame/commit/:commitid/**", ignSignIn, middleware.RepoAssignment(true), repo.Single) + // m.Get("/:username/:reponame/commit/:commitid", ignSignIn, middleware.RepoAssignment(true), repo.Single) - m.Get("/:username/:reponame", ignSignIn, middleware.RepoAssignment(true), repo.Single) - - m.Any("/:username/:reponame/**", ignSignIn, repo.Http) + m.Group("/:username", func(r martini.Router) { + r.Get("/:reponame", middleware.RepoAssignment(true), repo.Single) + r.Any("/:reponame/**", repo.Http) + }, ignSignIn) if martini.Env == martini.Dev { m.Get("/template/**", dev.TemplatePreview) From cb52f6d07d62925a31185fedf591d0241ee2bf63 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 16:40:09 -0400 Subject: [PATCH 13/38] Add auto-login --- conf/app.ini | 4 ++++ modules/auth/auth.go | 1 + modules/base/conf.go | 8 +++++++ modules/middleware/context.go | 43 +++++++++++++++++++++++++++++++++++ routers/user/user.go | 42 +++++++++++++++++++++++++++++++++- templates/user/signin.tmpl | 11 +++++++++ 6 files changed, 108 insertions(+), 1 deletion(-) diff --git a/conf/app.ini b/conf/app.ini index ec5fcb23b3..7f283012fd 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -34,6 +34,10 @@ PATH = data/gogs.db [security] ; !!CHANGE THIS TO KEEP YOUR USER DATA SAFE!! SECRET_KEY = !#@FDEWREWR&*( +; Auto-login remember days +LOGIN_REMEMBER_DAYS = 7 +COOKIE_USERNAME = gogs_awesome +COOKIE_REMEMBER_NAME = gogs_incredible [service] ACTIVE_CODE_LIVE_MINUTES = 180 diff --git a/modules/auth/auth.go b/modules/auth/auth.go index 0e87168891..2e0555f6df 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -61,6 +61,7 @@ func (f *RegisterForm) Validate(errors *binding.Errors, req *http.Request, conte type LogInForm struct { UserName string `form:"username" binding:"Required;AlphaDash;MaxSize(30)"` Password string `form:"passwd" binding:"Required;MinSize(6);MaxSize(30)"` + Remember string `form:"remember"` } func (f *LogInForm) Name(field string) string { diff --git a/modules/base/conf.go b/modules/base/conf.go index 7c8ed93654..cdbe2b36ce 100644 --- a/modules/base/conf.go +++ b/modules/base/conf.go @@ -38,6 +38,10 @@ var ( RunUser string RepoRootPath string + LogInRememberDays int + CookieUserName string + CookieRememberName string + Cfg *goconfig.ConfigFile MailService *Mailer @@ -252,6 +256,10 @@ func NewConfigContext() { SecretKey = Cfg.MustValue("security", "SECRET_KEY") RunUser = Cfg.MustValue("", "RUN_USER") + LogInRememberDays = Cfg.MustInt("security", "LOGIN_REMEMBER_DAYS") + CookieUserName = Cfg.MustValue("security", "COOKIE_USERNAME") + CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME") + PictureService = Cfg.MustValue("picture", "SERVICE") PictureRootPath = Cfg.MustValue("picture", "PATH") diff --git a/modules/middleware/context.go b/modules/middleware/context.go index 5727b4f094..d81ab999bf 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -5,9 +5,14 @@ package middleware import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" "fmt" "html/template" "net/http" + "strconv" + "strings" "time" "github.com/codegangsta/martini" @@ -155,6 +160,44 @@ func (ctx *Context) SetCookie(name string, value string, others ...interface{}) ctx.Res.Header().Add("Set-Cookie", cookie.String()) } +// Get secure cookie from request by a given key. +func (ctx *Context) GetSecureCookie(Secret, key string) (string, bool) { + val := ctx.GetCookie(key) + if val == "" { + return "", false + } + + parts := strings.SplitN(val, "|", 3) + + if len(parts) != 3 { + return "", false + } + + vs := parts[0] + timestamp := parts[1] + sig := parts[2] + + h := hmac.New(sha1.New, []byte(Secret)) + fmt.Fprintf(h, "%s%s", vs, timestamp) + + if fmt.Sprintf("%02x", h.Sum(nil)) != sig { + return "", false + } + res, _ := base64.URLEncoding.DecodeString(vs) + return string(res), true +} + +// Set Secure cookie for response. +func (ctx *Context) SetSecureCookie(Secret, name, value string, others ...interface{}) { + vs := base64.URLEncoding.EncodeToString([]byte(value)) + timestamp := strconv.FormatInt(time.Now().UnixNano(), 10) + h := hmac.New(sha1.New, []byte(Secret)) + fmt.Fprintf(h, "%s%s", vs, timestamp) + sig := fmt.Sprintf("%02x", h.Sum(nil)) + cookie := strings.Join([]string{vs, timestamp, sig}, "|") + ctx.SetCookie(name, cookie, others...) +} + func (ctx *Context) CsrfToken() string { if len(ctx.csrfToken) > 0 { return ctx.csrfToken diff --git a/routers/user/user.go b/routers/user/user.go index 2244697714..56bc5f8e37 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -77,7 +77,39 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { ctx.Data["Title"] = "Log In" if ctx.Req.Method == "GET" { - ctx.HTML(200, "user/signin") + // Check auto-login. + userName := ctx.GetCookie(base.CookieUserName) + if len(userName) == 0 { + ctx.HTML(200, "user/signin") + return + } + + isSucceed := false + defer func() { + if !isSucceed { + log.Trace("%s auto-login cookie cleared: %s", ctx.Req.RequestURI, userName) + ctx.SetCookie(base.CookieUserName, "", -1) + ctx.SetCookie(base.CookieRememberName, "", -1) + } + }() + + user, err := models.GetUserByName(userName) + if err != nil { + ctx.HTML(200, "user/signin") + return + } + + secret := base.EncodeMd5(user.Rands + user.Passwd) + value, _ := ctx.GetSecureCookie(secret, base.CookieRememberName) + if value != user.Name { + ctx.HTML(200, "user/signin") + return + } + + isSucceed = true + ctx.Session.Set("userId", user.Id) + ctx.Session.Set("userName", user.Name) + ctx.Redirect("/") return } @@ -89,6 +121,7 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { user, err := models.LoginUserPlain(form.UserName, form.Password) if err != nil { if err == models.ErrUserNotExist { + log.Trace("%s Log in failed: %s/%s", ctx.Req.RequestURI, form.UserName, form.Password) ctx.RenderWithErr("Username or password is not correct", "user/signin", &form) return } @@ -97,6 +130,13 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { return } + if form.Remember == "on" { + secret := base.EncodeMd5(user.Rands + user.Passwd) + days := 86400 * base.LogInRememberDays + ctx.SetCookie(base.CookieUserName, user.Name, days) + ctx.SetSecureCookie(secret, base.CookieRememberName, user.Name, days) + } + ctx.Session.Set("userId", user.Id) ctx.Session.Set("userName", user.Name) ctx.Redirect("/") diff --git a/templates/user/signin.tmpl b/templates/user/signin.tmpl index 8dc7292ff8..1cd3275cce 100644 --- a/templates/user/signin.tmpl +++ b/templates/user/signin.tmpl @@ -19,6 +19,17 @@ </div> </div> + <div class="form-group"> + <div class="col-md-6 col-md-offset-4"> + <div class="checkbox"> + <label> + <input type="checkbox" name="remember" {{if .remember}}checked{{end}}> + <strong>Remember me</strong> + </label> + </div> + </div> + </div> + <div class="form-group"> <div class="col-md-offset-4 col-md-6"> <button type="submit" class="btn btn-lg btn-primary">Log In</button> From 7356153ba3c19ff49f3ecfa28bac0b8bb38eccb9 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 17:59:22 -0400 Subject: [PATCH 14/38] Batch updates --- README.md | 4 ++-- conf/app.ini | 10 ++++++++-- modules/base/conf.go | 4 ++++ modules/middleware/auth.go | 3 +++ routers/admin/user.go | 2 +- routers/repo/issue.go | 2 +- routers/repo/repo.go | 8 ++++---- routers/user/user.go | 21 ++++++++++++++++++--- 8 files changed, 41 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 89a346d602..325c3a9736 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ There are two ways to install Gogs: ## Acknowledgments - Logo is inspired by [martini](https://github.com/martini-contrib). -- Mail Service is based on [WeTalk](https://github.com/beego/wetalk). -- System Monitor Status is based on [GoBlog](https://github.com/fuxiaohei/goblog). +- Mail Service, modules design is inspired by [WeTalk](https://github.com/beego/wetalk). +- System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog). ## Contributors diff --git a/conf/app.ini b/conf/app.ini index 7f283012fd..b051557f41 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -107,7 +107,7 @@ SERVICE = server PATH = data/pictures [log] -; Either "console", "file", "conn" or "smtp", default is "console" +; Either "console", "file", "conn", "smtp" or "database", default is "console" MODE = console ; Buffer length of channel, keep it as it is if you don't know what it is. BUFFER_LEN = 10000 @@ -156,4 +156,10 @@ HOST = USER = PASSWD = ; Receivers, can be one or more, e.g. ["1@example.com","2@example.com"] -RECEIVERS = \ No newline at end of file +RECEIVERS = + +; For "database" mode only +[log.database] +LEVEL = +Driver = +CONN = \ No newline at end of file diff --git a/modules/base/conf.go b/modules/base/conf.go index cdbe2b36ce..19f587077b 100644 --- a/modules/base/conf.go +++ b/modules/base/conf.go @@ -143,6 +143,10 @@ func newLogService() { Cfg.MustValue(modeSec, "HOST", "127.0.0.1:25"), Cfg.MustValue(modeSec, "RECEIVERS", "[]"), Cfg.MustValue(modeSec, "SUBJECT", "Diagnostic message from serve")) + case "database": + LogConfig = fmt.Sprintf(`{"level":%s,"driver":%s,"conn":%s}`, level, + Cfg.MustValue(modeSec, "Driver"), + Cfg.MustValue(modeSec, "CONN")) } log.NewLogger(Cfg.MustInt64("log", "BUFFER_LEN", 10000), LogMode, LogConfig) diff --git a/modules/middleware/auth.go b/modules/middleware/auth.go index 3224b3df91..82c3367c48 100644 --- a/modules/middleware/auth.go +++ b/modules/middleware/auth.go @@ -5,6 +5,8 @@ package middleware import ( + "net/url" + "github.com/codegangsta/martini" "github.com/gogits/gogs/modules/base" @@ -35,6 +37,7 @@ func Toggle(options *ToggleOptions) martini.Handler { if options.SignInRequire { if !ctx.IsSigned { + ctx.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI)) ctx.Redirect("/user/login") return } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { diff --git a/routers/admin/user.go b/routers/admin/user.go index fa27d11664..7f66c5528c 100644 --- a/routers/admin/user.go +++ b/routers/admin/user.go @@ -140,5 +140,5 @@ func DeleteUser(ctx *middleware.Context, params martini.Params) { log.Trace("%s User deleted by admin(%s): %s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.User.LowerName) - ctx.Redirect("/admin/users", 302) + ctx.Redirect("/admin/users") } diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 154e8308ab..4cc007e9ee 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -56,7 +56,7 @@ func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat form.IssueName, form.Labels, form.Content, false) if err == nil { log.Trace("%s Issue created: %d", form.RepoId, issue.Id) - ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index), 302) + ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index)) return } ctx.Handle(200, "issue.CreateIssue", err) diff --git a/routers/repo/repo.go b/routers/repo/repo.go index c436d38714..4782d64f70 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -40,7 +40,7 @@ func Create(ctx *middleware.Context, form auth.CreateRepoForm) { form.Language, form.License, form.Visibility == "private", form.InitReadme == "on") 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, 302) + ctx.Redirect("/" + ctx.User.Name + "/" + form.RepoName) return } else if err == models.ErrRepoAlreadyExist { ctx.RenderWithErr("Repository name has already been used", "repo/create", &form) @@ -73,7 +73,7 @@ func SettingPost(ctx *middleware.Context) { } log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) - ctx.Redirect("/", 302) + ctx.Redirect("/") } func Branches(ctx *middleware.Context, params martini.Params) { @@ -113,8 +113,8 @@ func Single(ctx *middleware.Context, params martini.Params) { treename := params["_1"] if len(treename) > 0 && treename[len(treename)-1] == '/' { - ctx.Redirect("/"+ctx.Repo.Owner.LowerName+"/"+ - ctx.Repo.Repository.Name+"/src/"+params["branchname"]+"/"+treename[:len(treename)-1], 302) + ctx.Redirect("/" + ctx.Repo.Owner.LowerName + "/" + + ctx.Repo.Repository.Name + "/src/" + params["branchname"] + "/" + treename[:len(treename)-1]) return } diff --git a/routers/user/user.go b/routers/user/user.go index 56bc5f8e37..c34b529ec3 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -6,6 +6,7 @@ package user import ( "fmt" + "net/url" "strings" "github.com/codegangsta/martini" @@ -109,7 +110,13 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { isSucceed = true ctx.Session.Set("userId", user.Id) ctx.Session.Set("userName", user.Name) - ctx.Redirect("/") + redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")) + if len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1) + ctx.Redirect(redirectTo) + } else { + ctx.Redirect("/") + } return } @@ -139,12 +146,20 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) { ctx.Session.Set("userId", user.Id) ctx.Session.Set("userName", user.Name) - ctx.Redirect("/") + redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")) + if len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1) + ctx.Redirect(redirectTo) + } else { + ctx.Redirect("/") + } } func SignOut(ctx *middleware.Context) { ctx.Session.Delete("userId") ctx.Session.Delete("userName") + ctx.SetCookie(base.CookieUserName, "", -1) + ctx.SetCookie(base.CookieRememberName, "", -1) ctx.Redirect("/") } @@ -314,7 +329,7 @@ func Activate(ctx *middleware.Context) { ctx.Session.Set("userId", user.Id) ctx.Session.Set("userName", user.Name) - ctx.Redirect("/", 302) + ctx.Redirect("/") return } From ad31893bbbb1479f6801235ddca44b5bae2cc5c2 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sat, 22 Mar 2014 20:25:39 -0400 Subject: [PATCH 15/38] Update README --- README.md | 14 +++++++---- README_ZH.md | 53 +++++++++++++++++++++++++++++++++++++++++ gogs.go | 2 +- templates/repo/nav.tmpl | 4 ++-- 4 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 README_ZH.md diff --git a/README.md b/README.md index 325c3a9736..504c21975b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ -Gogs - Go Git Service [](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [](https://gowalker.org/github.com/gogits/gogs) +Gogs - Go Git Service [](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [](https://drone.io/github.com/gogits/gogs/latest) ===================== -Gogs(Go Git Service) is a GitHub-like clone in the Go Programming Language. +Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language. -Since we choose to use pure Go implementation of Git manipulation, Gogs certainly supports **ALL platforms** that Go supports, including Linux, Max OS X, and Windows with **ZERO** dependency. + ##### Current version: 0.1.6 Alpha +[简体中文](README_ZH.md) + ## Purpose -There are some very good products in this category such as [gitlab](http://gitlab.com), but the environment setup steps often make us crazy. So our goal of Gogs is to build a GitHub-like clone with very easy setup steps, which take advantages of the Go Programming Language. +Since we choose to use pure Go implementation of Git manipulation, Gogs certainly supports **ALL platforms** that Go supports, including Linux, Mac OS X, and Windows with **ZERO** dependency. + +More importantly, Gogs only needs one binary to setup your own project hosting on the fly! ## Overview @@ -23,7 +27,7 @@ There are some very good products in this category such as [gitlab](http://gitla - Activity timeline - SSH protocol support. - Register/delete account. -- Create/delete public repository. +- Create/delete/watch public repository. - User profile page. - Repository viewer. - Gravatar support. diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000000..0ab8dfdd07 --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,53 @@ +Gogs - Go Git Service [](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [](https://drone.io/github.com/gogits/gogs/latest) +===================== + +Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。 + + + +##### 当前版本:0.1.6 Alpha + +## 开发目的 + +Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依赖,并且支持 Go 语言所支持的 **所有平台**,包括 Linux、Mac OS X 以及 Windows。 + +更重要的是,您只需要一个可执行文件就能借助 Gogs 快速搭建属于您自己的代码托管服务! + +## 项目概览 + +- 有关项目设计、开发说明、变更日志和路线图,请通过 [Wiki](https://github.com/gogits/gogs/wiki) 查看。 +- 您可以到 [Trello Broad](https://trello.com/b/uxAoeLUl/gogs-go-git-service) 跟随开发团队的脚步。 +- 想要先睹为快?通过 [在线体验](http://try.gogits.org/Unknown/gogs) 或查看 **安装部署 -> 二进制安装** 小节。 +- 使用过程中遇到问题?尝试从 [故障排查](https://github.com/gogits/gogs/wiki/Troubleshooting) 页面获取帮助。 + +## 功能特性 + +- 活动时间线 +- SSH 协议支持 +- 注册/删除用户 +- 创建/删除/关注公开仓库 +- 用户个人信息页面 +- 仓库浏览器 +- Gravatar 支持 +- 邮件服务(注册) +- 管理员面板 +- 支持 MySQL、PostgreSQL 以及 SQLite3(仅限二进制版本) + +## 安装部署 + +在安装 Gogs 之前,您需要先安装 [基本环境](https://github.com/gogits/gogs/wiki/Prerequirements)。 + +然后,您可以通过以下两种方式来安装 Gogs: + +- [二进制安装](https://github.com/gogits/gogs/wiki/Install-from-binary): **强烈推荐** 适合体验者和实际部署 +- [源码安装](https://github.com/gogits/gogs/wiki/Install-from-source) + +## 特别鸣谢 + +- Logo 基于 [martini](https://github.com/martini-contrib) 修改而来。 +- 邮件服务、模块设计基于 [WeTalk](https://github.com/beego/wetalk) 修改而来。 +- 系统监视状态基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改而来。 + +## 贡献成员 + +本项目最初由 [Unknown](https://github.com/Unknwon) 和 [lunny](https://github.com/lunny) 发起,随后 [fuxiaohei](https://github.com/fuxiaohei) 与 [slene](https://github.com/slene) 加入到开发团队。您可以通过查看 [贡献者页面](https://github.com/gogits/gogs/graphs/contributors) 获取完整的贡献者列表。 \ No newline at end of file diff --git a/gogs.go b/gogs.go index 2bb80a6644..0bdbbc0697 100644 --- a/gogs.go +++ b/gogs.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -// gogs(Go Git Service) is a Go clone of Github. +// Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language. package main import ( diff --git a/templates/repo/nav.tmpl b/templates/repo/nav.tmpl index d4a692fd03..cf1b7d0389 100644 --- a/templates/repo/nav.tmpl +++ b/templates/repo/nav.tmpl @@ -1,11 +1,11 @@ <div id="gogs-body-nav" class="gogs-repo-nav"> <div class="container"> <div class="row"> - <div class="col-md-6"> + <div class="col-md-7"> <h3 class="name"><i class="fa fa-book fa-lg"></i><a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / {{.Repository.Name}}</h3> <p class="desc">{{.Repository.Description}}{{if .Repository.Website}}<a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}</p> </div> - <div class="col-md-6 actions text-right clone-group-btn"> + <div class="col-md-5 actions text-right clone-group-btn"> {{if not .IsBareRepo}} <!--<div class="btn-group" id="gogs-repo-clone"> <button type="button" class="btn btn-default"><i class="fa fa-download fa-lg fa-m"></i></button> From 47493a0191f3de8aa4e80bce1911f14623cfa46a Mon Sep 17 00:00:00 2001 From: FuXiaoHei <fuxiaohei@hexiaz.com> Date: Sun, 23 Mar 2014 13:12:55 +0800 Subject: [PATCH 16/38] use ctx.Handle to handle 404 page --- routers/repo/issue.go | 6 +++--- routers/repo/repo.go | 32 ++++++++++++++++---------------- routers/user/user.go | 2 +- templates/status/404.tmpl | 7 +++++++ templates/status/500.tmpl | 7 +++++++ 5 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 templates/status/404.tmpl create mode 100644 templates/status/500.tmpl diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 4cc007e9ee..78fe4b25d0 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -36,7 +36,7 @@ func Issues(ctx *middleware.Context, params martini.Params) { func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) { if !ctx.Repo.IsOwner { - ctx.Error(404) + ctx.Handle(404, "issue.CreateIssue", nil) return } @@ -65,14 +65,14 @@ func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat func ViewIssue(ctx *middleware.Context, params martini.Params) { issueid, err := base.StrTo(params["issueid"]).Int() if err != nil { - ctx.Error(404) + ctx.Handle(404, "issue.ViewIssue", err) return } issue, err := models.GetIssueById(int64(issueid)) if err != nil { if err == models.ErrIssueNotExist { - ctx.Error(404) + ctx.Handle(404, "issue.ViewIssue", err) } else { ctx.Handle(200, "issue.ViewIssue", err) } diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 4782d64f70..0f1ea31235 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -86,7 +86,7 @@ func Branches(ctx *middleware.Context, params martini.Params) { ctx.Handle(200, "repo.Branches", err) return } else if len(brs) == 0 { - ctx.Error(404) + ctx.Handle(404, "repo.Branches", nil) return } @@ -123,8 +123,8 @@ func Single(ctx *middleware.Context, params martini.Params) { // Branches. brs, err := models.GetBranches(params["username"], params["reponame"]) if err != nil { - log.Error("repo.Single(GetBranches): %v", err) - ctx.Error(404) + //log.Error("repo.Single(GetBranches): %v", err) + ctx.Handle(404, "repo.Single(GetBranches)", err) return } else if ctx.Repo.Repository.IsBare { ctx.Data["IsBareRepo"] = true @@ -138,15 +138,15 @@ func Single(ctx *middleware.Context, params martini.Params) { params["branchname"], params["commitid"], treename) if err != nil && err != models.ErrRepoFileNotExist { - log.Error("repo.Single(GetTargetFile): %v", err) - ctx.Error(404) + //log.Error("repo.Single(GetTargetFile): %v", err) + ctx.Handle(404, "repo.Single(GetTargetFile)", err) return } branchLink := "/" + ctx.Repo.Owner.LowerName + "/" + ctx.Repo.Repository.Name + "/src/" + params["branchname"] if len(treename) != 0 && repoFile == nil { - ctx.Error(404) + ctx.Handle(404, "repo.Single", nil) return } @@ -154,8 +154,8 @@ func Single(ctx *middleware.Context, params martini.Params) { if repoFile.Size > 1024*1024 || repoFile.Filemode != git.FileModeBlob { ctx.Data["FileIsLarge"] = true } else if blob, err := repoFile.LookupBlob(); err != nil { - log.Error("repo.Single(repoFile.LookupBlob): %v", err) - ctx.Error(404) + //log.Error("repo.Single(repoFile.LookupBlob): %v", err) + ctx.Handle(404, "repo.Single(repoFile.LookupBlob)", err) } else { ctx.Data["IsFile"] = true ctx.Data["FileName"] = repoFile.Name @@ -179,8 +179,8 @@ func Single(ctx *middleware.Context, params martini.Params) { files, err := models.GetReposFiles(params["username"], params["reponame"], params["branchname"], params["commitid"], treename) if err != nil { - log.Error("repo.Single(GetReposFiles): %v", err) - ctx.Error(404) + //log.Error("repo.Single(GetReposFiles): %v", err) + ctx.Handle(404, "repo.Single(GetReposFiles)", err) return } @@ -203,8 +203,8 @@ func Single(ctx *middleware.Context, params martini.Params) { if readmeFile.Size > 1024*1024 || readmeFile.Filemode != git.FileModeBlob { ctx.Data["FileIsLarge"] = true } else if blob, err := readmeFile.LookupBlob(); err != nil { - log.Error("repo.Single(readmeFile.LookupBlob): %v", err) - ctx.Error(404) + //log.Error("repo.Single(readmeFile.LookupBlob): %v", err) + ctx.Handle(404, "repo.Single(readmeFile.LookupBlob)", err) return } else { // current repo branch link @@ -239,7 +239,7 @@ func Single(ctx *middleware.Context, params martini.Params) { params["branchname"], params["commitid"]) if err != nil { log.Error("repo.Single(GetCommit): %v", err) - ctx.Error(404) + ctx.Handle(404, "repo.Single(GetCommit)", err) return } ctx.Data["LastCommit"] = commit @@ -275,7 +275,7 @@ func Http(ctx *middleware.Context, params martini.Params) { func Setting(ctx *middleware.Context, params martini.Params) { if !ctx.Repo.IsOwner { - ctx.Error(404) + ctx.Handle(404, "repo.Setting", nil) return } @@ -307,7 +307,7 @@ func Commits(ctx *middleware.Context, params martini.Params) { ctx.Handle(200, "repo.Commits", err) return } else if len(brs) == 0 { - ctx.Error(404) + ctx.Handle(404, "repo.Commits", nil) return } @@ -315,7 +315,7 @@ func Commits(ctx *middleware.Context, params martini.Params) { commits, err := models.GetCommits(params["username"], params["reponame"], params["branchname"]) if err != nil { - ctx.Error(404) + ctx.Handle(404, "repo.Commits", nil) return } ctx.Data["Username"] = params["username"] diff --git a/routers/user/user.go b/routers/user/user.go index c34b529ec3..a0321f187b 100644 --- a/routers/user/user.go +++ b/routers/user/user.go @@ -301,7 +301,7 @@ func Activate(ctx *middleware.Context) { if len(code) == 0 { ctx.Data["IsActivatePage"] = true if ctx.User.IsActive { - ctx.Error(404) + ctx.Handle(404, "user.Activate", nil) return } // Resend confirmation e-mail. diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl new file mode 100644 index 0000000000..4e836b228d --- /dev/null +++ b/templates/status/404.tmpl @@ -0,0 +1,7 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +<div id="gogs-body" class="container"> + <h4>This page is not found !</h4> + <p>Application Version: {{AppVer}}</p> +</div> +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl new file mode 100644 index 0000000000..9a00eb1ff7 --- /dev/null +++ b/templates/status/500.tmpl @@ -0,0 +1,7 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +<div id="gogs-body" class="container"> + <p>An error is occurred : {{.ErrorMsg}}</p> + <p>Application Version: {{AppVer}}</p> +</div> +{{template "base/footer" .}} \ No newline at end of file From bdd32f152d73f31beffa854fb7382072043ef235 Mon Sep 17 00:00:00 2001 From: FuXiaoHei <fuxiaohei@hexiaz.com> Date: Sun, 23 Mar 2014 13:48:01 +0800 Subject: [PATCH 17/38] add m.NotFound handler --- routers/dashboard.go | 7 +++++++ web.go | 3 +++ 2 files changed, 10 insertions(+) diff --git a/routers/dashboard.go b/routers/dashboard.go index f61d67b7de..dafe9f31ec 100644 --- a/routers/dashboard.go +++ b/routers/dashboard.go @@ -20,5 +20,12 @@ func Home(ctx *middleware.Context) { func Help(ctx *middleware.Context) { ctx.Data["PageIsHelp"] = true + ctx.Data["Title"] = "Help" ctx.HTML(200, "help") } + +func NotFound(ctx *middleware.Context) { + ctx.Data["PageIsNotFound"] = true + ctx.Data["Title"] = 404 + ctx.Handle(404, "home.NotFound", nil) +} diff --git a/web.go b/web.go index bf654aace2..a0f9f7051a 100644 --- a/web.go +++ b/web.go @@ -163,6 +163,9 @@ func runWeb(*cli.Context) { m.Get("/template/**", dev.TemplatePreview) } + // not found handler + m.NotFound(routers.NotFound) + listenAddr := fmt.Sprintf("%s:%s", base.Cfg.MustValue("server", "HTTP_ADDR"), base.Cfg.MustValue("server", "HTTP_PORT", "3000")) From 1c01db4019abc2460ac1743f9675c9effd211e3f Mon Sep 17 00:00:00 2001 From: slene <vslene@gmail.com> Date: Sun, 23 Mar 2014 15:21:34 +0800 Subject: [PATCH 18/38] minor fix --- web.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web.go b/web.go index bf654aace2..0d31161854 100644 --- a/web.go +++ b/web.go @@ -95,7 +95,7 @@ func runWeb(*cli.Context) { m.Group("/user", func(r martini.Router) { r.Any("/login", binding.BindIgnErr(auth.LogInForm{}), user.SignIn) - r.Any("/sign_up", reqSignOut, binding.BindIgnErr(auth.RegisterForm{}), user.SignUp) + r.Any("/sign_up", binding.BindIgnErr(auth.RegisterForm{}), user.SignUp) }, reqSignOut) m.Group("/user", func(r martini.Router) { r.Any("/logout", user.SignOut) From 1a16b3e99a0be7dbfeba1e683d253a22db5d1af5 Mon Sep 17 00:00:00 2001 From: FuXiaoHei <fuxiaohei@hexiaz.com> Date: Sun, 23 Mar 2014 16:06:07 +0800 Subject: [PATCH 19/38] repo options ui --- public/css/gogs.css | 8 ++++++-- templates/repo/setting.tmpl | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/public/css/gogs.css b/public/css/gogs.css index 78040bee51..65a6c03d71 100755 --- a/public/css/gogs.css +++ b/public/css/gogs.css @@ -346,6 +346,10 @@ html, body { border-left: 4px solid #DD4B39; } +#gogs-repo-setting-container .form-horizontal label { + line-height: 30px; +} + /* gogits user ssh keys */ #gogs-ssh-keys .list-group-item { @@ -575,12 +579,12 @@ html, body { min-width: 200px; } -#gogs-repo-clone .dropdown-menu{ +#gogs-repo-clone .dropdown-menu { width: 400px; padding: 20px; } -#gogs-repo-clone .input-group{ +#gogs-repo-clone .input-group { margin-bottom: 15px; } diff --git a/templates/repo/setting.tmpl b/templates/repo/setting.tmpl index 38c3fd3bcc..719547b1a9 100644 --- a/templates/repo/setting.tmpl +++ b/templates/repo/setting.tmpl @@ -19,7 +19,41 @@ </div> <div class="panel-body"> - + <form action="/{{.Owner.Name}}/{{.Repository.Name}}/settings" method="post" class="form-horizontal"> + {{.CsrfTokenHtml}} + <input type="hidden" name="action" value="update"> + <div class="form-group"> + <label class="col-md-3 text-right">Repository Name <strong class="text-danger">*</strong></label> + <div class="col-md-9"> + <input type="text" class="form-control" name="repo-name" required="required" value="{{.Repository.Name}}"/> + </div> + </div> + <div class="form-group"> + <label class="col-md-3 text-right">Description</label> + <div class="col-md-9"> + <textarea class="form-control" name="desc" id="repo-desc" rows="6"></textarea> + </div> + </div> + <div class="form-group"> + <label class="col-md-3 text-right">Official Site</label> + <div class="col-md-9"> + <input type="url" class="form-control" name="repo-site"/> + </div> + </div> + <div class="form-group"> + <label class="col-md-3 text-right">Default Branch</label> + <div class="col-md-9"> + <select name="branch" id="repo-default-branch" class="form-control"> + <option value="">Branch</option> + </select> + </div> + </div> + <div class="form-group"> + <div class="col-md-9 col-md-offset-3"> + <button class="btn btn-primary" type="submit">Save Options</button> + </div> + </div> + </form> </div> </div> From 13b6e06943b8cb44bb246e1d57856ed3574f6184 Mon Sep 17 00:00:00 2001 From: FuXiaoHei <fuxiaohei@hexiaz.com> Date: Sun, 23 Mar 2014 16:20:47 +0800 Subject: [PATCH 20/38] 404 and 500 page ui --- public/img/404.png | Bin 0 -> 9776 bytes public/img/500.png | Bin 0 -> 12087 bytes templates/status/404.tmpl | 5 +++-- templates/status/500.tmpl | 5 ++++- 4 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 public/img/404.png create mode 100644 public/img/500.png diff --git a/public/img/404.png b/public/img/404.png new file mode 100644 index 0000000000000000000000000000000000000000..1f0ee0ef4953defd225425d52531cd8b27be9ebb GIT binary patch literal 9776 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 literal 0 HcmV?d00001 diff --git a/public/img/500.png b/public/img/500.png new file mode 100644 index 0000000000000000000000000000000000000000..2c122fde93fcba1d61c8412867a68b2a5ebcc44c GIT binary patch literal 12087 zcmcJ#XH-*N@GpE4I?|*Qq={fbDH2*lKq5`0_aY!s2pt5ZLl6W}La`yeDlIexK_C!N zDI!&x2q6d>ijYu4)%W1@e?Q!BcinYw)`Fcfd-lxi*}pyIB$^oMv9rQh0RUh((7$R1 z08sEPKw@SD{|6NjO~HZ1Pv15G0FLn-{Xu}tY(W5EHFQTJO-#_ffxZD~Uq5jJBvRb( zuCJ@R*Bt-|nanW{ws}h9*O;N*;k448pw^~)#Q-NIam?LZwuM|zfLn1~+*+R=JY==v z<vX6OZ~5{5D64+X!yox)8r`ludT969(m{1qY^tO_GK{javOm7xLe3^1^oI^`Vvci5 zr~NAQ0MyJLs3RuCQjGOK_TVv4K7A6sIM=V<4mXBR06?V+4T%b@(L8`u`+^$zjmn<l z?wb7@+hihb4Tz@!q?6p_V~|ucpj+0Wn-}O71ZvFR-ev`!0f1L%g!W0GoE`YRX`sOX zOq4yH5&$L&Pn0nOSO}1DM(-3<s~K>%j4?Y8C3FD!IIWYi^n@x1#`fJsJtzjx0NhKq zN#)dX1~3`g=f42rs!#wa%F{<DeuFMwez=WP=H&OFQ6B;jdY|{DUmsTFZG1im3tp#^ zen|Z2@T%3mC2*F@m1X{9c6yd1k3O01Mn4+>z;=h)|EOxFl7G$5{&FRIQ8qsn{_&*x zp45<7oB7j{1f>VKj0Px^PCtK+(fM^jYJ%E4%j|(kD*!#SdsFc{OTHcJVA$^CP}Ku1 z(cA))Zgr6ptjCV6NVyc?<>wx2{8gOwB|VPX27mqYJ&X$YXKH0s5{hCE{cri)t1?R_ z_tI-C_|V9bf4Uw-A10q2`T?(XIM4TJ%_gzY<fLI&=zFuYs}eoWe{e5GUtAM=ameyC zm>s~y8xxG)G1d2a5u#Qh(8nA$!}kEKsErFruw!WemmB<IC=CFfw(X{9$v}X<^x`=H zXju?+D19byqmvl`uI9&_tGU9&*3OF~aAdcOv=9WKw-~Q#v7I8Qb7?cqV@|LZ2)?)? zWI;eo(AU?{!Sau1&T`eYE2y%QN!$)G%YFik&Vsoef-UhZ^jEr>d2UG{9v*i|o;cah zERc~*fAW?-{0XL5eDNW#>B&#scZ;EA+8${)^(EIY+~rlnI%w^se(x5)_~_tKi=M)> zm^Y?$BhVnN_oZU6N1gcrLxFJ4o0u1co<p!Y(aV^3g(XAR7lHzT$VbU2%#Z?yYtpw{ z4oL|*&WScK=`McMPa7qx6eX01jmkYfVM6XL3iB!GV+_x4X2&?L4zK&Esd_dyY>=f= zza{Iqo0zRxV&fD#PM>^QYFsB3#=ZM7#062CYNLyjUX>7)$dz(D&B5dJRR0u5&y#s0 zRtc%piw4IGJoUfpUnn*&))?k;&%PnOoa)^}oM(Q6!HK7*HKcN;8b8_Z`7qzWi}>{L z#|@UkQ)b1_xUVVQ(fmpOD(|9pMt-?zUs>)5bNNF=o}Gbr{yh^p^Mo=Ucu=NPUlu>I zxA;QQ=wPP1n>!6Hw;0&OG94?blRlU}?76_SNM4NH3}$1GXOHdDcR3^EDHJYLBG@1# zmvQ4Y!`JkTZVUPGi^3UhFF9ZOTH2i3J{Q54_L=)R;<?rHsORKz3(EjY&C$^EV-^*b zoE45%k*{?v-<r&ov5fFnjFyR5HkT<^^_vBorI-wuIu*4#9p2wVvA>Cbpqn4BUu1!5 z=oG?A@ONIkvu2|`cGrq-`Ml>#Ll^ruyT5PTW7rqICl`Lh92rpVigQhK&H3fu2jS?f zB~jJBWnb%;=x6T_zd(0E^Fr;IN2P0JRHe#f*ktRZ%gWsq{*~oPN|0ZUR?hjHErd6M z7R1t|++^DHpy_+iz>>yS)N<$|%hu;Dj>YLAy)Ey}qAh_X${1xxOhq2fteh(3rJ#zU zen~|wc)oOR9HPd}mLBg#H>G_<7Y!K{oo<v~O>mo23K7t7#rd9a!NG}lraaYexwv2a zUi`86(|qgMzGt49u1!OAUjx5<={`S&o51z_PF5Su_RPK2`mN>r0rtTwo$OE79xFfY zdNKJTqL<-?;R_n>rIm(NsMVI$IL=_qVfp;B_n5VpiLiARTNWa#_g!gaR^`WgS>7|e zKX_Bl4ximJ_nB1qeej$0cjj-#6}9bK+fv)!+XEtBPH>BiT?)JuaW^pN%RMi@mgVAE z_=mEOWgix;-Gto=0&+H#1YTcH?d49tmo8xLUGk{JCoin``A^tt({TOQt)i^vtf|Ux z-XqgrE`N%w*f*5T67ISA;WFjQCY5IM!~e@%PDxft$3`ky&heh3t>Zpk)c9?-bnckE zclY;?_9T37xHCfI@}0f@MLLa64QO6iUP9~Z){`@rTP35$qB5eG4s!P|?e!j}?6l2> zuJf-MF*rloAwMDb7+B0(I!`(=`V9J4OiBz`hGs@NHVr;)HWqeUvAa)w2?FcFyC}se zxAU1pe4m+f`0RxD<iAMY5FI<796!K3!xiQGdiTcSmv_t$NqSxn8^zp(G6Z%bdC#7^ zST4I{T4x^UA4oethMTwDcf%*IrIaPTzx3VX-Q9a`I1eu~FVyd>MoeRQV@ycGj?P*- zcOcIxmX*Z$j}sp+C(Z>x$zGX?+4GqeF9K0jKc@=c3;bH_@68QXw^g!DjZl5?k~{s4 z=FeDhZ4)^`<DVryh9W%caaj+Hby^Ftg{HL)jn~wl{K~O<zO6caDH6GdjyKi~>QrPg zHj@cW6MFJqa>LN->KG9>@Xi0*1+{0{&$5fm*xg^$$&AZ4oM|w2m63VMJ|Fta`?K)V zgwM2j$=!gB?|fblZVVOGnN^u^@ZXRtl?kyIv=3?2$LrgTD9*V4S=yC6cAq~2pIT$m zSo3zGF~oL_bh1sSnP2j;xj?Y2!cDWL+ad1M;mBTe@0%BgCO>S<EZ03kr`D6rVuxS7 zLdx8?hCeTR*3;`#wQ2Vr#dquZZA*$r`l!8C^f`g&PB=eYY1K}RZsV^X)<#aF5nG|& zlas`24WCz*^_Pp6eQ-3}w(4tmk1L~t@l^Io;Wg|}VJGiv#K3|ukFDgsy1vhSbV|3B zR_wpL!+l>bP%hdI%Za|(Yjs#z(a`!$dUC97IyiiTBUXg(5|Q%kcT2rYh_xqPCvwYY zc=i5t3d}{gsqI(PbfaFd@u%FB@K@nX+d7nGk3e6qsp_`2H!R*}%2-i-gx}9BjPGRD zN@j4qpWQi*jw?UgPtUxL2%j@qDCqw%^`UPjBf4RWd09VHW*E^Hgu77WoqxFiPgzJR z$`NrC(Q9q^8%0r=Lj`pyrC8_Y=bX8`wmX0S>0HU}lH434&C4ShQ=zmQ2N|9>O>g4e zkOR4z0)eYH8k}f78$@!-O=^qZ=1H2%cd3T!m6xk7>qYq;&gxg&`!3(1^{!Z}R|Rew zE?*j=e2w1oAdCi#?~Kdd%nQX+I)4i{_()HmXq0N&+PSyAA`r~FdSmKlSV~CsLF?q< z64M^%Qgq#)+B*bq(vCC1dEkRBCxwG0A@|R-zh4t(hxNYbjY{fDVpDR&#Dv*l`1RZU zPiITsl<*9Ej<Wh2x*SkJo;m!ne|^B_V1jyV_whhmRcP79T}nk+HKI$hORS|o^zz#4 zgUbHzk(5SC(@Ym$EaS|9)*q9Np*>Vr$)GV!gB<baK#mr-u`e~8rwB%y;wXJHLjVYq z0su@b0POw+$3-v}l?Q+&X8=IF1ONfwCr+Pq0pQeagR568LMDIbguc9KnXwwpAi-iF zE@8nO$7U%JhttwW(T_ZwFI1gn7)l=0mxzVGGgqOxyZfjO)&=^1`hE`UGB?8hpmdP# z0%=g3<>b?A7=1Se?u*B}owcP)Q@QT0ig}thoU?QzerOm7jUITaCrVEw+f!(jTe*?j z-<n^UoDq-X(th~MMe{-omo{?&aP*Ox0gwQPGXV3^0MR-cSOINtFaf|{CE}o%qk$JV zqWwRf^8c9q!*;<!snV4GexvLFrNd_8{-eLP?)s7tEI--dis$Gv*aa;KN>E^6-~v~9 z><jIJe9KZr08RHKUIM#DxIsCDJ2T3&sGbmHs)Hh9VrfGY3@xl)bjm;$Mj+-V;UdY_ z`7AZ_K)zBz^d-x)t=1G>OQOAfMOBroh&C(mjS2A98t&FAVo()T6;V~&Q#@OMxCb!? z+89LXSLxn;PWb|7+|npO?1fVBZJZMxw<>u7Uf>Ho<|6yw27=|}yvq>U{YO1Qd~$OD zLgb13F;JL{CrA>eiTh%ZiqYmoPmB3T)}Y^8L`~=v`?D>Lo_aH&lyjP1P*E!L-GwTN zGFQT@A$i!_7+)UQuS7ZrlU1s5WAY*9J)yq;y(PgHb3Nwxy81nAd5dvD@?myBK#XV1 z8)8YVl{SPB#*F?deHwdCX`4<%yK(+cG3^fzK|*)=<*^7Rn_~xJ&iB=;ucI_clEi(c znA0&Ji}%TIh%)qN0C+evT2ZxexTdUD%ukjIr}B_i)XL|$NWK^{#uw1*-YADNhU66? zu7y=|<pYB_2@>F3sI2C0vE#l(i=$ZGOkNh`IFOn(S|6bMFdu4*EhLh0$%X$hM=`&o z|6Ofp1d|dZXYWhQ?vL=m(3QH-@vC`g<8*SYM-tuVu)GiTu#xZw00YML7{q&WzS#8s zE`giW!suAxPKbug+|JATl-W}K>k|0|Kvtqguu&Z)v>^Ez=&!S9u_yCVeIP!k)k|Lq zjV+lIPLdj>DHTyfm7a*rjUzH~n#DwtH$weYEoT>y0ueE6v;s$H^ki|QQYp>wqS#@J zHM0qTj^~oMOelfxQGGhhN0YsO)WcY&AP&>FBQ1ybr882Z{?q5k&fuaCAF~*nT(a?G zAf#im76P7n(a|h*h$=%7n_*5B@g_hw@*f>L<?uOy#-e#_)b23)&xywRcBGc&e`Nn` z`B1|7j85*O*B{=N!K>)2M(y}URnz^<9VFv1GAt>5&&hZGldX<RK7;y8RwTEt!asC| zPRrBMVosgZ%sytFF0*4<JhY70#z3_sA7IK5>REPkMTGG>D?U$$t07?yk~m9E%1>}t z94UNIUH~RVU$;p0vcm>L|KjS)?dD(|cV^Yc6-sIAEjZn38Y_eqz;Zz+GjIF7Qjp)7 zHp<9qt+UcmYUTX$*<C~mpH9A`6ox-CSobB+s98p+;(*l>f=#7mA54<DjZgE8TTl}H z=P<c}>mL7O|8a;`g4<Vj2hK0UcB7U4Y<{TzrN*ck8G-c{Y3zSyoqv8L_%)P<U*gy~ zW$Z=_=da@AU4qoQy7K=|ft@9Uh~yd;`JbYSR9gOq%}BLe9KS1a*pg1Jb^o80jE_`1 zhxLV^>a1!#>!3~wj$Ht#{CODNKRe1JGhDsC&VGZ#qFJ+F)nB_Ychb3P2=NG%7BwVD z`e#H@ZGwdevtfb4Bfq)4v2(Uq;m0esLSv?5;i00BJAr>j#3f*q*?r8>2fmM5DD5^P zy)2s5P07@AV-+!3Ewnc1yI+KqITn?8+og&}Eh>Y}hb=UslVJ#tlO@s3M$dqv<6Xzg z%225-5_s?43lHLVmSDy%#QFMGrsf#99oy*8YPP+0<LuWTc6h<ausw;I@{gKLZ2_y^ zalMsf^H0LZ%vURv2!YP&F+^{s@^y`$_2-r&i)%Gap3{U@r07~GOH0EN#WH|vHGQ3w z!l}JyKG){XQ4alLgIb^tv)P_Ze_FrHa#EXUf39>~S}uV!2yLM*`emfGHkD73(ueKv zP>16T&z=zVVAXV%p?O1WFSZ=d05+@;Z$=d+_m3qCl6xO-S0;ZVWMQhI&7Olc$dI(Y z2<L<=ntw8>nJee_J>qG9B4Y8)f+0@>Rx1drF!;G`jUu7*%lOh0kqjoM_oEmF6HNQx zi_{;eEH}1nYq7!v2Vc4sCyUXM1??*s2pNQ41@|wEtC9yNdd-|7v#DQpaA6JO;m8dI z!ahRj+`2}r(C;lrCkQAOJJ1D**7pyX08aSKV|*B~MHn}s`Ez#hLFWOYULvK-bL1u8 zgI*-tZY}Ri7T${s6%e8@OH^!_Q;Dt)u|z!<Pm}>h5?Z19$?rUvipC@o;V)k0$H2pO zohJ4SCe28{%@U$TV9|ia)t#z|M`DVYuZl|`54WjiDSWkg(MgJVZu=v=04tEeh~ZU9 z_=(6j|El`TudD7k;vQ6t@tss@E!R$=f~H2!6(_zejUr-Y_kTM46gq%|&@I1m)|aC< zNUP+vzk*+W0pd?QHK49|d<j8nh(FlD1o*goKEfcjum%^(^5)Tc+3lPF&v913)~s;$ zyE*Yh?pH|8Bi+XVs~3?$2mRrHE1!)m1(D=RN#V3-2WW__er>f~Daj3ec6*h=OcB>d zX@L77rD=yb76?5HS`gxWBtf87N#U9-=?pT+0rK0>zmgxKd2zl=$Mnqg4=KUNeSo^* zW@{kaJUitP-nb{_G2+rCdoaXcL~7Z8&iZgyWUvhFfG$lAXPFszjkk(+>a)bejeEfS z?p})mzYnG`@Vm;3@rCDg*ck@&=it8TK?rutsxJkjmCf5tyj@bWFuYKgalua-zLC9+ z$#Qq<tgd7J!smAnc;R7qmEmTm&u9(M*&ozrrkDWo{LslG#YaKKJ8cR2l0oXQ!yb>y zKkcdH4N!v9WkUn|N#Vg5+S!3FO~K5K-RbG+J(w1RV8M(w4h^uWqC@au1K3CX%{x7} zQVa5OjiA3y;ry!#i1@Q}yCJ1hxle!Cu~R|6zioJ=-#RZm^D=$X$teI`kbEId#nAP= z6$8l?nVXlEuWL$CT$p}BEScAXG=3Y*&MS}vm9qf!!F3Ag5dK)2Nqg3eS^3<({4RoB zknsA!A&bYeO^u(#3(Jw$q5_38H)zY<rhvQvU^ewnr^Zg(_@&0L-FB7IEs_2A$*0Zc zUnwb|v+sa*gkKb^z|$UMzw9Q0I#q&!pX3e0|1>hi@|Q<!hujbHVp>(Hv!V&EwLH-9 zO-wQ^a-o=0QOyCXQOFJdlr%NDMLY=Lzge|dvp!spqcCipy{v|{wb*_AiGH~;OM?=s zQe&m5IKSIM4+l+tOLLX+o^WPLo=|201Hb_ABr$MY^AE6jeWcr0(1z8u^<i-ul)Qn7 zjZhYJwht(!lCl_{DEyN1h(0Ut4-fIgi$#o)11h%w!4I>q7?0^n3W+*@N0eY0+o_Jk zHxEvL*R{snuIjty=6x<OTKx&65=gS+kepK8WAfwrB62_lTh-`?cy-OQx>$basx;!w zOD}1RzvgZs1ERRKA>xLN?ww_zAIof>y==<fKq>jmwrqnAtQ1w4-!)M1*bMW{4;GV0 zh@}Yr(*pD5KgYcodCk}YRaJlxRcx|qQ~F2yMQNltMJOj_29|lG!2e2`3~C^Jv!ai{ zD48oJR@8X{lzw6%f_F<JwjLo`0Hpdeqp#P_Ht5n#{LUMGI`R@#ntYnQdi1NVdLCO+ zq5&$ofEYPUWz+D!tsxy89Gr3m^f5uuoxOJO1WuM2P7OL-igvqaoLfC@e=hsG=;2@A zDU;1`5}D8jI6*Y@Lf%g1g;4a)(96eBFZmq{!nvkeYY5v_l(2_tTw=t6emK<(!gf2j z0=nOG-4UYovHcd|A+K=Mc|TDqhciN6R8Yj*G1fmdzz6PkN?vL&Zo`p;BJdG{;@q&B zkd_~KC%P#n2TljAos7{aMwR?x_-nckNh52|*6R0sk-5!+a4=>o6f+j${P7oDLj<(G z<mBDW#dqA&p_Cbxh!@oD>Jk<wwJDLC7gTdKD7SnGT=E}GL}ZexN3S70l`b=4h2q14 zAFSbTzbdHDtU~ES#5aIJEeU5QtU}zlucJWZ#Zbsj$-=b`2s=MCS<#}4b<VT?^PruG zNNlJ7|4e;eP>-<dN`+z)fkA>qzMLMAetcERhI5KJdrDlwIlcg41#_w(vsaF83c}Cd zJhXL>kP9VFj*5YM#IT~a7nl{-X+x63=&=M}aAHmB_Gec8oGNxCqpWjIT74#WY^Q0w z>)_sK-c15R(o;$2Xst3}@U{er-IZ<zG6p#L^<#T(iLAsQ*ksTRj0@UU+=>&vyF7$e zfyc@O)THAS-dKGAagy_^X)%z>x(`R$Cvz;+!8sUO*>HFJBSi%>o+5N1b4fdEtTl>E zxxVm`X7i;%K~iLV5Ze;IU2O)9ZX31)J@AIMmOTe2I8~Q!T$TI+BKE4gnu%>x;ajTM z^4%Z1x=8$`0uF3X{E|)j(|@xv^UVz);wxN!^!K6^eQf8L$y1dC=($Fe$_IorY#n88 zq&{=IG?=p*i7G5WDF3|OuOc*A%`u(`3aY#kHsxapE8&jP-O0FXO?R<;&JXEXFL658 zS9zp4VYI-%5u7p)P7$804&($Q&!sx02}n+SXAD7;J4#!XXd}YIRyo?49nv~vLJ>7h z=%hymd41{0_J!9uiNt`=a}qd1KT^CVO5!A_BZHC@>#Qz;YA7Qkm0ojT{o|J=(m*v_ z(QU9;koep`Vg-&yft}(sL?_IyLPTxt;Ld-<wf_-Ee;afx1oDAY#EU4!Tx+ZZp@G>~ z+N-BA83{T-a&`z2Ra(tahMY4&2F?FmGEOPL&vS|BftNND@BovkR0Xu=_%jQ7qAMMH z63Lh2_m=VzM@|zebvl5ELd&V?ccnqKCYM#;Cvq&n;j)q~*5Vah@}Ns@&41jejNkLH z5P=nevD;Hna1XRxS&t?IU@F5iK{gCwh18-WppA$kgszZX#fsWmL`gv_qJ{%&3d)`f z(z}EFP4u2XwKD6J7LV*G5R=TdBq*y{J~nZuH2GE#nk}7d>G}JjMVEXWRO``=(O;>h zoivO}i9^?!-84^ZS`a$viP-xw!9)|F<{pDqto_?g6VmBt@86fijC-W)m3v?%*v+~` zLmlfoxkL<Dfif;7lsyld<rvI7#`m>Nmp;;S>n`XL-^KBlhD}7|$hZ4xzuE)9lX8_A z{dSVd{@Nen7j`X(ElteQ)5|v%5f7$?O7F<8I0^tR74OLtW?vf<S$)=r><o~wVK6K( zP>fp;0$@`X@W~is&&nGfC-Un(?67paU=EU*mPq(x37>Q9c&s=E^s-ccjQp%d<E5UW z0#vGSv{(qth8-uhr(q(Tx)0^kpfqz}rx9G`V}jqrgMPmN4UzN(CDLbfy})Gf+{hI- zH?q1jq=v(W0SAn!lE;oaroGRl8d3U&Agyovbw#;Fq7qce91PMr*5cAS(aDK_*b-4T z<w70Llv)??ZPHh;l?1e;XOP_p>ct@;T$6FarMUVYIHSrGB_ltnFH)jL<`>*Pz_y)V zj(mh~E&^ZD6h7c;J;eFyX4xN0WC9t2p-ZMCUJdD6Wr?ibfom~O(BIf5Qf}xM(YWMm ztla2=e~CZCt`U6|hONJsu46Hgcp5Q9*{tguA*VCeNe3eSi8wtrG^C(gVjjB5r**i= zc5lb}dtE(;!Ex+3<`co2;KU<ZsK^*1Hd_+73?rG{mhsDVY?r}u2Yg$oCOz*Y!7hW5 zz%fBsxd=fHGr+ZKA5syp%D5dm>ea8DaGhslq6}N-fgJ)p&N89X=*SV!$e@u;8y5y( zXaPjDZD<NKFR0%T&|px1`T=3Ym~5zD)HUdO8Gbs7O);>>y8>C%Wg>zM;D*Kvu_}HK zTsi|DON-9l{`bN$8H6qjIl!vxAP)Wxw)au_<msp+dGJ0c;lNhLFA1hS#ovxAkb&*7 zg6X&jY=N8TNaA;9q(;Y#2bgsw@C;9QbZ-o)EmkPR|3Y0Cf!VW<Dj-gW_Xr}xfAE8` zfVP$)>@tCjF~4?`UEZS0pYV(xt}MJBan)$gBd%a~!YrZF6&bX!WV{sw3U!1QWoyi{ z<8++l{N%rBn9l3*2deU?Bb7P^BfLNekU|EXc==DH%cc-5h?aynBNiK%PB|~{*ab#B zAZJt{$Qf5aoVG}ae^sGb6!0cJNvZC%4_IG2L;taGh%N%y!OtRyPcWU2;}6h%22LU# zrNQIl1#2Lf+JV$3JkZ7tV4P~g{7{KLTPLKr_7mbYU6IR}jc<3#SoWr61sDX>a$vXO zm(YDZj>8`4wy@&`Kt31OzR69Ox@3OmdtYFl?7IrGpx8tciccvLRat=O{CQhW$k(}& zI5?5dqkklM6gC_at!US^`BG^Ng!Krcz4B2LiiT4{ryYo^13iu|i%SC=h)A$h1${Eq z_=J0axw|I0Ou^HOMr^zN@ko}@Te@MvDv`2t<N38YtWgY4o`_YdeY?(~SYt)k0;V;r zkTs9<On*K&i<t8a+PHsADPX&sIO^<Vs4(1Ifzi%^$zZ=YYf{)LQs!(pK0<kNG6(*L z)sb*luoPk3Tn#<k3btM}9pZ$AcEW5co-3enaCq!WC+&&pvt1VSBTvhxzF@6VBL6Oy zfzaEzr2k|6t;)P!^IEG1cro;%2C4C`HPbte!>;Jc7t6(_pjGv)Q0W1y8mOQ;>HM`M z3C&3?ZS*#HN5L7fZft$|TNRp5LD)CTxp*XgkEgmTE;)?_eO}=o*UTe6AM<NJ4zk&` z?J4#R+Fbx+uwosf)hT0blmXp?3>Vn-7`*P1>e!B5ADDs+3&Qt8^ZY5Io)K~x?&$s? zhJRkefPK}uB$$Kg>~|5q#u)YGq&l_R!>JEG+?@50tk6%_GT_J98<w}m)(?N<<h``8 zFlY|&f%{-KIN^dz*4Bm@3$CGfdk#hvst~S1i_<y^9_qJlGs>)Md=SdMbGywBtlfgD z>Yx&85${eH*LJ6hbqTLjNX97WhS3rQmH34++vl*WQc`&1&`OV^Ko2~S)oYoxi0xCw zYo!vJM6bGPBQT~&=_*|-Ef9)$JA|LViriRhG!kb3Z@oZxr(gxjTpRY7IYpqJ)OA*O zf2lG)j5aH%bS*4zoh{cTP%gBh;Be_yj>eH47tu)yR$ag1;Eu04DE#Ksxl>0)g-E4w zeM%Lh<3rOT9PMmB$$;P&8{Xp}xj(v8RaGSrxPEkIii;FJ*@(!UDqZuGjAElRf{$4v zW7I%33h3cc-nSVOOxu5MKXdatBj42>wUugg;z+(t_%BdC+xxIKFb}<i-5Q;W)XZ>g zv^vH!xbqxTr=%8NJRVNBQEO6yJpzd^*qG0?Fxe~E8K4=vvvvg)859rZb8-;uj7j@t z@w2<&SK&QE@!J*q;0?tJDMPc-5j}CqCEOzm5>4;FV8q@oZlG=Gz-ej3$mEN@_bW_` zy3}YjX+qiTb;6U^`{my_T^=7DeboS*1B#7fHCY>8b~eYvvX9m>g&=NWo7r~#-=(!? zGJcUCZf?6_GAD~v40)G^FD|7C;YQ`aj2&Fm{os8qBA5Q26wW_gLbG3G*qSW7G@U11 z>YrX9q(0N_c()Zac?pQ-4=@&RSDu@DVEEg?3x82N@OD;lbB?}apySQ&3Vsp_Jhqo= zz(eLpVLv*6ika<?T8!N*-_iX`>(9`isvd$yU^V2V70kLK&sL7M!Pe*AB<(L*<oD@Q z{v<8v;^~$Cj1Fcyy;yTJtEHXAjSe(5HQkdu!V2#3MnTm+8<A?0AE&hxsz*~m)nxr_ z;v=`LK@ZvHg-6{+_cMinUh?NTWPs_YKm;yWq;PCUFE;4lzQ)Yo(=ArlANnZ6NnOnO zijtbHP&e4(^o1clO!47SfE%DuN;|u&z@z%LI|{7Bf-MUPaYt-ooN$4zAa3sT+3L{? z@#<}CFH>X0@=ou;y0s`o)AN8dsp~@guLu(`ot@ht?(>6QCIY^i11JBjU!pLi7Ocsq zzNZq4cxU!WgrHD4+Q%Ep<HDHB$y8<dErkc8sW{TCQlcsr8<sbeEwH6=MH8$pym6Yw znd?Ht6`Puy2U0XdR6LkfbE!!yMc6TanLDz7PuExpe<Poc5ekoj49&%WxwIz9e)ByG ze2#eY#wO<Vvx75Bk-8hEBm49RpmLr2U@`a@1E!ec?_o^zN%hFzU4%=F-vt9sKs8v@ z8E+^%bg3xXRS2^?WGq&!$q^=a4;r(%tE2n!gMB|cYxuzVw;zXzy*-j~HY$h*Os|ke zfG!k?3BWKhr$`>!lS)q{V2Uu|g>{-!ej=JoxjpF0)spT$j8Nt1$1`!OdHJ38XEuV6 zpYhEXnxN0NBfx4y?CzPx$Qv*z{D3oK#WuKKAN#&D?S`)qED3izzAjdsbkH3DtJVxv zl2a=OQOG=>$7{htRhE#QFkNfcNAB4i5$Pwb3zkM+{>t7rCILu%IL|CZ%ROQK4be?r z?5Grmf|-o%KXa>*raXXZd4*7n&u*lANv>j4XTUvT+&n>^QbkFS&>aK&R&;riEpy+8 z+m!{|=tcWf<);~OD(|cWj?1!eVogzqS+!#cLaa5_keP1kHCtPXqD{pU;&`dTv|2;z z$$_kH@Umpzi3dRpgHwC;C_WDwk?mLJQxvdjK>AL2fRXu$)J6uKqOx?bM-7BgG?5v3 zJ3_1w2GTJ`jR!5P5uZqQH?=}8{zy|mw+M$6Q=)tInRg&HvPC58TJWG+8c-@m7Pg2J z&wIr}DC3Jei3$Jpnx6Fc_X}aF?+iX>>*a7hKD)j@ywHZ{4bhOs8pKi7j2MZ0H*-oo zKa>Y8pavV&Sd9!kEbO{s2&^Q(SkL+ktk-4=KJNd1j0OwS(g$NLpD^To(bg`&^Wy6C zBXCzLuZ2137J*O^1|E%mH3R|?D1H_w76oLn@iL3?Gwdybk(0K|+4h_*ybSS|hw|HW zcg{|o>A-4L#$CUs$0a)>x`NkKoomNlN7)B{x}8&i&@gDcHSFOWmP_?~E~dw1br#FR zEEl?~I?HJ7$s|nJ;tl2XX2jcrT31?{QkaHHyZ4ij1c1`q)bu}V`#=zP_gD}Eu=FrW zn;R}!BCfe3r6)$L0&}|XZ{(bGITBOW|DGpit#jYQp4USKJmcjp10jq|A(ZS(^e<U0 zen#pZ<+4yPOXP<*OX(2fUvZ_r?5t)Ez9hglwx(G&n=`-O;(_fgBQe%ITQ2<^9_skE zGyh*r)(49FL(S7qw7&IgQ>krCNtwsT@0?)eoNUKip83$bFwvdY?TGSq$K3H^(^?Qh z#NCNi?S?n!v^(%zFP3+B2PFr1hRy)#YUFLf%A#{%C%_WM(7~P<epI(qwT#Uw?;Ise ztCgbaX@Aaz=05CXC&b1t+Zcu{q^qYm%`uXvViEUvfg7akOFFSxnVmVNK=*YKrw!s? zyWFnw+WXYKTgyQa>^t@9DV9a!`*svbka6wM^z>!^mu2mSI&;TKMOSC`$;DtFkb$@_ zvaZgL9cQL|3sGYV`Ykn8@&=jPRj5Gt*ZIO_T;KF#WA`b98PmBv8K78N)1`NB*nn-J zD6sct_1_1^t$!V@>%@X~Lx(vb%Aq^q^@(wRCTdWqV|wR`1+_yeHfZX~1Gz^}$U5L9 ze$-@2<tst^@r>GNhghtM2|HI-0jud`C3khBkoSfXB|#2XLcD9#s~y3*@=<@t>{WAf zIK8}HY}U(8NmGK|cX7{prX8-DtGU*%YWY<kpNlqg$0c7T?sMwISmjz*nLhv5GIC$% z?<mq#YFzU?(A(_!`-;)N!ke&<5tt9vfn{O7w$S9@1Uv5yVjE}3m0auMS{BvH7hBw{ zfV<x#MwR1OIXW@f=X(LIR-TwpR>&QtcU+EFnsG6|P*_@2uC|u#JAkrHl>4es1f+fk zgJcsv`|JQWs8iv6N*Fk)mxZFRQ>2ZN+3xvk(b4Mx^VWC~PBCWsb7tV|a%3e8bPNR$ z*b@NXQBvpYHDQu5ra2Q`sZLcUsXYK1D11*EAkk>jE|s|702$~B`25dq&R8`9GrP|; zteH8uD-XsV{kC#+9sMEPL4p8LXUx7D_^qv-j0)w2idr-0<NjH}l}gNe^0eR$&R<Mk zc<0<~VrXBJl`xCb>FLRN36d3gO5GXB)7RCBmU~~{LwjP+Li^;UuZ{c%#aoG$uGo5l z9`j6$W>G!V`Sc(pVl2F^xo5AOsZq83@2GTkN)B<KgY=fLD*gR>80K+KfC!<&U<~Z( z|F|t<V!~W{4J9*s_itdw)3rJ)y7v&(p76FtgS{owj&b+DxLv9Wmu!rj&l7xc9?g9O zoaItbQL(H|QYG<I4n>j3JEkbb+0oJ!;(oPm-o1LOPod{nA>$VNiSl}$TNkP6al!8A zpOf)0V;zb7bCiI0RytrCusioZXuJ<Xqh!qE>;M6XdBq{E!hPN@FYtL<=c5Il71{)O z#apdH@;bICCt#dMRCI*d(-MdRB97af2D^kO*e!nI*XqkB_QSvsd10^stUwuXp%Z;v zno_C1niLE}YVaHf&>dxJ0OR}j#uE9`*ib@!T#50o+GHq|(*M4#==1d{Ls6*s)Cmy3 z8JEuhe>p(dsr;7<-o6$btU`dq<-LncrJi3-WN+=caB`|AJc|~EOkS?30!vWq3(*@U zU9u$Q4!O%MV3!;$mSac2mTEwMNxc;-uvHKaES>V09B0-H*oVsty<~`B+);VdGN$Y{ zGq=LXe^q#xx1&$piAj0Enj_KUfICD^KGrXECq;MJ=EhELFm#q9T5e!@taM^OX&G!X z-qDP!0zO`Ib8~zDYnl@bGk}*og_>_M1@W2>ekW`E)Vvx}xv2fQrQmwrgxSzU9yQ=| zK;eg3Y7&@m8;4LRrw%$$W$MNbN744Eskr*P1{}>DXK8Jn&LP2o`Aw(woIM&K6&eqJ z#nZkrtMD4paTtZtO~TmMTslM+jS*yi(^*Pjy{+5pA?=Ww;l()0;~DNq*6W4I)<wh} z5vmq1tJ4#)40o`g9fM#Dq2Qx?KHHrI)9RNjfP&INfhm{j2~{>ULnmaes7-V^TbSbn zeNs}#$JWiOc|U@I?=01?B74IBo~#iKNuxTf^Tp+@xWDyb!?KvxsK@(R?}ACganqSP zUF#fO_BZVmsPZMUvd&@63+luVGRBQ1eUsMB`4xsIhEq})r6G6l!Tl?b)T=*-%Xq#5 z>wDth^D7U}4H;touRYW9zxU+K|M4R;|9kBJmj`p;7ykMJZClO-O~Nhd0QfS{F}hl% I?R@|L1B#tVe*gdg literal 0 HcmV?d00001 diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl index 4e836b228d..c2cafe0c9d 100644 --- a/templates/status/404.tmpl +++ b/templates/status/404.tmpl @@ -1,7 +1,8 @@ {{template "base/head" .}} {{template "base/navbar" .}} -<div id="gogs-body" class="container"> - <h4>This page is not found !</h4> +<div id="gogs-body" class="container text-center"> + <p style="margin-top: 80px"><img src="/img/404.png" alt="404"/></p> + <hr/> <p>Application Version: {{AppVer}}</p> </div> {{template "base/footer" .}} \ No newline at end of file diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl index 9a00eb1ff7..f3cd24d696 100644 --- a/templates/status/500.tmpl +++ b/templates/status/500.tmpl @@ -1,7 +1,10 @@ {{template "base/head" .}} {{template "base/navbar" .}} -<div id="gogs-body" class="container"> +<div id="gogs-body" class="container text-center"> + <p style="margin-top: 80px"><img src="/img/500.png" alt="404"/></p> + <hr/> <p>An error is occurred : {{.ErrorMsg}}</p> + <hr/> <p>Application Version: {{AppVer}}</p> </div> {{template "base/footer" .}} \ No newline at end of file From 1201c6a9b4897736650a4b416451aa521024b899 Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 16:31:13 +0800 Subject: [PATCH 21/38] bug fixed & more commits for push --- models/action.go | 2 +- models/models.go | 2 +- serve.go | 157 +++++++++++++++++++++++++++++++++++++++-------- update.go | 22 ++++--- 4 files changed, 145 insertions(+), 38 deletions(-) diff --git a/models/action.go b/models/action.go index 107d4b1057..d338da24ba 100644 --- a/models/action.go +++ b/models/action.go @@ -28,7 +28,7 @@ type Action struct { ActUserName string // Action user name. RepoId int64 RepoName string - Content string + Content string `xorm:"varchar(1000)"` Created time.Time `xorm:"created"` } diff --git a/models/models.go b/models/models.go index 8713ff2896..7f5f626f5e 100644 --- a/models/models.go +++ b/models/models.go @@ -91,5 +91,5 @@ func GetStatistic() (stats Statistic) { stats.Counter.Watch, _ = orm.Count(new(Watch)) stats.Counter.Action, _ = orm.Count(new(Action)) stats.Counter.Access, _ = orm.Count(new(Access)) - return stats + return } diff --git a/serve.go b/serve.go index 3ce8f9046c..b786c54356 100644 --- a/serve.go +++ b/serve.go @@ -5,14 +5,19 @@ package main import ( + "bytes" + "container/list" "fmt" + "io" "os" "os/exec" "strconv" "strings" "github.com/codegangsta/cli" + "github.com/qiniu/log" + "github.com/gogits/git" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/base" ) @@ -39,12 +44,27 @@ gogs serv provide access auth for repositories`, Flags: []cli.Flag{}, } +func parseCmd(cmd string) (string, string) { + ss := strings.SplitN(cmd, " ", 2) + if len(ss) != 2 { + return "", "" + } + + verb, args := ss[0], ss[1] + if verb == "git" { + ss = strings.SplitN(args, " ", 2) + args = ss[1] + verb = fmt.Sprintf("%s %s", verb, ss[0]) + } + return verb, args +} + func In(b string, sl map[string]int) bool { _, e := sl[b] return e } -func runServ(*cli.Context) { +func runServ(k *cli.Context) { base.NewConfigContext() models.LoadModelsConfig() models.NewEngine() @@ -84,15 +104,20 @@ func runServ(*cli.Context) { repoName = repoName[:len(repoName)-4] } - os.Setenv("userName", user.Name) - os.Setenv("userId", strconv.Itoa(int(user.Id))) + //os.Setenv("userName", user.Name) + //os.Setenv("userId", strconv.Itoa(int(user.Id))) repo, err := models.GetRepositoryByName(user, repoName) + var isExist bool = true if err != nil { - println("Unavilable repository", err) - return + if err == models.ErrRepoNotExist { + isExist = false + } else { + println("Unavilable repository", err) + return + } } - os.Setenv("repoId", strconv.Itoa(int(repo.Id))) - os.Setenv("repoName", repoName) + //os.Setenv("repoId", strconv.Itoa(int(repo.Id))) + //os.Setenv("repoName", repoName) isWrite := In(verb, COMMANDS_WRITE) isRead := In(verb, COMMANDS_READONLY) @@ -130,12 +155,6 @@ func runServ(*cli.Context) { return } - isExist, err := models.IsRepositoryExist(user, repoName) - if err != nil { - println("Inernel error:", err.Error()) - return - } - if !isExist { if isRead { println("Repository", user.Name+"/"+repoName, "is not exist") @@ -149,28 +168,114 @@ func runServ(*cli.Context) { } } + rep, err := git.OpenRepository(models.RepoPath(user.Name, repoName)) + if err != nil { + println(err.Error()) + return + } + + refs, err := rep.AllReferencesMap() + if err != nil { + println(err.Error()) + return + } + gitcmd := exec.Command(verb, rRepo) gitcmd.Dir = base.RepoRootPath - gitcmd.Stdout = os.Stdout - gitcmd.Stdin = os.Stdin + + var s string + b := bytes.NewBufferString(s) + + gitcmd.Stdout = io.MultiWriter(os.Stdout, b) + gitcmd.Stdin = io.MultiReader(os.Stdin, b) gitcmd.Stderr = os.Stderr if err = gitcmd.Run(); err != nil { println("execute command error:", err.Error()) } -} -func parseCmd(cmd string) (string, string) { - ss := strings.SplitN(cmd, " ", 2) - if len(ss) != 2 { - return "", "" + // update + w, _ := os.Create("serve.log") + defer w.Close() + log.SetOutput(w) + + var t = "ok refs/heads/" + var i int + var refname string + for { + l, err := b.ReadString('\n') + if err != nil { + break + } + i = i + 1 + l = l[:len(l)-1] + idx := strings.Index(l, t) + if idx > 0 { + refname = l[idx+len(t):] + } + } + var ref *git.Reference + var ok bool + + var l *list.List + //log.Info("----", refname, "-----") + if ref, ok = refs[refname]; !ok { + refs, err = rep.AllReferencesMap() + if err != nil { + println(err.Error()) + return + } + if ref, ok = refs[refname]; !ok { + println("unknow reference name", refname) + return + } + l, err = ref.AllCommits() + if err != nil { + println(err.Error()) + return + } + } else { + //log.Info("----", ref, "-----") + var last *git.Commit + //log.Info("00000", ref.Oid.String()) + last, err = ref.LastCommit() + if err != nil { + println(err.Error()) + return + } + + ref2, err := rep.LookupReference(ref.Name) + if err != nil { + println(err.Error()) + return + } + + //log.Info("11111", ref2.Oid.String()) + before, err := ref2.LastCommit() + if err != nil { + println(err.Error()) + return + } + //log.Info("----", before.Id(), "-----", last.Id()) + l = ref.CommitsBetween(before, last) } - verb, args := ss[0], ss[1] - if verb == "git" { - ss = strings.SplitN(args, " ", 2) - args = ss[1] - verb = fmt.Sprintf("%s %s", verb, ss[0]) + commits := make([][]string, 0) + for e := l.Back(); e != nil; e = e.Prev() { + commit := e.Value.(*git.Commit) + commits = append(commits, []string{commit.Id().String(), commit.Message()}) + } + + if err = models.CommitRepoAction(user.Id, user.Name, + repo.Id, ref.BranchName(), commits); err != nil { + log.Error("runUpdate.models.CommitRepoAction: %v", err, commits) + } else { + //log.Info("refname", refname) + //log.Info("Listen: %v", cmd) + //fmt.Println("...", cmd) + + //runUpdate(k) + c := exec.Command("exec", "git", "update-server-info") + c.Run() } - return verb, args } diff --git a/update.go b/update.go index baa433d75c..915e046530 100644 --- a/update.go +++ b/update.go @@ -4,16 +4,9 @@ package main -import ( - "os" - "strconv" +import "github.com/codegangsta/cli" - "github.com/codegangsta/cli" - - "github.com/gogits/git" - "github.com/gogits/gogs/models" - "github.com/gogits/gogs/modules/log" -) +//"github.com/gogits/gogs/modules/log" var CmdUpdate = cli.Command{ Name: "update", @@ -26,6 +19,9 @@ gogs serv provide access auth for repositories`, // for command: ./gogs update func runUpdate(*cli.Context) { + /*w, _ := os.Create("update.log") + log.SetOutput(w) + userName := os.Getenv("userName") userId := os.Getenv("userId") repoId := os.Getenv("repoId") @@ -35,16 +31,19 @@ func runUpdate(*cli.Context) { repo, err := git.OpenRepository(f) if err != nil { + log.Error("runUpdate.Open repoId: %v", err) return } ref, err := repo.LookupReference("HEAD") if err != nil { + log.Error("runUpdate.Ref repoId: %v", err) return } lastCommit, err := repo.LookupCommit(ref.Oid) if err != nil { + log.Error("runUpdate.Commit repoId: %v", err) return } @@ -63,5 +62,8 @@ func runUpdate(*cli.Context) { if err = models.CommitRepoAction(int64(sUserId), userName, int64(sRepoId), repoName, commits); err != nil { log.Error("runUpdate.models.CommitRepoAction: %v", err) - } + } else { + l := exec.Command("exec", "git", "update-server-info") + l.Run() + }*/ } From 4adb024715bdb576d7acb2900d933e7196abee4c Mon Sep 17 00:00:00 2001 From: slene <vslene@gmail.com> Date: Sun, 23 Mar 2014 16:40:51 +0800 Subject: [PATCH 22/38] minor fix markdown setting --- bee.json | 3 ++- modules/base/markdown.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bee.json b/bee.json index 19efcbc205..4f7f7a771c 100644 --- a/bee.json +++ b/bee.json @@ -13,7 +13,8 @@ "others": [ "modules", "$GOPATH/src/github.com/gogits/binding", - "$GOPATH/src/github.com/gogits/git" + "$GOPATH/src/github.com/gogits/git", + "$GOPATH/src/github.com/gogits/gfm" ] }, "cmd_args": [ diff --git a/modules/base/markdown.go b/modules/base/markdown.go index 2273cd772f..05ce0c833c 100644 --- a/modules/base/markdown.go +++ b/modules/base/markdown.go @@ -72,7 +72,7 @@ func (options *CustomRender) Link(out *bytes.Buffer, link []byte, title []byte, func RenderMarkdown(rawBytes []byte, urlPrefix string) []byte { htmlFlags := 0 - htmlFlags |= gfm.HTML_USE_XHTML + // htmlFlags |= gfm.HTML_USE_XHTML // htmlFlags |= gfm.HTML_USE_SMARTYPANTS // htmlFlags |= gfm.HTML_SMARTYPANTS_FRACTIONS // htmlFlags |= gfm.HTML_SMARTYPANTS_LATEX_DASHES @@ -81,7 +81,7 @@ func RenderMarkdown(rawBytes []byte, urlPrefix string) []byte { htmlFlags |= gfm.HTML_SKIP_SCRIPT htmlFlags |= gfm.HTML_GITHUB_BLOCKCODE htmlFlags |= gfm.HTML_OMIT_CONTENTS - htmlFlags |= gfm.HTML_COMPLETE_PAGE + // htmlFlags |= gfm.HTML_COMPLETE_PAGE renderer := &CustomRender{ Renderer: gfm.HtmlRenderer(htmlFlags, "", ""), urlPrefix: urlPrefix, From d594bb386f92455079e56b48f5add16b05609dea Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 16:51:43 +0800 Subject: [PATCH 23/38] log import fixed --- serve.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/serve.go b/serve.go index 54296049d5..d4ce138c1c 100644 --- a/serve.go +++ b/serve.go @@ -15,7 +15,7 @@ import ( "strings" "github.com/codegangsta/cli" - "github.com/qiniu/log" + "github.com/gogits/gogs/modules/log" "github.com/gogits/git" "github.com/gogits/gogs/models" @@ -195,9 +195,9 @@ func runServ(k *cli.Context) { } // update - w, _ := os.Create("serve.log") - defer w.Close() - log.SetOutput(w) + //w, _ := os.Create("serve.log") + //defer w.Close() + //log.SetOutput(w) var t = "ok refs/heads/" var i int From 21a138a75a379c1c8d1142b6bdf3f855c0361634 Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 17:10:09 +0800 Subject: [PATCH 24/38] bug fxied --- serve.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/serve.go b/serve.go index d4ce138c1c..16df38f420 100644 --- a/serve.go +++ b/serve.go @@ -104,8 +104,6 @@ func runServ(k *cli.Context) { repoName = repoName[:len(repoName)-4] } - //os.Setenv("userName", user.Name) - //os.Setenv("userId", strconv.Itoa(int(user.Id))) repo, err := models.GetRepositoryByName(user.Id, repoName) var isExist bool = true if err != nil { @@ -116,8 +114,6 @@ func runServ(k *cli.Context) { return } } - //os.Setenv("repoId", strconv.Itoa(int(repo.Id))) - //os.Setenv("repoName", repoName) isWrite := In(verb, COMMANDS_WRITE) isRead := In(verb, COMMANDS_READONLY) @@ -187,13 +183,18 @@ func runServ(k *cli.Context) { b := bytes.NewBufferString(s) gitcmd.Stdout = io.MultiWriter(os.Stdout, b) - gitcmd.Stdin = io.MultiReader(os.Stdin, b) + //gitcmd.Stdin = io.MultiReader(os.Stdin, b) + gitcmd.Stdin = os.Stdin gitcmd.Stderr = os.Stderr if err = gitcmd.Run(); err != nil { println("execute command error:", err.Error()) } + if !strings.HasPrefix(cmd, "git-receive-pack") { + return + } + // update //w, _ := os.Create("serve.log") //defer w.Close() From c4287bafabb96f3970bf629ff225c48112d008d9 Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 17:13:22 +0800 Subject: [PATCH 25/38] bug fixed --- serve.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serve.go b/serve.go index 16df38f420..2f7a9d6052 100644 --- a/serve.go +++ b/serve.go @@ -227,7 +227,7 @@ func runServ(k *cli.Context) { return } if ref, ok = refs[refname]; !ok { - println("unknow reference name", refname) + println("unknow reference name -", refname, "-") return } l, err = ref.AllCommits() From 406afa3c32eab4782291ca01eea273f2df25748d Mon Sep 17 00:00:00 2001 From: slene <vslene@gmail.com> Date: Sun, 23 Mar 2014 17:22:14 +0800 Subject: [PATCH 26/38] markdown img max-width --- public/css/markdown.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/css/markdown.css b/public/css/markdown.css index a810fa3ce7..9f4024ba89 100644 --- a/public/css/markdown.css +++ b/public/css/markdown.css @@ -175,6 +175,10 @@ margin-bottom: 0; } +.markdown img { + max-width: 100%; +} + .markdown .btn { color: #fff; } From c9e830cefb3b19593e7de05c785efdd93a0982d6 Mon Sep 17 00:00:00 2001 From: FuXiaoHei <fuxiaohei@hexiaz.com> Date: Sun, 23 Mar 2014 17:36:12 +0800 Subject: [PATCH 27/38] fix dropdown close bug --- public/js/app.js | 2 +- templates/repo/nav.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index df755727b5..e0f92854f4 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -98,7 +98,7 @@ var Gogits = { }; // fix dropdown inside click Gogits.initDropDown = function(){ - $('.dropdown-menu').on('click','a,button,input,select',function(e){ + $('.dropdown-menu.no-propagation').on('click',function(e){ e.stopPropagation(); }); }; diff --git a/templates/repo/nav.tmpl b/templates/repo/nav.tmpl index cf1b7d0389..4d939e2504 100644 --- a/templates/repo/nav.tmpl +++ b/templates/repo/nav.tmpl @@ -18,7 +18,7 @@ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"> <span class="caret"></span> </button> - <div class="dropdown-menu clone-group-btn dropdown-menu-right"> + <div class="dropdown-menu clone-group-btn dropdown-menu-right no-propagation"> <div class="input-group"> <span class="input-group-btn"> <button class="btn btn-default" data-link="{{.CloneLink.SSH}}" type="button">SSH</button> From 0119675480505a6534ee0fc98c91e3bd2b8cdaaf Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 17:40:50 +0800 Subject: [PATCH 28/38] bug fixed --- models/action.go | 4 +++- serve.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/models/action.go b/models/action.go index cfb124363c..4f78deb30e 100644 --- a/models/action.go +++ b/models/action.go @@ -30,6 +30,7 @@ type Action struct { ActUserName string // Action user name. RepoId int64 RepoName string + RefName string Content string `xorm:"TEXT"` Created time.Time `xorm:"created"` } @@ -52,7 +53,7 @@ func (a Action) GetContent() string { // CommitRepoAction records action for commit repository. func CommitRepoAction(userId int64, userName string, - repoId int64, repoName string, commits [][]string) error { + repoId int64, repoName string, refName string, commits [][]string) error { bs, err := json.Marshal(commits) if err != nil { return err @@ -78,6 +79,7 @@ func CommitRepoAction(userId int64, userName string, Content: string(bs), RepoId: repoId, RepoName: repoName, + RefName: refName, }) return err } diff --git a/serve.go b/serve.go index 2f7a9d6052..bb9165dc06 100644 --- a/serve.go +++ b/serve.go @@ -268,7 +268,7 @@ func runServ(k *cli.Context) { } if err = models.CommitRepoAction(user.Id, user.Name, - repo.Id, ref.BranchName(), commits); err != nil { + repo.Id, repoName, refname, commits); err != nil { log.Error("runUpdate.models.CommitRepoAction: %v", err, commits) } else { //log.Info("refname", refname) From d6619cfe15885b7004585bf2b96933a7a3e51f39 Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 17:51:12 +0800 Subject: [PATCH 29/38] limit max commits view number on activity --- serve.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/serve.go b/serve.go index bb9165dc06..812c37bb58 100644 --- a/serve.go +++ b/serve.go @@ -262,9 +262,13 @@ func runServ(k *cli.Context) { } commits := make([][]string, 0) + var maxCommits = 5 for e := l.Back(); e != nil; e = e.Prev() { commit := e.Value.(*git.Commit) commits = append(commits, []string{commit.Id().String(), commit.Message()}) + if len(commits) >= maxCommits { + break + } } if err = models.CommitRepoAction(user.Id, user.Name, From 24630e0c9b92bcd9fdeb07ce15c3dd2cfc459a52 Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 18:00:09 +0800 Subject: [PATCH 30/38] improved activity --- models/action.go | 7 ++++++- serve.go | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/models/action.go b/models/action.go index 4f78deb30e..ca2ff3cbf1 100644 --- a/models/action.go +++ b/models/action.go @@ -51,9 +51,14 @@ func (a Action) GetContent() string { return a.Content } +type PushCommits struct { + Len int + Commits [][]string +} + // CommitRepoAction records action for commit repository. func CommitRepoAction(userId int64, userName string, - repoId int64, repoName string, refName string, commits [][]string) error { + repoId int64, repoName string, refName string, commits *PushCommits) error { bs, err := json.Marshal(commits) if err != nil { return err diff --git a/serve.go b/serve.go index 812c37bb58..71abf5fdd6 100644 --- a/serve.go +++ b/serve.go @@ -262,7 +262,7 @@ func runServ(k *cli.Context) { } commits := make([][]string, 0) - var maxCommits = 5 + var maxCommits = 3 for e := l.Back(); e != nil; e = e.Prev() { commit := e.Value.(*git.Commit) commits = append(commits, []string{commit.Id().String(), commit.Message()}) @@ -272,7 +272,7 @@ func runServ(k *cli.Context) { } if err = models.CommitRepoAction(user.Id, user.Name, - repo.Id, repoName, refname, commits); err != nil { + repo.Id, repoName, refname, &models.PushCommits{l.Len(), commits}); err != nil { log.Error("runUpdate.models.CommitRepoAction: %v", err, commits) } else { //log.Info("refname", refname) From 1eb078d0a8c5424de9512d810ab2fbf21f59ff78 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sun, 23 Mar 2014 06:27:01 -0400 Subject: [PATCH 31/38] Fix action --- README.md | 6 ++-- README_ZH.md | 2 +- models/action.go | 12 ++++---- models/issue.go | 26 ++++++++-------- modules/base/tool.go | 23 ++++++++++++--- routers/repo/repo.go | 59 +++++++++++++++++++++---------------- serve.go | 2 +- templates/repo/nav.tmpl | 2 +- templates/repo/setting.tmpl | 16 ++++------ web.go | 2 +- 10 files changed, 85 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 504c21975b..e947d7739a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Gogs - Go Git Service [](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [](https://drone.io/github.com/gogits/gogs/latest) +Gogs - Go Git Service [](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [](https://drone.io/github.com/gogits/gogs/latest) [](https://bitdeli.com/free "Bitdeli Badge") ===================== Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language. @@ -7,7 +7,9 @@ Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language ##### Current version: 0.1.6 Alpha -[简体中文](README_ZH.md) +#### Other language version + +- [简体中文](README_ZH.md) ## Purpose diff --git a/README_ZH.md b/README_ZH.md index 0ab8dfdd07..78e26fada4 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -1,4 +1,4 @@ -Gogs - Go Git Service [](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [](https://drone.io/github.com/gogits/gogs/latest) +Gogs - Go Git Service [](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [](https://drone.io/github.com/gogits/gogs/latest) [](https://bitdeli.com/free "Bitdeli Badge") ===================== Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。 diff --git a/models/action.go b/models/action.go index ca2ff3cbf1..1174929354 100644 --- a/models/action.go +++ b/models/action.go @@ -8,6 +8,7 @@ import ( "encoding/json" "time" + "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" ) @@ -47,18 +48,17 @@ func (a Action) GetRepoName() string { return a.RepoName } +func (a Action) GetBranch() string { + return a.RefName +} + func (a Action) GetContent() string { return a.Content } -type PushCommits struct { - Len int - Commits [][]string -} - // CommitRepoAction records action for commit repository. func CommitRepoAction(userId int64, userName string, - repoId int64, repoName string, refName string, commits *PushCommits) error { + repoId int64, repoName string, refName string, commits *base.PushCommits) error { bs, err := json.Marshal(commits) if err != nil { return err diff --git a/models/issue.go b/models/issue.go index f78c240cbc..929567b1b7 100644 --- a/models/issue.go +++ b/models/issue.go @@ -83,42 +83,42 @@ func GetIssues(userId, repoId, posterId, milestoneId int64, page int, isClosed, sess := orm.Limit(20, (page-1)*20) if repoId > 0 { - sess = sess.Where("repo_id=?", repoId).And("is_closed=?", isClosed) + sess.Where("repo_id=?", repoId).And("is_closed=?", isClosed) } else { - sess = sess.Where("is_closed=?", isClosed) + sess.Where("is_closed=?", isClosed) } if userId > 0 { - sess = sess.And("assignee_id=?", userId) + sess.And("assignee_id=?", userId) } else if posterId > 0 { - sess = sess.And("poster_id=?", posterId) + sess.And("poster_id=?", posterId) } else if isMention { - sess = sess.And("mentions like '%$" + base.ToStr(userId) + "|%'") + sess.And("mentions like '%$" + base.ToStr(userId) + "|%'") } if milestoneId > 0 { - sess = sess.And("milestone_id=?", milestoneId) + sess.And("milestone_id=?", milestoneId) } if len(labels) > 0 { for _, label := range strings.Split(labels, ",") { - sess = sess.And("mentions like '%$" + label + "|%'") + sess.And("mentions like '%$" + label + "|%'") } } switch sortType { case "oldest": - sess = sess.Asc("created") + sess.Asc("created") case "recentupdate": - sess = sess.Desc("updated") + sess.Desc("updated") case "leastupdate": - sess = sess.Asc("updated") + sess.Asc("updated") case "mostcomment": - sess = sess.Desc("num_comments") + sess.Desc("num_comments") case "leastcomment": - sess = sess.Asc("num_comments") + sess.Asc("num_comments") default: - sess = sess.Desc("created") + sess.Desc("created") } var issues []Issue diff --git a/modules/base/tool.go b/modules/base/tool.go index c7ee2ee857..edf7a953c9 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -471,6 +471,7 @@ type Actioner interface { GetOpType() int GetActUserName() string GetRepoName() string + GetBranch() string GetContent() string } @@ -493,25 +494,39 @@ const ( TPL_COMMIT_REPO_LI = `<div><img id="gogs-user-avatar-commit" src="%s?s=16" alt="user-avatar" title="username"/> <a href="/%s/%s/commit/%s">%s</a> %s</div>` ) +type PushCommits struct { + Len int + Commits [][]string +} + // ActionDesc accepts int that represents action operation type // and returns the description. func ActionDesc(act Actioner, avatarLink string) string { actUserName := act.GetActUserName() repoName := act.GetRepoName() + branch := act.GetBranch() content := act.GetContent() switch act.GetOpType() { case 1: // Create repository. return fmt.Sprintf(TPL_CREATE_REPO, actUserName, actUserName, actUserName, repoName, repoName) case 5: // Commit repository. - var commits [][]string - if err := json.Unmarshal([]byte(content), &commits); err != nil { + var push *PushCommits + if err := json.Unmarshal([]byte(content), &push); err != nil { return err.Error() } buf := bytes.NewBuffer([]byte("\n")) - for _, commit := range commits { + max := 3 + count := len(push.Commits) + if count < max { + max = count + } + for _, commit := range push.Commits[:max] { buf.WriteString(fmt.Sprintf(TPL_COMMIT_REPO_LI, avatarLink, actUserName, repoName, commit[0], commit[0][:7], commit[1]) + "\n") } - return fmt.Sprintf(TPL_COMMIT_REPO, actUserName, actUserName, actUserName, repoName, "master", "master", actUserName, repoName, actUserName, repoName, + if count > max { + buf.WriteString(fmt.Sprintf(`<div><a href="/%s/%s/commits">%d other commits >></a></div>`, actUserName, repoName, count-max)) + } + return fmt.Sprintf(TPL_COMMIT_REPO, actUserName, actUserName, actUserName, repoName, branch, branch, actUserName, repoName, actUserName, repoName, buf.String()) default: return "invalid type" diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 0f1ea31235..82956098b7 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -52,30 +52,6 @@ func Create(ctx *middleware.Context, form auth.CreateRepoForm) { ctx.Handle(200, "repo.Create", err) } -func SettingPost(ctx *middleware.Context) { - if !ctx.Repo.IsOwner { - ctx.Error(404) - return - } - - switch ctx.Query("action") { - case "delete": - if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { - ctx.Data["ErrorMsg"] = "Please make sure you entered repository name is correct." - ctx.HTML(200, "repo/setting") - return - } - - if err := models.DeleteRepository(ctx.User.Id, ctx.Repo.Repository.Id, ctx.User.LowerName); err != nil { - ctx.Handle(200, "repo.Delete", err) - return - } - } - - log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) - ctx.Redirect("/") -} - func Branches(ctx *middleware.Context, params martini.Params) { if !ctx.Repo.IsValid { return @@ -203,7 +179,6 @@ func Single(ctx *middleware.Context, params martini.Params) { if readmeFile.Size > 1024*1024 || readmeFile.Filemode != git.FileModeBlob { ctx.Data["FileIsLarge"] = true } else if blob, err := readmeFile.LookupBlob(); err != nil { - //log.Error("repo.Single(readmeFile.LookupBlob): %v", err) ctx.Handle(404, "repo.Single(readmeFile.LookupBlob)", err) return } else { @@ -301,6 +276,40 @@ func Setting(ctx *middleware.Context, params martini.Params) { ctx.HTML(200, "repo/setting") } +func SettingPost(ctx *middleware.Context, params martini.Params) { + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + + switch ctx.Query("action") { + case "update": + ctx.Repo.Repository.Description = ctx.Query("desc") + ctx.Repo.Repository.Website = ctx.Query("site") + if err := models.UpdateRepository(ctx.Repo.Repository); err != nil { + ctx.Handle(404, "repo.SettingPost(update)", err) + return + } + ctx.Data["IsSuccess"] = true + ctx.HTML(200, "repo/setting") + log.Trace("%s Repository updated: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) + case "delete": + if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { + ctx.Data["ErrorMsg"] = "Please make sure you entered repository name is correct." + ctx.HTML(200, "repo/setting") + return + } + + if err := models.DeleteRepository(ctx.User.Id, ctx.Repo.Repository.Id, ctx.User.LowerName); err != nil { + ctx.Handle(200, "repo.Delete", err) + return + } + + log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) + ctx.Redirect("/") + } +} + func Commits(ctx *middleware.Context, params martini.Params) { brs, err := models.GetBranches(params["username"], params["reponame"]) if err != nil { diff --git a/serve.go b/serve.go index 71abf5fdd6..f9291366ab 100644 --- a/serve.go +++ b/serve.go @@ -272,7 +272,7 @@ func runServ(k *cli.Context) { } if err = models.CommitRepoAction(user.Id, user.Name, - repo.Id, repoName, refname, &models.PushCommits{l.Len(), commits}); err != nil { + repo.Id, repoName, refname, &base.PushCommits{l.Len(), commits}); err != nil { log.Error("runUpdate.models.CommitRepoAction: %v", err, commits) } else { //log.Info("refname", refname) diff --git a/templates/repo/nav.tmpl b/templates/repo/nav.tmpl index 4d939e2504..b2d3500f92 100644 --- a/templates/repo/nav.tmpl +++ b/templates/repo/nav.tmpl @@ -3,7 +3,7 @@ <div class="row"> <div class="col-md-7"> <h3 class="name"><i class="fa fa-book fa-lg"></i><a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / {{.Repository.Name}}</h3> - <p class="desc">{{.Repository.Description}}{{if .Repository.Website}}<a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}</p> + <p class="desc">{{.Repository.Description}}{{if .Repository.Website}} <a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}</p> </div> <div class="col-md-5 actions text-right clone-group-btn"> {{if not .IsBareRepo}} diff --git a/templates/repo/setting.tmpl b/templates/repo/setting.tmpl index 719547b1a9..c826e55a77 100644 --- a/templates/repo/setting.tmpl +++ b/templates/repo/setting.tmpl @@ -12,7 +12,7 @@ </div> <div id="gogs-repo-setting-container" class="col-md-9"> - {{if .ErrorMsg}}<p class="alert alert-danger">{{.ErrorMsg}}</p>{{end}} + {{if .IsSuccess}}<p class="alert alert-success">Repository option has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}} <div class="panel panel-default"> <div class="panel-heading"> Repository Options @@ -22,32 +22,26 @@ <form action="/{{.Owner.Name}}/{{.Repository.Name}}/settings" method="post" class="form-horizontal"> {{.CsrfTokenHtml}} <input type="hidden" name="action" value="update"> - <div class="form-group"> - <label class="col-md-3 text-right">Repository Name <strong class="text-danger">*</strong></label> - <div class="col-md-9"> - <input type="text" class="form-control" name="repo-name" required="required" value="{{.Repository.Name}}"/> - </div> - </div> <div class="form-group"> <label class="col-md-3 text-right">Description</label> <div class="col-md-9"> - <textarea class="form-control" name="desc" id="repo-desc" rows="6"></textarea> + <textarea class="form-control" name="desc" id="repo-desc" rows="3">{{.Repository.Description}}</textarea> </div> </div> <div class="form-group"> <label class="col-md-3 text-right">Official Site</label> <div class="col-md-9"> - <input type="url" class="form-control" name="repo-site"/> + <input type="url" class="form-control" name="site" value="{{.Repository.Website}}" /> </div> </div> - <div class="form-group"> + <!-- <div class="form-group"> <label class="col-md-3 text-right">Default Branch</label> <div class="col-md-9"> <select name="branch" id="repo-default-branch" class="form-control"> <option value="">Branch</option> </select> </div> - </div> + </div> --> <div class="form-group"> <div class="col-md-9 col-md-offset-3"> <button class="btn btn-primary" type="submit">Save Options</button> diff --git a/web.go b/web.go index 7b36ccd9db..9d90851c96 100644 --- a/web.go +++ b/web.go @@ -163,7 +163,7 @@ func runWeb(*cli.Context) { m.Get("/template/**", dev.TemplatePreview) } - // not found handler + // Not found handler. m.NotFound(routers.NotFound) listenAddr := fmt.Sprintf("%s:%s", From c386bb4bd314843a532012877e148ae70ee44672 Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sun, 23 Mar 2014 06:34:41 -0400 Subject: [PATCH 32/38] Bug fix --- modules/base/tool.go | 11 +++-------- templates/repo/nav.tmpl | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/modules/base/tool.go b/modules/base/tool.go index edf7a953c9..b48566f542 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -515,16 +515,11 @@ func ActionDesc(act Actioner, avatarLink string) string { return err.Error() } buf := bytes.NewBuffer([]byte("\n")) - max := 3 - count := len(push.Commits) - if count < max { - max = count - } - for _, commit := range push.Commits[:max] { + for _, commit := range push.Commits { buf.WriteString(fmt.Sprintf(TPL_COMMIT_REPO_LI, avatarLink, actUserName, repoName, commit[0], commit[0][:7], commit[1]) + "\n") } - if count > max { - buf.WriteString(fmt.Sprintf(`<div><a href="/%s/%s/commits">%d other commits >></a></div>`, actUserName, repoName, count-max)) + if push.Len > 3 { + buf.WriteString(fmt.Sprintf(`<div><a href="/%s/%s/commits">%d other commits >></a></div>`, actUserName, repoName, push.Len)) } return fmt.Sprintf(TPL_COMMIT_REPO, actUserName, actUserName, actUserName, repoName, branch, branch, actUserName, repoName, actUserName, repoName, buf.String()) diff --git a/templates/repo/nav.tmpl b/templates/repo/nav.tmpl index b2d3500f92..a3358fd890 100644 --- a/templates/repo/nav.tmpl +++ b/templates/repo/nav.tmpl @@ -32,7 +32,7 @@ <p class="help-block text-center">Need help cloning? Visit <a href="#">Help</a>!</p> </div> </div> - <div class="btn-group {{if .IsRepositoryWatching}}watching{{else}}no-watching{{end}}" id="gogs-repo-watching" data-watch="/{{.SignedUser.Name}}/{{.Repository.Name}}/action/watch" data-unwatch="/{{.SignedUser.Name}}/{{.Repository.Name}}/action/unwatch"> + <div class="btn-group {{if .IsRepositoryWatching}}watching{{else}}no-watching{{end}}" id="gogs-repo-watching" data-watch="/{{.Owner.Name}}/{{.Repository.Name}}/action/watch" data-unwatch="/{{.Owner.Name}}/{{.Repository.Name}}/action/unwatch"> {{if .IsRepositoryWatching}} <button type="button" class="btn btn-default"><i class="fa fa-eye fa-lg fa-m"></i></button> {{else}} From e9a3432d984d19b675ee4a1ff82b2a148f970645 Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 19:24:06 +0800 Subject: [PATCH 33/38] bug fixed --- serve.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serve.go b/serve.go index 71abf5fdd6..84fd8d7b82 100644 --- a/serve.go +++ b/serve.go @@ -263,7 +263,7 @@ func runServ(k *cli.Context) { commits := make([][]string, 0) var maxCommits = 3 - for e := l.Back(); e != nil; e = e.Prev() { + for e := l.Front(); e != nil; e = e.Next() { commit := e.Value.(*git.Commit) commits = append(commits, []string{commit.Id().String(), commit.Message()}) if len(commits) >= maxCommits { From 3d1a967477dd8245dabee6f9f8feb3c1c3924fc3 Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 19:53:50 +0800 Subject: [PATCH 34/38] bug fixed --- models/repo.go | 7 +++++++ serve.go | 7 +++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/models/repo.go b/models/repo.go index a37923c8b1..eafb12f471 100644 --- a/models/repo.go +++ b/models/repo.go @@ -10,6 +10,7 @@ import ( "fmt" "io/ioutil" "os" + "os/exec" "path" "path/filepath" "regexp" @@ -198,6 +199,12 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv return nil, err } + c := exec.Command("git", "update-server-info") + err = c.Run() + if err != nil { + log.Error("repo.CreateRepository(exec update-server-info): %v", err) + } + return repo, NewRepoAction(user, repo) } diff --git a/serve.go b/serve.go index a8dfe70b4c..ce1dc3184f 100644 --- a/serve.go +++ b/serve.go @@ -280,7 +280,10 @@ func runServ(k *cli.Context) { //fmt.Println("...", cmd) //runUpdate(k) - c := exec.Command("exec", "git", "update-server-info") - c.Run() + c := exec.Command("git", "update-server-info") + err := c.Run() + if err != nil { + log.Error("update-server-info: %v", err) + } } } From 97e82a0ff6286fd0a42281cd1e34dd734110e2f4 Mon Sep 17 00:00:00 2001 From: Lunny Xiao <xiaolunwen@gmail.com> Date: Sun, 23 Mar 2014 20:04:26 +0800 Subject: [PATCH 35/38] bug fixed --- models/repo.go | 1 + serve.go | 1 + 2 files changed, 2 insertions(+) diff --git a/models/repo.go b/models/repo.go index eafb12f471..e27e99b056 100644 --- a/models/repo.go +++ b/models/repo.go @@ -200,6 +200,7 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv } c := exec.Command("git", "update-server-info") + c.Dir = repoPath err = c.Run() if err != nil { log.Error("repo.CreateRepository(exec update-server-info): %v", err) diff --git a/serve.go b/serve.go index ce1dc3184f..b84fa2a4e5 100644 --- a/serve.go +++ b/serve.go @@ -281,6 +281,7 @@ func runServ(k *cli.Context) { //runUpdate(k) c := exec.Command("git", "update-server-info") + c.Dir = models.RepoPath(user.Name, repoName) err := c.Run() if err != nil { log.Error("update-server-info: %v", err) From 97debac18534e030924654befc6dc1eeb870a38b Mon Sep 17 00:00:00 2001 From: Unknown <joe2010xtmf@163.com> Date: Sun, 23 Mar 2014 08:40:40 -0400 Subject: [PATCH 36/38] SSL enable config option --- README.md | 4 ++-- README_ZH.md | 4 ++-- conf/app.ini | 2 ++ gogs.go | 2 +- models/user.go | 2 +- modules/base/conf.go | 4 ++++ modules/base/tool.go | 2 +- modules/middleware/repo.go | 6 +++++- routers/admin/admin.go | 1 + routers/dashboard.go | 2 +- templates/admin/config.tmpl | 1 + templates/status/404.tmpl | 1 + 12 files changed, 22 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e947d7739a..42eba6362d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language  -##### Current version: 0.1.6 Alpha +##### Current version: 0.1.7 Alpha #### Other language version @@ -27,7 +27,7 @@ More importantly, Gogs only needs one binary to setup your own project hosting o ## Features - Activity timeline -- SSH protocol support. +- SSH/HTTPS protocol support. - Register/delete account. - Create/delete/watch public repository. - User profile page. diff --git a/README_ZH.md b/README_ZH.md index 78e26fada4..b405e04198 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -5,7 +5,7 @@ Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。  -##### 当前版本:0.1.6 Alpha +##### 当前版本:0.1.7 Alpha ## 开发目的 @@ -23,7 +23,7 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依 ## 功能特性 - 活动时间线 -- SSH 协议支持 +- SSH/HTTPS 协议支持 - 注册/删除用户 - 创建/删除/关注公开仓库 - 用户个人信息页面 diff --git a/conf/app.ini b/conf/app.ini index b051557f41..ab9f6dc4bb 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -32,6 +32,8 @@ PATH = data/gogs.db [admin] [security] +; Use HTTPS to clone repository, otherwise use HTTP. +ENABLE_HTTPS_CLONE = false ; !!CHANGE THIS TO KEEP YOUR USER DATA SAFE!! SECRET_KEY = !#@FDEWREWR&*( ; Auto-login remember days diff --git a/gogs.go b/gogs.go index 0bdbbc0697..09b28f9b3f 100644 --- a/gogs.go +++ b/gogs.go @@ -20,7 +20,7 @@ import ( // Test that go1.2 tag above is included in builds. main.go refers to this definition. const go12tag = true -const APP_VER = "0.1.6.0323.1" +const APP_VER = "0.1.7.0323.1" func init() { base.AppVer = APP_VER diff --git a/models/user.go b/models/user.go index 9333d1ee67..c9d6e61303 100644 --- a/models/user.go +++ b/models/user.go @@ -208,7 +208,7 @@ func UpdateUser(user *User) (err error) { user.Website = user.Website[:255] } - _, err = orm.Id(user.Id).UseBool().Cols("website", "location").Update(user) + _, err = orm.Id(user.Id).UseBool().Cols("website", "location", "is_active", "is_admin").Update(user) return err } diff --git a/modules/base/conf.go b/modules/base/conf.go index 19f587077b..fba05e8800 100644 --- a/modules/base/conf.go +++ b/modules/base/conf.go @@ -38,6 +38,8 @@ var ( RunUser string RepoRootPath string + EnableHttpsClone bool + LogInRememberDays int CookieUserName string CookieRememberName string @@ -260,6 +262,8 @@ func NewConfigContext() { SecretKey = Cfg.MustValue("security", "SECRET_KEY") RunUser = Cfg.MustValue("", "RUN_USER") + EnableHttpsClone = Cfg.MustBool("security", "ENABLE_HTTPS_CLONE", false) + LogInRememberDays = Cfg.MustInt("security", "LOGIN_REMEMBER_DAYS") CookieUserName = Cfg.MustValue("security", "COOKIE_USERNAME") CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME") diff --git a/modules/base/tool.go b/modules/base/tool.go index b48566f542..6d31b05252 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -519,7 +519,7 @@ func ActionDesc(act Actioner, avatarLink string) string { buf.WriteString(fmt.Sprintf(TPL_COMMIT_REPO_LI, avatarLink, actUserName, repoName, commit[0], commit[0][:7], commit[1]) + "\n") } if push.Len > 3 { - buf.WriteString(fmt.Sprintf(`<div><a href="/%s/%s/commits">%d other commits >></a></div>`, actUserName, repoName, push.Len)) + buf.WriteString(fmt.Sprintf(`<div><a href="/%s/%s/commits/%s">%d other commits >></a></div>`, actUserName, repoName, branch, push.Len)) } return fmt.Sprintf(TPL_COMMIT_REPO, actUserName, actUserName, actUserName, repoName, branch, branch, actUserName, repoName, actUserName, repoName, buf.String()) diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index 3864caaf80..eea2570ca6 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -69,8 +69,12 @@ func RepoAssignment(redirect bool) martini.Handler { ctx.Repo.IsWatching = models.IsWatching(ctx.User.Id, repo.Id) } ctx.Repo.Repository = repo + scheme := "http" + if base.EnableHttpsClone { + scheme = "https" + } ctx.Repo.CloneLink.SSH = fmt.Sprintf("git@%s:%s/%s.git", base.Domain, user.LowerName, repo.LowerName) - ctx.Repo.CloneLink.HTTPS = fmt.Sprintf("https://%s/%s/%s.git", base.Domain, user.LowerName, repo.LowerName) + ctx.Repo.CloneLink.HTTPS = fmt.Sprintf("%s://%s/%s/%s.git", scheme, base.Domain, user.LowerName, repo.LowerName) ctx.Data["IsRepositoryValid"] = true ctx.Data["Repository"] = repo diff --git a/routers/admin/admin.go b/routers/admin/admin.go index c0f39f7159..f1f951ef25 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -141,6 +141,7 @@ func Config(ctx *middleware.Context) { ctx.Data["Domain"] = base.Domain ctx.Data["RunUser"] = base.RunUser ctx.Data["RunMode"] = strings.Title(martini.Env) + ctx.Data["EnableHttpsClone"] = base.EnableHttpsClone ctx.Data["RepoRootPath"] = base.RepoRootPath ctx.Data["Service"] = base.Service diff --git a/routers/dashboard.go b/routers/dashboard.go index dafe9f31ec..76ecc3f676 100644 --- a/routers/dashboard.go +++ b/routers/dashboard.go @@ -26,6 +26,6 @@ func Help(ctx *middleware.Context) { func NotFound(ctx *middleware.Context) { ctx.Data["PageIsNotFound"] = true - ctx.Data["Title"] = 404 + ctx.Data["Title"] = "Page Not Found" ctx.Handle(404, "home.NotFound", nil) } diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 048740e617..915c9dc088 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -17,6 +17,7 @@ <div><b>Run User:</b> {{.RunUser}}</div> <div><b>Run Mode:</b> {{.RunMode}}</div> <hr/> + <div><b>Enable HTTPS Clone</b> <i class="fa fa{{if .EnableHttpsClone}}-check{{end}}-square-o"></i></div> <div><b>Repository Root Path:</b> {{.RepoRootPath}}</div> </div> </div> diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl index c2cafe0c9d..b971f279a8 100644 --- a/templates/status/404.tmpl +++ b/templates/status/404.tmpl @@ -4,5 +4,6 @@ <p style="margin-top: 80px"><img src="/img/404.png" alt="404"/></p> <hr/> <p>Application Version: {{AppVer}}</p> + <p>If you think it is an error, please open an issue on <a href="https://github.com/gogits/gogs/issues/new">GitHub</a>.</p> </div> {{template "base/footer" .}} \ No newline at end of file From 559cd63fc5feace32bb44c695207579609516f15 Mon Sep 17 00:00:00 2001 From: slene <vslene@gmail.com> Date: Sun, 23 Mar 2014 20:58:12 +0800 Subject: [PATCH 37/38] fix code view indentation --- public/css/markdown.css | 5 ++-- public/js/app.js | 44 +++++++++++++++++++-------------- templates/repo/single_file.tmpl | 4 +-- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/public/css/markdown.css b/public/css/markdown.css index 9f4024ba89..d46fd94365 100644 --- a/public/css/markdown.css +++ b/public/css/markdown.css @@ -135,13 +135,12 @@ box-shadow: inset 40px 0 0 #f5f5f5, inset 41px 0 0 #ccc; } -.markdown > pre > code, -.markdown > pre > ol.linenums > li > code { +.markdown > pre > code { white-space: pre; word-wrap: normal; } -.markdown > pre > ol.linenums > li > code { +.markdown > pre > ol.linenums > li { padding: 0 10px; } diff --git a/public/js/app.js b/public/js/app.js index e0f92854f4..3e995d5b0e 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -110,25 +110,6 @@ var Gogits = { $pre.addClass('prettyprint linenums'); prettyPrint(); - var $lineNums = $pre.parent().siblings('.lines-num'); - if ($lineNums.length > 0) { - var nums = $pre.find('ol.linenums > li').length; - for (var i = 1; i <= nums; i++) { - $lineNums.append('<span id="L' + i + '" rel=".L' + i + '">' + i + '</span>'); - } - - var last; - $(document).on('click', '.lines-num span', function () { - var $e = $(this); - if (last) { - last.removeClass('active'); - } - last = $e.parent().siblings('.lines-code').find('ol.linenums > ' + $e.attr('rel')); - last.addClass('active'); - window.location.href = '#' + $e.attr('id'); - }); - } - // Set anchor. var headers = {}; $md.find('h1, h2, h3, h4, h5, h6').each(function () { @@ -148,6 +129,30 @@ var Gogits = { }); } + Gogits.renderCodeView = function () { + $('.code-view .lines-code > pre').each(function(){ + var $pre = $(this); + var $lineNums = $pre.parent().siblings('.lines-num'); + if ($lineNums.length > 0) { + var nums = $pre.find('ol.linenums > li').length; + for (var i = 1; i <= nums; i++) { + $lineNums.append('<span id="L' + i + '" rel=".L' + i + '">' + i + '</span>'); + } + + var last; + $(document).on('click', '.lines-num span', function () { + var $e = $(this); + if (last) { + last.removeClass('active'); + } + last = $e.parent().siblings('.lines-code').find('ol.linenums > ' + $e.attr('rel')); + last.addClass('active'); + window.location.href = '#' + $e.attr('id'); + }); + } + }); + }; + })(jQuery); // ajax utils @@ -177,6 +182,7 @@ function initCore() { Gogits.initModals(); Gogits.initDropDown(); Gogits.renderMarkdown(); + Gogits.renderCodeView(); } function initRegister() { diff --git a/templates/repo/single_file.tmpl b/templates/repo/single_file.tmpl index 7bca626aaa..40ef44eb1b 100644 --- a/templates/repo/single_file.tmpl +++ b/templates/repo/single_file.tmpl @@ -16,12 +16,12 @@ {{.FileContent|str2html}} </div> {{else}} - <div class="panel-body file-body file-code"> + <div class="panel-body file-body file-code code-view"> <table> <tbody> <tr> <td class="lines-num"></td> - <td class="lines-code markdown"><pre class="linenums lang-{{.FileExt}}"><code>{{.FileContent}}</code></pre></td> + <td class="lines-code markdown"><pre class="prettyprint linenums lang-{{.FileExt}}">{{.FileContent}}</pre></td> </tr> </tbody> </table> From f7f175a0793a53f3c50d20d89e324a610f94c442 Mon Sep 17 00:00:00 2001 From: slene <vslene@gmail.com> Date: Sun, 23 Mar 2014 22:14:29 +0800 Subject: [PATCH 38/38] fix code select range --- public/js/app.js | 83 ++++++++++++++++++++++++++++++++++++++++-------- public/js/lib.js | 2 +- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 3e995d5b0e..d5185580f6 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -31,6 +31,23 @@ var Gogits = { } }; return ajax(url, options); + }, + + changeHash: function(hash) { + if(history.pushState) { + history.pushState(null, null, hash); + } + else { + location.hash = hash; + } + }, + + deSelect: function() { + if(window.getSelection) { + window.getSelection().removeAllRanges(); + } else { + document.selection.empty(); + } } }); }(jQuery)); @@ -130,27 +147,67 @@ var Gogits = { } Gogits.renderCodeView = function () { + function selectRange($list, $select, $from){ + $list.removeClass('active'); + if($from){ + var a = parseInt($select.attr('rel').substr(1)); + var b = parseInt($from.attr('rel').substr(1)); + var c; + if(a != b){ + if(a > b){ + c = a; + a = b; + b = c; + } + var classes = []; + for(i = a; i <= b; i++) { + classes.push('.L'+i); + } + $list.filter(classes.join(',')).addClass('active'); + $.changeHash('#L' + a + '-' + 'L' + b); + return + } + } + $select.addClass('active'); + $.changeHash('#' + $select.attr('rel')); + } + + $(document).on('click', '.lines-num span', function (e) { + var $select = $(this); + var $list = $select.parent().siblings('.lines-code').find('ol.linenums > li'); + selectRange($list, $list.filter('[rel='+$select.attr('rel')+']'), (e.shiftKey?$list.filter('.active').eq(0):null)); + $.deSelect(); + }); + $('.code-view .lines-code > pre').each(function(){ var $pre = $(this); - var $lineNums = $pre.parent().siblings('.lines-num'); + var $lineCode = $pre.parent(); + var $lineNums = $lineCode.siblings('.lines-num'); if ($lineNums.length > 0) { var nums = $pre.find('ol.linenums > li').length; for (var i = 1; i <= nums; i++) { - $lineNums.append('<span id="L' + i + '" rel=".L' + i + '">' + i + '</span>'); + $lineNums.append('<span id="L' + i + '" rel="L' + i + '">' + i + '</span>'); } - - var last; - $(document).on('click', '.lines-num span', function () { - var $e = $(this); - if (last) { - last.removeClass('active'); - } - last = $e.parent().siblings('.lines-code').find('ol.linenums > ' + $e.attr('rel')); - last.addClass('active'); - window.location.href = '#' + $e.attr('id'); - }); } }); + + $(window).on('hashchange', function(e) { + var m = window.location.hash.match(/^#(L\d+)\-(L\d+)$/); + var $list = $('.code-view ol.linenums > li'); + if(m){ + var $first = $list.filter('.'+m[1]); + selectRange($list, $first, $list.filter('.'+m[2])); + $("html, body").scrollTop($first.offset().top-200); + console.log($first.offset().top); + return; + } + m = window.location.hash.match(/^#(L\d+)$/); + if(m){ + var $first = $list.filter('.'+m[1]); + selectRange($list, $first); + $("html, body").scrollTop($first.offset().top-200); + } + }).trigger('hashchange'); }; })(jQuery); diff --git a/public/js/lib.js b/public/js/lib.js index b5cc41c042..8735ac9c11 100644 --- a/public/js/lib.js +++ b/public/js/lib.js @@ -340,7 +340,7 @@ q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(? s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/, q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d= c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i<c.length;++i)b(c[i]);d===(d|0)&&c[0].setAttribute("value",d);var r=j.createElement("ol"); -r.className="linenums";for(var d=Math.max(0,d-1|0)||0,i=0,n=c.length;i<n;++i)k=c[i],k.className="L"+(i+d+1),k.firstChild||k.appendChild(j.createTextNode("\u00a0")),r.appendChild(k);a.appendChild(r)}function p(a,d){for(var g=d.length;--g>=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*</.test(d)?"default-markup":"default-code";return F[a]}function K(a){var d=a.h;try{var g=T(a.c,a.i),b=g.a; +r.className="linenums";for(var d=Math.max(0,d-1|0)||0,i=0,n=c.length;i<n;++i)k=c[i],k.setAttribute("rel", "L"+(i+d+1)),k.className="L"+(i+d+1),k.firstChild||k.appendChild(j.createTextNode("\u00a0")),r.appendChild(k);a.appendChild(r)}function p(a,d){for(var g=d.length;--g>=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*</.test(d)?"default-markup":"default-code";return F[a]}function K(a){var d=a.h;try{var g=T(a.c,a.i),b=g.a; a.a=b;a.d=g.d;a.e=0;I(d,b)(a);var s=/\bMSIE\s(\d+)/.exec(navigator.userAgent),s=s&&+s[1]<=8,d=/\n/g,x=a.a,m=x.length,g=0,j=a.d,k=j.length,b=0,c=a.g,i=c.length,r=0;c[i]=m;var n,e;for(e=n=0;e<i;)c[e]!==c[e+2]?(c[n++]=c[e++],c[n++]=c[e++]):e+=2;i=n;for(e=n=0;e<i;){for(var p=c[e],w=c[e+1],t=e+2;t+2<=i&&c[t+1]===w;)t+=2;c[n++]=p;c[n++]=w;e=t}c.length=n;var f=a.c,h;if(f)h=f.style.display,f.style.display="none";try{for(;b<k;){var l=j[b+2]||m,B=c[r+2]||m,t=Math.min(l,B),A=j[b+1],G;if(A.nodeType!==1&&(G=x.substring(g, t))){s&&(G=G.replace(d,"\r"));A.nodeValue=G;var L=A.ownerDocument,o=L.createElement("span");o.className=c[r+1];var v=A.parentNode;v.replaceChild(o,A);o.appendChild(A);g<l&&(j[b+1]=A=L.createTextNode(x.substring(t,l)),v.insertBefore(A,o.nextSibling))}g=t;g>=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],