Browse Source

merge with branch master

skyblue 11 years ago
parent
commit
4bac361605
58 changed files with 1875 additions and 551 deletions
  1. 1 0
      .gitignore
  2. 0 1
      .gopmfile
  3. 15 8
      README.md
  4. 53 0
      README_ZH.md
  5. 2 1
      bee.json
  6. 46 2
      conf/app.ini
  7. 2 2
      gogs.go
  8. 25 2
      models/action.go
  9. 145 6
      models/issue.go
  10. 2 2
      models/models.go
  11. 28 19
      models/publickey.go
  12. 26 4
      models/repo.go
  13. 9 4
      models/user.go
  14. 1 0
      modules/auth/auth.go
  15. 54 0
      modules/auth/issue.go
  16. 6 5
      modules/auth/user.go
  17. 60 2
      modules/base/conf.go
  18. 2 2
      modules/base/markdown.go
  19. 103 9
      modules/base/tool.go
  20. 36 26
      modules/middleware/auth.go
  21. 155 11
      modules/middleware/context.go
  22. 4 1
      modules/middleware/render.go
  23. 6 2
      modules/middleware/repo.go
  24. 6 2
      public/css/gogs.css
  25. 6 3
      public/css/markdown.css
  26. BIN
      public/img/404.png
  27. BIN
      public/img/500.png
  28. 116 20
      public/js/app.js
  29. 1 1
      public/js/lib.js
  30. 93 0
      routers/admin/admin.go
  31. 35 0
      routers/admin/user.go
  32. 7 0
      routers/dashboard.go
  33. 85 0
      routers/repo/issue.go
  34. 312 4
      routers/repo/repo.go
  35. 0 307
      routers/repo/single.go
  36. 60 5
      routers/user/user.go
  37. 140 26
      serve.go
  38. 32 0
      templates/admin/config.tmpl
  39. 33 1
      templates/admin/dashboard.tmpl
  40. 1 1
      templates/admin/repos.tmpl
  41. 2 1
      templates/admin/users/edit.tmpl
  42. 1 0
      templates/admin/users/new.tmpl
  43. 1 0
      templates/base/head.tmpl
  44. 1 0
      templates/repo/create.tmpl
  45. 5 5
      templates/repo/nav.tmpl
  46. 31 2
      templates/repo/setting.tmpl
  47. 2 2
      templates/repo/single_file.tmpl
  48. 9 0
      templates/status/404.tmpl
  49. 10 0
      templates/status/500.tmpl
  50. 2 1
      templates/user/active.tmpl
  51. 1 0
      templates/user/delete.tmpl
  52. 3 1
      templates/user/password.tmpl
  53. 1 0
      templates/user/publickey.tmpl
  54. 1 0
      templates/user/setting.tmpl
  55. 12 0
      templates/user/signin.tmpl
  56. 1 0
      templates/user/signup.tmpl
  57. 12 10
      update.go
  58. 72 50
      web.go

+ 1 - 0
.gitignore

@@ -5,6 +5,7 @@ gogs
 *.db
 *.log
 custom/
+data/
 .vendor/
 .idea/
 *.iml

+ 0 - 1
.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=

+ 15 - 8
README.md

@@ -1,15 +1,21 @@
-Gogs - Go Git Service [![wercker status](https://app.wercker.com/status/ad0bdb0bc450ac6f09bc56b9640a50aa/s/ "wercker status")](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [![Go Walker](http://gowalker.org/api/v1/badge)](https://gowalker.org/github.com/gogits/gogs)
+Gogs - Go Git Service [![wercker status](https://app.wercker.com/status/ad0bdb0bc450ac6f09bc56b9640a50aa/s/ "wercker status")](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [![Build Status](https://drone.io/github.com/gogits/gogs/status.png)](https://drone.io/github.com/gogits/gogs/latest) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/gogits/gogs/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
 =====================
 
-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.
+![Demo](http://gowalker.org/public/gogs_demo.gif)
 
-##### Current version: 0.1.5 Alpha
+##### Current version: 0.1.7 Alpha
+
+#### Other language version
+
+- [简体中文](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
 
@@ -21,9 +27,9 @@ There are some very good products in this category such as [gitlab](http://gitla
 ## Features
 
 - Activity timeline
-- SSH protocol support.
+- SSH/HTTPS protocol support.
 - Register/delete account.
-- Create/delete public repository.
+- Create/delete/watch public repository.
 - User profile page.
 - Repository viewer.
 - Gravatar support.
@@ -42,8 +48,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, modules design is inspired by [WeTalk](https://github.com/beego/wetalk).
+- System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog).
 
 ## Contributors
 

+ 53 - 0
README_ZH.md

@@ -0,0 +1,53 @@
+Gogs - Go Git Service [![wercker status](https://app.wercker.com/status/ad0bdb0bc450ac6f09bc56b9640a50aa/s/ "wercker status")](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [![Build Status](https://drone.io/github.com/gogits/gogs/status.png)](https://drone.io/github.com/gogits/gogs/latest) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/gogits/gogs/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
+=====================
+
+Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。
+
+![Demo](http://gowalker.org/public/gogs_demo.gif)
+
+##### 当前版本:0.1.7 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/HTTPS 协议支持
+- 注册/删除用户
+- 创建/删除/关注公开仓库
+- 用户个人信息页面
+- 仓库浏览器
+- 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) 获取完整的贡献者列表。

+ 2 - 1
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": [

+ 46 - 2
conf/app.ini

@@ -32,8 +32,14 @@ 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
+LOGIN_REMEMBER_DAYS = 7
+COOKIE_USERNAME = gogs_awesome
+COOKIE_REMEMBER_NAME = gogs_incredible
 
 [service]
 ACTIVE_CODE_LIVE_MINUTES = 180
@@ -44,6 +50,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,8 +78,38 @@ INTERVAL = 60
 ; memcache: "127.0.0.1:11211"
 HOST =
 
+[session]
+; Either "memory", "file", "redis" or "mysql", default is "memory"
+PROVIDER = file
+; Provider config options
+; memory: not have any config yet
+; 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
+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, Either "sha1", "sha256" or "md5" 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
+; 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"
+; 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
@@ -120,4 +158,10 @@ HOST =
 USER = 
 PASSWD =
 ; Receivers, can be one or more, e.g. ["1@example.com","2@example.com"]
-RECEIVERS = 
+RECEIVERS = 
+
+; For "database" mode only
+[log.database]
+LEVEL = 
+Driver = 
+CONN = 

+ 2 - 2
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 (
@@ -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.7.0323.1"
 
 func init() {
 	base.AppVer = APP_VER

+ 25 - 2
models/action.go

@@ -7,6 +7,9 @@ package models
 import (
 	"encoding/json"
 	"time"
+
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/log"
 )
 
 // Operation types of user action.
@@ -28,7 +31,8 @@ type Action struct {
 	ActUserName string // Action user name.
 	RepoId      int64
 	RepoName    string
-	Content     string
+	RefName     string
+	Content     string    `xorm:"TEXT"`
 	Created     time.Time `xorm:"created"`
 }
 
@@ -44,13 +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
 }
 
 // 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 *base.PushCommits) error {
 	bs, err := json.Marshal(commits)
 	if err != nil {
 		return err
@@ -76,9 +84,22 @@ func CommitRepoAction(userId int64, userName string,
 			Content:     string(bs),
 			RepoId:      repoId,
 			RepoName:    repoName,
+			RefName:     refName,
 		})
 		return err
 	}
+
+	// Update repository last update time.
+	repo, err := GetRepositoryByName(userId, repoName)
+	if err != nil {
+		return err
+	}
+	repo.IsBare = false
+	if err = UpdateRepository(repo); err != nil {
+		return err
+	}
+
+	log.Trace("action.CommitRepoAction: %d/%s", userId, repo.LowerName)
 	return nil
 }
 
@@ -92,6 +113,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
 }
 

+ 145 - 6
models/issue.go

@@ -4,16 +4,155 @@
 
 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
-	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 `xorm:"TEXT"`
+	Mentions    string `xorm:"TEXT"`
+	Content     string `xorm:"TEXT"`
+	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, content string, isPull bool) (*Issue, error) {
+	count, err := GetIssueCount(repoId)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO: find out mentions
+	mentions := ""
+
+	issue := &Issue{
+		Index:       count + 1,
+		Name:        name,
+		RepoId:      repoId,
+		PosterId:    userId,
+		MilestoneId: milestoneId,
+		AssigneeId:  assigneeId,
+		IsPull:      isPull,
+		Labels:      labels,
+		Mentions:    mentions,
+		Content:     content,
+	}
+	_, err = orm.Insert(issue)
+	return issue, err
 }
 
+// GetIssueCount returns count of issues in the repository.
+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)
+
+	if repoId > 0 {
+		sess.Where("repo_id=?", repoId).And("is_closed=?", isClosed)
+	} else {
+		sess.Where("is_closed=?", isClosed)
+	}
+
+	if userId > 0 {
+		sess.And("assignee_id=?", userId)
+	} else if posterId > 0 {
+		sess.And("poster_id=?", posterId)
+	} else if isMention {
+		sess.And("mentions like '%$" + base.ToStr(userId) + "|%'")
+	}
+
+	if milestoneId > 0 {
+		sess.And("milestone_id=?", milestoneId)
+	}
+
+	if len(labels) > 0 {
+		for _, label := range strings.Split(labels, ",") {
+			sess.And("mentions like '%$" + label + "|%'")
+		}
+	}
+
+	switch sortType {
+	case "oldest":
+		sess.Asc("created")
+	case "recentupdate":
+		sess.Desc("updated")
+	case "leastupdate":
+		sess.Asc("updated")
+	case "mostcomment":
+		sess.Desc("num_comments")
+	case "leastcomment":
+		sess.Asc("num_comments")
+	default:
+		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"`
 }

+ 2 - 2
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)
 	}
@@ -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
 }

+ 28 - 19
models/publickey.go

@@ -19,6 +19,8 @@ import (
 	"time"
 
 	"github.com/Unknwon/com"
+
+	"github.com/gogits/gogs/modules/log"
 )
 
 const (
@@ -78,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"`
 }
@@ -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)

+ 26 - 4
models/repo.go

@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"os/exec"
 	"path"
 	"path/filepath"
 	"regexp"
@@ -83,10 +84,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 +141,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)
@@ -196,6 +199,13 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv
 		return nil, err
 	}
 
+	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)
+	}
+
 	return repo, NewRepoAction(user, repo)
 }
 
@@ -369,6 +379,18 @@ func RepoPath(userName, repoName string) string {
 	return filepath.Join(UserPath(userName), repoName+".git")
 }
 
+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
+}
+
 // DeleteRepository deletes a repository for a user or orgnaztion.
 func DeleteRepository(userId, repoId int64, userName string) (err error) {
 	repo := &Repository{Id: repoId, OwnerId: userId}
@@ -413,9 +435,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)

+ 9 - 4
models/user.go

@@ -201,7 +201,14 @@ 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)
+	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", "is_active", "is_admin").Update(user)
 	return err
 }
 
@@ -279,9 +286,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

+ 1 - 0
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 {

+ 54 - 0
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)
+}

+ 6 - 5
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
 }
 

+ 60 - 2
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"
 )
@@ -37,21 +38,35 @@ var (
 	RunUser      string
 	RepoRootPath string
 
+	EnableHttpsClone bool
+
+	LogInRememberDays  int
+	CookieUserName     string
+	CookieRememberName string
+
 	Cfg         *goconfig.ConfigFile
 	MailService *Mailer
 
+	LogMode   string
+	LogConfig string
+
 	Cache        cache.Cache
 	CacheAdapter string
 	CacheConfig  string
 
-	LogMode   string
-	LogConfig string
+	SessionProvider string
+	SessionConfig   *session.Config
+	SessionManager  *session.Manager
+
+	PictureService  string
+	PictureRootPath string
 )
 
 var Service struct {
 	RegisterEmailConfirm   bool
 	DisenableRegisteration bool
 	RequireSignInView      bool
+	EnableCacheAvatar      bool
 	ActiveCodeLives        int
 	ResetPwdCodeLives      int
 }
@@ -82,6 +97,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() {
@@ -129,6 +145,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)
@@ -159,6 +179,34 @@ 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")
+
+	if SessionProvider == "file" {
+		os.MkdirAll(path.Dir(SessionConfig.ProviderConfig), os.ModePerm)
+	}
+
+	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") {
@@ -214,6 +262,15 @@ 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")
+
+	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 {
@@ -226,6 +283,7 @@ func NewServices() {
 	newService()
 	newLogService()
 	newCacheService()
+	newSessionService()
 	newMailService()
 	newRegisterMailService()
 }

+ 2 - 2
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,

+ 103 - 9
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)
 }
@@ -111,6 +115,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 +206,6 @@ func TimeSince(then time.Time) string {
 	}
 
 	switch {
-
 	case diff <= 0:
 		return "now"
 	case diff <= 2:
@@ -156,8 +238,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()
 }
@@ -387,6 +471,7 @@ type Actioner interface {
 	GetOpType() int
 	GetActUserName() string
 	GetRepoName() string
+	GetBranch() string
 	GetContent() string
 }
 
@@ -409,25 +494,34 @@ 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 {
+		for _, commit := range push.Commits {
 			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 push.Len > 3 {
+			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())
 	default:
 		return "invalid type"

+ 36 - 26
modules/middleware/auth.go

@@ -5,44 +5,54 @@
 package middleware
 
 import (
+	"net/url"
+
 	"github.com/codegangsta/martini"
 
 	"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.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI))
+				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
 		}
-		ctx.Data["PageIsAdmin"] = true
 	}
 }

+ 155 - 11
modules/middleware/context.go

@@ -5,14 +5,20 @@
 package middleware
 
 import (
+	"crypto/hmac"
+	"crypto/sha1"
+	"encoding/base64"
 	"fmt"
+	"html/template"
 	"net/http"
+	"strconv"
+	"strings"
 	"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,11 +33,13 @@ 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
 
+	csrfToken string
+
 	Repo struct {
 		IsValid    bool
 		IsOwner    bool
@@ -90,23 +98,157 @@ 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())
+}
+
+// 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
+	}
+
+	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,
-		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,
 		}
 
+		ctx.Data["PageStartTime"] = time.Now()
+
+		// start session
+		ctx.Session = base.SessionManager.SessionStart(res, r)
+		rw := res.(martini.ResponseWriter)
+		rw.Before(func(martini.ResponseWriter) {
+			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
 
@@ -119,7 +261,9 @@ func InitContext() martini.Handler {
 			ctx.Data["IsAdmin"] = ctx.User.IsAdmin
 		}
 
-		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)
 

+ 4 - 1
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) {

+ 6 - 2
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("/")
@@ -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

+ 6 - 2
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;
 }
 

+ 6 - 3
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;
 }
 
@@ -175,6 +174,10 @@
   margin-bottom: 0;
 }
 
+.markdown img {
+  max-width: 100%;
+}
+
 .markdown .btn {
   color: #fff;
 }

BIN
public/img/404.png


BIN
public/img/500.png


+ 116 - 20
public/js/app.js

@@ -2,6 +2,56 @@ 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);
+        },
+
+        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));
+
 (function ($) {
 
     Gogits.showTab = function (selector, index) {
@@ -65,7 +115,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();
         });
     };
@@ -77,25 +127,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 () {
@@ -115,6 +146,70 @@ 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 $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>');
+                }
+            }
+        });
+
+        $(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);
 
 // ajax utils
@@ -144,6 +239,7 @@ function initCore() {
     Gogits.initModals();
     Gogits.initDropDown();
     Gogits.renderMarkdown();
+    Gogits.renderCodeView();
 }
 
 function initRegister() {

+ 1 - 1
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"],

+ 93 - 0
routers/admin/admin.go

@@ -5,7 +5,10 @@
 package admin
 
 import (
+	"fmt"
+	"runtime"
 	"strings"
+	"time"
 
 	"github.com/codegangsta/martini"
 
@@ -14,10 +17,93 @@ import (
 	"github.com/gogits/gogs/modules/middleware"
 )
 
+var startTime = time.Now()
+
+var sysStatus struct {
+	Uptime       string
+	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() {
+	sysStatus.Uptime = base.TimeSincePro(startTime)
+
+	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")
 }
 
@@ -55,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
@@ -70,6 +157,12 @@ 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
+
 	ctx.Data["LogMode"] = base.LogMode
 	ctx.Data["LogConfig"] = base.LogConfig
 

+ 35 - 0
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")
+}

+ 7 - 0
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"] = "Page Not Found"
+	ctx.Handle(404, "home.NotFound", nil)
+}

+ 85 - 0
routers/repo/issue.go

@@ -0,0 +1,85 @@
+// 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 (
+	"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()
+	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")
+}
+
+func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) {
+	if !ctx.Repo.IsOwner {
+		ctx.Handle(404, "issue.CreateIssue", nil)
+		return
+	}
+
+	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))
+		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.Handle(404, "issue.ViewIssue", err)
+		return
+	}
+
+	issue, err := models.GetIssueById(int64(issueid))
+	if err != nil {
+		if err == models.ErrIssueNotExist {
+			ctx.Handle(404, "issue.ViewIssue", err)
+		} else {
+			ctx.Handle(200, "issue.ViewIssue", err)
+		}
+		return
+	}
+
+	ctx.Data["Title"] = issue.Name
+	ctx.Data["Issue"] = issue
+	ctx.HTML(200, "issue/view")
+}

+ 312 - 4
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"
 )
@@ -22,11 +31,16 @@ 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 {
 		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)
@@ -38,13 +52,247 @@ func Create(ctx *middleware.Context, form auth.CreateRepoForm) {
 	ctx.Handle(200, "repo.Create", err)
 }
 
-func SettingPost(ctx *middleware.Context) {
+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.Handle(404, "repo.Branches", nil)
+		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])
+		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.Handle(404, "repo.Single(GetBranches)", err)
+		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.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.Handle(404, "repo.Single", nil)
+		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.Handle(404, "repo.Single(repoFile.LookupBlob)", err)
+		} 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.Handle(404, "repo.Single(GetReposFiles)", err)
+			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 {
+				ctx.Handle(404, "repo.Single(readmeFile.LookupBlob)", err)
+				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.Handle(404, "repo.Single(GetCommit)", err)
+		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.Handle(404, "repo.Setting", nil)
+		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 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."
@@ -56,8 +304,68 @@ func SettingPost(ctx *middleware.Context) {
 			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 {
+		ctx.Handle(200, "repo.Commits", err)
+		return
+	} else if len(brs) == 0 {
+		ctx.Handle(404, "repo.Commits", nil)
+		return
 	}
 
-	log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName)
-	ctx.Redirect("/", 302)
+	ctx.Data["IsRepoToolbarCommits"] = true
+	commits, err := models.GetCommits(params["username"],
+		params["reponame"], params["branchname"])
+	if err != nil {
+		ctx.Handle(404, "repo.Commits", nil)
+		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,
+	})
 }

+ 0 - 307
routers/repo/single.go

@@ -1,307 +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 len(brs) == 0 {
-		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
-
-	// 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 {
-		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,
-	})
-}

+ 60 - 5
routers/user/user.go

@@ -6,6 +6,7 @@ package user
 
 import (
 	"fmt"
+	"net/url"
 	"strings"
 
 	"github.com/codegangsta/martini"
@@ -77,7 +78,45 @@ 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)
+		redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to"))
+		if len(redirectTo) > 0 {
+			ctx.SetCookie("redirect_to", "", -1)
+			ctx.Redirect(redirectTo)
+		} else {
+			ctx.Redirect("/")
+		}
 		return
 	}
 
@@ -88,7 +127,8 @@ 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 {
+			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,14 +137,29 @@ 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("/")
+	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("/")
 }
 
@@ -246,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.
@@ -274,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
 	}
 

+ 140 - 26
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/gogits/gogs/modules/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,16 @@ func runServ(*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, repoName)
+	repo, err := models.GetRepositoryByName(user.Id, 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)
 
 	isWrite := In(verb, COMMANDS_WRITE)
 	isRead := In(verb, COMMANDS_READONLY)
@@ -130,12 +151,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 +164,127 @@ 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
+
+	var s string
+	b := bytes.NewBufferString(s)
+
+	gitcmd.Stdout = io.MultiWriter(os.Stdout, 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())
 	}
-}
 
-func parseCmd(cmd string) (string, string) {
-	ss := strings.SplitN(cmd, " ", 2)
-	if len(ss) != 2 {
-		return "", ""
+	if !strings.HasPrefix(cmd, "git-receive-pack") {
+		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])
+	// 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)
+	}
+
+	commits := make([][]string, 0)
+	var maxCommits = 3
+	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 {
+			break
+		}
+	}
+
+	if err = models.CommitRepoAction(user.Id, user.Name,
+		repo.Id, repoName, refname, &base.PushCommits{l.Len(), 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("git", "update-server-info")
+		c.Dir = models.RepoPath(user.Name, repoName)
+		err := c.Run()
+		if err != nil {
+			log.Error("update-server-info: %v", err)
+		}
 	}
-	return verb, args
 }

+ 32 - 0
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>
@@ -45,6 +46,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 +78,36 @@
             </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
+            </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

+ 33 - 1
templates/admin/dashboard.tmpl

@@ -15,10 +15,42 @@
 
         <div class="panel panel-default">
             <div class="panel-heading">
-                System Status
+                System Monitor Status
             </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>
+                <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>

+ 1 - 1
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>

+ 2 - 1
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>
@@ -71,7 +72,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>

+ 1 - 0
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>

+ 1 - 0
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" />

+ 1 - 0
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">

+ 5 - 5
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>
+                <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>
@@ -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>
@@ -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}}

+ 31 - 2
templates/repo/setting.tmpl

@@ -12,14 +12,42 @@
     </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
             </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">Description</label>
+                        <div class="col-md-9">
+                            <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="site" value="{{.Repository.Website}}" />
+                        </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>
 
@@ -40,6 +68,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">

+ 2 - 2
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>

+ 9 - 0
templates/status/404.tmpl

@@ -0,0 +1,9 @@
+{{template "base/head" .}}
+{{template "base/navbar" .}}
+<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>
+    <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" .}}

+ 10 - 0
templates/status/500.tmpl

@@ -0,0 +1,10 @@
+{{template "base/head" .}}
+{{template "base/navbar" .}}
+<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" .}}

+ 2 - 1
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}}

+ 1 - 0
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">&times;</button>
                     <h4 class="modal-title" id="myModalLabel">Delete Account</h4>

+ 3 - 1
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>

+ 1 - 0
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">&times;</button>
                             <h4 class="modal-title" id="myModalLabel">Add SSH Key</h4>

+ 1 - 0
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">

+ 12 - 0
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}}">
@@ -18,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>

+ 1 - 0
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}}

+ 12 - 10
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()
+	}*/
 }

+ 72 - 50
web.go

@@ -12,7 +12,6 @@ import (
 
 	"github.com/codegangsta/cli"
 	"github.com/codegangsta/martini"
-	"github.com/martini-contrib/sessions"
 
 	"github.com/gogits/binding"
 
@@ -82,72 +81,95 @@ 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)
-	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)
 	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.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.Get("/help", routers.Help)
+
+	avatarCache := avatar.HttpHandler("public/img/avatar/", "public/img/avatar/default.jpg")
+	m.Get("/avatar/:hash", avatarCache.ServeHTTP)
+
+	m.Group("/user", func(r martini.Router) {
+		r.Any("/login", binding.BindIgnErr(auth.LogInForm{}), user.SignIn)
+		r.Any("/sign_up", 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)
-
-	avatarHandler := avatar.HttpHandler("public/img/avatar", "public/img/avatar/default.jpg")
-	m.Get("/avatar/:hash", avatarHandler.ServeHTTP)
-
-	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.Get("/admin/repos", reqSignIn, adminReq, admin.Repositories)
-	m.Get("/admin/config", reqSignIn, 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)
-
-	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)
-	m.Get("/:username/:reponame", ignSignIn, middleware.RepoAssignment(true), repo.Single)
-
-	m.Any("/:username/:reponame/**", ignSignIn, repo.Http)
+	adminReq := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true, AdminRequire: true})
+
+	m.Get("/admin", adminReq, admin.Dashboard)
+	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.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))
+
+	// 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.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)
 	}
 
+	// 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"))