Browse Source

Add memcached and redis Docker supported

Meaglith Ma 11 năm trước cách đây
mục cha
commit
ee7bfe2ebe
100 tập tin đã thay đổi với 5779 bổ sung1393 xóa
  1. 12 0
      .fswatch.json
  2. 1 0
      .gitignore
  3. 19 22
      .gopmfile
  4. 1 1
      CONTRIBUTING.md
  5. 20 12
      README.md
  6. 16 11
      README_ZH.md
  7. 1 1
      bee.json
  8. 61 4
      conf/app.ini
  9. 23 0
      conf/gitignore/Android
  10. 12 0
      conf/gitignore/Java
  11. 7 0
      conf/gitignore/Objective-C
  12. 8 0
      conf/supervisor.ini
  13. 1 1
      dockerfiles/README.md
  14. 3 3
      gogs.go
  15. 23 5
      models/access.go
  16. 15 2
      models/action.go
  17. 212 0
      models/git_diff.go
  18. 55 19
      models/models.go
  19. 15 0
      models/models_sqlite.go
  20. 64 6
      models/oauth2.go
  21. 2 2
      models/publickey.go
  22. 83 0
      models/release.go
  23. 250 52
      models/repo.go
  24. 84 0
      models/update.go
  25. 75 20
      models/user.go
  26. 1 3
      modules/auth/admin.go
  27. 12 12
      modules/auth/auth.go
  28. 1 3
      modules/auth/issue.go
  29. 50 0
      modules/auth/release.go
  30. 41 5
      modules/auth/repo.go
  31. 1 3
      modules/auth/setting.go
  32. 2 3
      modules/auth/user.go
  33. 3 2
      modules/avatar/avatar.go
  34. 48 0
      modules/base/base.go
  35. 11 0
      modules/base/base_memcache.go
  36. 11 0
      modules/base/base_redis.go
  37. 66 46
      modules/base/conf.go
  38. 54 3
      modules/base/markdown.go
  39. 136 0
      modules/base/template.go
  40. 41 108
      modules/base/tool.go
  41. 17 0
      modules/cron/cron.go
  42. 1 2
      modules/log/log.go
  43. 45 8
      modules/mailer/mail.go
  44. 1 1
      modules/middleware/auth.go
  45. 426 0
      modules/middleware/binding.go
  46. 701 0
      modules/middleware/binding_test.go
  47. 80 6
      modules/middleware/context.go
  48. 1 1
      modules/middleware/render.go
  49. 79 17
      modules/middleware/repo.go
  50. 396 0
      modules/social/social.go
  51. 0 0
      public/css/bootstrap.css.map
  52. 1 1
      public/css/bootstrap.min.css
  53. 198 11
      public/css/gogs.css
  54. 0 0
      public/css/todc-bootstrap.css.map
  55. 1 1
      public/css/todc-bootstrap.min.css
  56. BIN
      public/img/favicon.png
  57. 54 1
      public/js/app.js
  58. 6 0
      routers/admin/admin.go
  59. 39 27
      routers/admin/user.go
  60. 1 1
      routers/api/v1/miscellaneous.go
  61. 6 0
      routers/dashboard.go
  62. 64 38
      routers/install.go
  63. 1 2
      routers/repo/branch.go
  64. 66 20
      routers/repo/commit.go
  65. 68 0
      routers/repo/download.go
  66. 55 0
      routers/repo/git.go
  67. 496 0
      routers/repo/http.go
  68. 35 15
      routers/repo/issue.go
  69. 137 3
      routers/repo/release.go
  70. 151 104
      routers/repo/repo.go
  71. 196 0
      routers/user/home.go
  72. 58 29
      routers/user/setting.go
  73. 77 27
      routers/user/social.go
  74. 276 226
      routers/user/user.go
  75. 39 44
      serve.go
  76. 12 3
      start.sh
  77. 26 4
      templates/admin/config.tmpl
  78. 1 1
      templates/admin/dashboard.tmpl
  79. 1 1
      templates/admin/users/edit.tmpl
  80. 1 1
      templates/admin/users/new.tmpl
  81. 2 0
      templates/base/alert.tmpl
  82. 14 3
      templates/base/head.tmpl
  83. 26 4
      templates/base/navbar.tmpl
  84. 20 1
      templates/home.tmpl
  85. 5 9
      templates/install.tmpl
  86. 2 1
      templates/issue/create.tmpl
  87. 1 1
      templates/issue/view.tmpl
  88. 3 3
      templates/mail/auth/active_email.tmpl
  89. 4 4
      templates/mail/auth/register_success.tmpl
  90. 33 0
      templates/mail/auth/reset_passwd.tmpl
  91. 27 33
      templates/release/list.tmpl
  92. 70 0
      templates/release/new.tmpl
  93. 16 7
      templates/repo/commits.tmpl
  94. 10 4
      templates/repo/create.tmpl
  95. 15 329
      templates/repo/diff.tmpl
  96. 99 0
      templates/repo/migrate.tmpl
  97. 6 6
      templates/repo/nav.tmpl
  98. 38 7
      templates/repo/setting.tmpl
  99. 30 33
      templates/repo/single_list.tmpl
  100. 4 4
      templates/repo/toolbar.tmpl

+ 12 - 0
.fswatch.json

@@ -0,0 +1,12 @@
+{
+    "paths": ["."],
+    "depth": 2,
+    "exclude": [],
+    "include": ["\\.go$", "\\.ini$"],
+    "command": [
+        "bash", "-c", "go build && ./gogs web"
+    ],
+    "env": {
+        "POWERED_BY": "github.com/shxsun/fswatch"
+    }
+}

+ 1 - 0
.gitignore

@@ -33,3 +33,4 @@ _testmain.go
 *.exe~
 gogs
 __pycache__
+*.pem

+ 19 - 22
.gopmfile

@@ -1,28 +1,25 @@
 [target]
-path=github.com/gogits/gogs
+path = github.com/gogits/gogs
 
 [deps]
-github.com/codegangsta/cli=
-github.com/go-martini/martini=
-github.com/Unknwon/com=
-github.com/Unknwon/cae=
-github.com/Unknwon/goconfig=
-github.com/dchest/scrypt=
-github.com/nfnt/resize=
-github.com/lunny/xorm=
-github.com/go-sql-driver/mysql=
-github.com/lib/pq=
-github.com/gogits/logs=
-github.com/gogits/binding=
-github.com/gogits/git=
-github.com/gogits/gfm=
-github.com/gogits/cache=
-github.com/gogits/session=
-github.com/gogits/webdav=
-github.com/martini-contrib/oauth2=
-github.com/martini-contrib/sessions=
-code.google.com/p/goauth2=
+github.com/Unknwon/cae = 
+github.com/Unknwon/com = 
+github.com/Unknwon/goconfig = 
+github.com/codegangsta/cli = 
+github.com/go-martini/martini = 
+github.com/go-sql-driver/mysql = 
+github.com/go-xorm/xorm = 
+github.com/gogits/cache = 
+github.com/gogits/gfm = 
+github.com/gogits/git = 
+github.com/gogits/logs = 
+github.com/gogits/oauth2 = 
+github.com/gogits/session = 
+github.com/lib/pq = 
+github.com/nfnt/resize = 
+github.com/qiniu/log = 
+github.com/robfig/cron = 
 
 [res]
-include=templates|public|conf
+include = templates|public|conf
 

+ 1 - 1
CONTRIBUTING.md

@@ -2,7 +2,7 @@
 
 > Thanks [drone](https://github.com/drone/drone) because this guidelines sheet is forked from its [CONTRIBUTING.md](https://github.com/drone/drone/blob/master/CONTRIBUTING.md).
 
-**This document is pre^3 release, we're not ready for receiving contribution until v0.5.0 release.**
+**This document is pre^2 release, we're not ready for receiving contribution until v0.5.0 release.**
 
 Want to hack on Gogs? Awesome! Here are instructions to get you started. They are probably not perfect, please let us know if anything feels wrong or incomplete.
 

+ 20 - 12
README.md

@@ -5,9 +5,12 @@ Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language
 
 ![Demo](http://gowalker.org/public/gogs_demo.gif)
 
-##### Current version: 0.2.0 Alpha
+##### Current version: 0.3.0 Alpha
 
-#### Due to testing purpose, data of [try.gogits.org](http://try.gogits.org) has been reset in March 29, 2014 and will reset multiple times after. Please do NOT put your important data on the site.
+### NOTICES
+
+- Due to testing purpose, data of [try.gogits.org](http://try.gogits.org) has been reset in **April 14, 2014** and will reset multiple times after. Please do **NOT** put your important data on the site.
+- Demo site [try.gogits.org](http://try.gogits.org) is running under `dev` branch.
 
 #### Other language version
 
@@ -21,7 +24,7 @@ More importantly, Gogs only needs one binary to setup your own project hosting o
 
 ## Overview
 
-- Please see [Wiki](https://github.com/gogits/gogs/wiki) for project design, known issues, change log and road map.
+- Please see [Wiki](https://github.com/gogits/gogs/wiki) for project design, known issues, and change log.
 - See [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) to follow the develop team.
 - Try it before anything? Do it [online](http://try.gogits.org/Unknown/gogs) or go down to **Installation -> Install from binary** section!
 - Having troubles? Get help from [Troubleshooting](https://github.com/gogits/gogs/wiki/Troubleshooting).
@@ -29,37 +32,42 @@ More importantly, Gogs only needs one binary to setup your own project hosting o
 ## Features
 
 - Activity timeline
-- SSH/HTTPS(Clone only) protocol support.
+- SSH/HTTP(S) protocol support.
 - Register/delete/rename account.
-- Create/delete/watch/rename public repository.
-- Repository viewer.
-- Issue tracker.
+- Create/migrate/mirror/delete/watch/rename/transfer public/private repository.
+- Repository viewer/release/issue tracker.
 - Gravatar and cache support.
 - Mail service(register, issue).
 - Administration panel.
-- Supports MySQL, PostgreSQL and SQLite3(binary release only).
+- Supports MySQL, PostgreSQL and SQLite3.
+- Social account login(GitHub, Google, QQ, Weibo)
 
 ## Installation
 
 Make sure you install [Prerequirements](https://github.com/gogits/gogs/wiki/Prerequirements) first.
 
-There are two ways to install Gogs:
+There are 3 ways to install Gogs:
 
-- [Install from binary](https://github.com/gogits/gogs/wiki/Install-from-binary): **STRONGLY RECOMMENDED** for just try and deployment!
+- [Install from binary](https://github.com/gogits/gogs/wiki/Install-from-binary): **STRONGLY RECOMMENDED**
 - [Install from source](https://github.com/gogits/gogs/wiki/Install-from-source)
+- [Ship with Docker](https://github.com/gogits/gogs/tree/master/dockerfiles)
 
 ## Acknowledgments
 
-- Logo is inspired by [martini-contrib](https://github.com/martini-contrib).
 - Router and middleware mechanism of [martini](http://martini.codegangsta.io/).
 - 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).
 - Usage and modification from [beego](http://beego.me) modules.
+- Thanks [lavachen](http://www.lavachen.cn/) for designing Logo.
 - Thanks [gobuild.io](http://gobuild.io) for providing binary compile and download service.
+- Great thanks to [Docker China](http://www.dockboard.org/) for providing [dockerfiles](https://github.com/gogits/gogs/tree/master/dockerfiles).
 
 ## Contributors
 
-This project was launched by [Unknown](https://github.com/Unknwon) and [lunny](https://github.com/lunny); [fuxiaohei](https://github.com/fuxiaohei), [slene](https://github.com/slene) and [skyblue](https://github.com/shxsun) joined the team soon after. See [contributors page](https://github.com/gogits/gogs/graphs/contributors) for full list of contributors.
+This project was launched by [Unknwon](https://github.com/Unknwon) and [lunny](https://github.com/lunny); [fuxiaohei](https://github.com/fuxiaohei), [slene](https://github.com/slene) and [codeskyblue](https://github.com/codeskyblue) joined the team soon after. See [contributors page](https://github.com/gogits/gogs/graphs/contributors) for full list of contributors.
+
+[![Clone in Koding](http://learn.koding.com/btn/clone_d.png)][koding]
+[koding]: https://koding.com/Teamwork?import=https://github.com/gogits/gogs/archive/master.zip&c=git1
 
 ## License
 

+ 16 - 11
README_ZH.md

@@ -5,7 +5,7 @@ Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。
 
 ![Demo](http://gowalker.org/public/gogs_demo.gif)
 
-##### 当前版本:0.2.0 Alpha
+##### 当前版本:0.3.0 Alpha
 
 ## 开发目的
 
@@ -15,7 +15,7 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依
 
 ## 项目概览
 
-- 有关项目设计、已知问题、变更日志和路线图,请通过  [Wiki](https://github.com/gogits/gogs/wiki) 查看。
+- 有关项目设计、已知问题和变更日志,请通过  [Wiki](https://github.com/gogits/gogs/wiki) 查看。
 - 您可以到 [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) 跟随开发团队的脚步。
 - 想要先睹为快?通过 [在线体验](http://try.gogits.org/Unknown/gogs) 或查看 **安装部署 -> 二进制安装** 小节。
 - 使用过程中遇到问题?尝试从 [故障排查](https://github.com/gogits/gogs/wiki/Troubleshooting) 页面获取帮助。
@@ -23,37 +23,42 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依
 ## 功能特性
 
 - 活动时间线
-- SSH/HTTPS(仅限 Clone) 协议支持
+- SSH/HTTP(S) 协议支持
 - 注册/删除/重命名用户
-- 创建/删除/关注/重命名公开仓库
-- 仓库浏览器
-- Bug 追踪系统
+- 创建/迁移/镜像/删除/关注/重命名/转移 公开/私有 仓库
+- 仓库 浏览器/发布/缺陷追踪
 - Gravatar 以及缓存支持
 - 邮件服务(注册、Issue)
 - 管理员面板
-- 支持 MySQL、PostgreSQL 以及 SQLite3(仅限二进制版本)
+- 支持 MySQL、PostgreSQL 以及 SQLite3 数据库
+- 社交帐号登录(GitHub、Google、QQ、微博)
 
 ## 安装部署
 
 在安装 Gogs 之前,您需要先安装 [基本环境](https://github.com/gogits/gogs/wiki/Prerequirements)。
 
-然后,您可以通过以下种方式来安装 Gogs:
+然后,您可以通过以下 3 种方式来安装 Gogs:
 
-- [二进制安装](https://github.com/gogits/gogs/wiki/Install-from-binary): **强烈推荐** 适合体验者和实际部署
+- [二进制安装](https://github.com/gogits/gogs/wiki/Install-from-binary): **强烈推荐**
 - [源码安装](https://github.com/gogits/gogs/wiki/Install-from-source)
+- [采用 Docker 部署](https://github.com/gogits/gogs/tree/master/dockerfiles)
 
 ## 特别鸣谢
 
-- Logo 基于 [martini-contrib](https://github.com/martini-contrib) 修改而来。
 - 基于 [WeTalk](https://github.com/beego/wetalk) 修改的邮件服务和模块设计。
 - 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。
 - [beego](http://beego.me) 模块的使用与修改。
 - [martini](http://martini.codegangsta.io/) 的路由与中间件机制。
 - 感谢 [gobuild.io](http://gobuild.io) 提供二进制编译与下载服务。
+- 感谢 [lavachen](http://www.lavachen.cn/) 设计的 Logo。
+- 感谢 [Docker 中文社区](http://www.dockboard.org/) 提供的 [dockerfiles](https://github.com/gogits/gogs/tree/master/dockerfiles)。
 
 ## 贡献成员
 
-本项目最初由 [Unknown](https://github.com/Unknwon) 和 [lunny](https://github.com/lunny) 发起,随后 [fuxiaohei](https://github.com/fuxiaohei)、[slene](https://github.com/slene) 以及 [skyblue](https://github.com/shxsun) 加入到开发团队。您可以通过查看 [贡献者页面](https://github.com/gogits/gogs/graphs/contributors) 获取完整的贡献者列表。
+本项目最初由 [Unknown](https://github.com/Unknwon) 和 [lunny](https://github.com/lunny) 发起,随后 [fuxiaohei](https://github.com/fuxiaohei)、[slene](https://github.com/slene) 以及 [codeskyblue](https://github.com/codeskyblue) 加入到开发团队。您可以通过查看 [贡献者页面](https://github.com/gogits/gogs/graphs/contributors) 获取完整的贡献者列表。
+
+[![Clone in Koding](http://learn.koding.com/btn/clone_d.png)][koding]
+[koding]: https://koding.com/Teamwork?import=https://github.com/gogits/gogs/archive/master.zip&c=git1
 
 ## 授权许可
 

+ 1 - 1
bee.json

@@ -12,7 +12,7 @@
 		"models": "",
 		"others": [
 			"modules",
-			"$GOPATH/src/github.com/gogits/binding",
+			"$GOPATH/src/github.com/gogits/logs",
 			"$GOPATH/src/github.com/gogits/git",
 			"$GOPATH/src/github.com/gogits/gfm"
 		]

+ 61 - 4
conf/app.ini

@@ -8,17 +8,24 @@ RUN_MODE = dev
 
 [repository]
 ROOT = 
-LANG_IGNS = Google Go|C|C++|Python|Ruby|C Sharp
+SCRIPT_TYPE = bash
+LANG_IGNS = Google Go|C|C++|Python|Ruby|C Sharp|Java|Objective-C|Android
 LICENSES = Apache v2 License|GPL v2|MIT License|Affero GPL|Artistic License 2.0|BSD (3-Clause) License
 
 [server]
+PROTOCOL = http
 DOMAIN = localhost
-ROOT_URL = http://%(DOMAIN)s:%(HTTP_PORT)s/
+ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
 HTTP_ADDR = 
 HTTP_PORT = 3000
+; Generate steps:
+; $ cd path/to/gogs/custom/https
+; $ go run $GOROOT/src/pkg/crypto/tls/generate_cert.go -ca=true -duration=8760h0m0s -host=myhost.example.com
+CERT_FILE = custom/https/cert.pem
+KEY_FILE = custom/https/key.pem
 
 [database]
-; Either "mysql", "postgres" or "sqlite3"(binary release only), it's your choice
+; Either "mysql", "postgres" or "sqlite3", it's your choice
 DB_TYPE = mysql
 HOST = 127.0.0.1:3306
 NAME = gogs
@@ -46,7 +53,7 @@ RESET_PASSWD_CODE_LIVE_MINUTES = 180
 ; User need to confirm e-mail for registration
 REGISTER_EMAIL_CONFIRM = false
 ; Does not allow register and admin create account only
-DISENABLE_REGISTERATION = false
+DISABLE_REGISTRATION = false
 ; User must sign in to view anything.
 REQUIRE_SIGNIN_VIEW = false
 ; Cache avatar as picture
@@ -62,6 +69,7 @@ SEND_BUFFER_LEN = 10
 SUBJECT = %(APP_NAME)s
 ; Mail server
 ; Gmail: smtp.gmail.com:587
+; QQ: smtp.qq.com:25
 HOST = 
 ; Mail from address
 FROM = 
@@ -69,6 +77,55 @@ FROM =
 USER = 
 PASSWD = 
 
+[oauth]
+ENABLED = false
+
+[oauth.github]
+ENABLED = false
+CLIENT_ID = 
+CLIENT_SECRET = 
+SCOPES = https://api.github.com/user
+AUTH_URL = https://github.com/login/oauth/authorize
+TOKEN_URL = https://github.com/login/oauth/access_token
+
+; Get client id and secret from
+; https://console.developers.google.com/project
+[oauth.google]
+ENABLED = false
+CLIENT_ID = 
+CLIENT_SECRET = 
+SCOPES = https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile
+AUTH_URL = https://accounts.google.com/o/oauth2/auth
+TOKEN_URL = https://accounts.google.com/o/oauth2/token
+
+[oauth.qq]
+ENABLED = false
+CLIENT_ID = 
+CLIENT_SECRET = 
+SCOPES = all
+; QQ 互联
+; AUTH_URL = https://graph.qq.com/oauth2.0/authorize
+; TOKEN_URL = https://graph.qq.com/oauth2.0/token
+; Tencent weibo
+AUTH_URL = https://open.t.qq.com/cgi-bin/oauth2/authorize
+TOKEN_URL = https://open.t.qq.com/cgi-bin/oauth2/access_token
+
+[oauth.twitter]
+ENABLED = false
+CLIENT_ID = 
+CLIENT_SECRET = 
+SCOPES = all
+AUTH_URL = https://api.twitter.com/oauth/authorize
+TOKEN_URL = https://api.twitter.com/oauth/access_token
+
+[oauth.weibo]
+ENABLED = false
+CLIENT_ID = 
+CLIENT_SECRET = 
+SCOPES = all
+AUTH_URL = https://api.weibo.com/oauth2/authorize
+TOKEN_URL = https://api.weibo.com/oauth2/access_token
+
 [cache]
 ; Either "memory", "redis", or "memcache", default is "memory"
 ADAPTER = memory

+ 23 - 0
conf/gitignore/Android

@@ -0,0 +1,23 @@
+# Built application files
+*.apk
+*.ap_
+
+# Files for the Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/

+ 12 - 0
conf/gitignore/Java

@@ -0,0 +1,12 @@
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*

+ 7 - 0
conf/gitignore/Objective-C

@@ -0,0 +1,7 @@
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control?
+#
+# Pods/

+ 8 - 0
conf/supervisor.ini

@@ -0,0 +1,8 @@
+[program:gogs]
+user=git
+command = /home/git/gogs/start.sh
+directory = /home/git/gogs
+autostart = true
+stdout_logfile = /var/gogs.log
+stderr_logfile = /var/gogs-error.log
+environment=HOME="/home/git"  

+ 1 - 1
dockerfiles/README.md

@@ -37,4 +37,4 @@ http://YOUR_HOST_IP:YOUR_HOST_PORT
 ```
 
 Let's 'gogs'!
-Ouya~
+Ouya~

+ 3 - 3
gogs.go

@@ -1,3 +1,5 @@
+// +build go1.2
+
 // 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.
@@ -14,12 +16,10 @@ import (
 	"github.com/gogits/gogs/modules/base"
 )
 
-// +build go1.2
-
 // Test that go1.2 tag above is included in builds. main.go refers to this definition.
 const go12tag = true
 
-const APP_VER = "0.2.0.0403 Alpha"
+const APP_VER = "0.3.0.0421 Alpha"
 
 func init() {
 	base.AppVer = APP_VER

+ 23 - 5
models/access.go

@@ -7,6 +7,8 @@ package models
 import (
 	"strings"
 	"time"
+
+	"github.com/go-xorm/xorm"
 )
 
 // Access types.
@@ -19,7 +21,7 @@ const (
 type Access struct {
 	Id       int64
 	UserName string    `xorm:"unique(s)"`
-	RepoName string    `xorm:"unique(s)"`
+	RepoName string    `xorm:"unique(s)"` // <user name>/<repo name>
 	Mode     int       `xorm:"unique(s)"`
 	Created  time.Time `xorm:"created"`
 }
@@ -40,12 +42,28 @@ func UpdateAccess(access *Access) error {
 	return err
 }
 
+// UpdateAccess updates access information with session for rolling back.
+func UpdateAccessWithSession(sess *xorm.Session, access *Access) error {
+	if _, err := sess.Id(access.Id).Update(access); err != nil {
+		sess.Rollback()
+		return err
+	}
+	return nil
+}
+
 // HasAccess returns true if someone can read or write to given repository.
 func HasAccess(userName, repoName string, mode int) (bool, error) {
-	return orm.Get(&Access{
-		Id:       0,
+	access := &Access{
 		UserName: strings.ToLower(userName),
 		RepoName: strings.ToLower(repoName),
-		Mode:     mode,
-	})
+	}
+	has, err := orm.Get(access)
+	if err != nil {
+		return false, err
+	} else if !has {
+		return false, nil
+	} else if mode > access.Mode {
+		return false, nil
+	}
+	return true, nil
 }

+ 15 - 2
models/action.go

@@ -6,8 +6,11 @@ package models
 
 import (
 	"encoding/json"
+	"strings"
 	"time"
 
+	"github.com/gogits/git"
+
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
 )
@@ -22,6 +25,7 @@ const (
 	OP_CREATE_ISSUE
 	OP_PULL_REQUEST
 	OP_TRANSFER_REPO
+	OP_PUSH_TAG
 )
 
 // Action represents user operation type and other information to repository.,
@@ -67,7 +71,16 @@ func (a Action) GetContent() string {
 // CommitRepoAction adds new action for committing repository.
 func CommitRepoAction(userId int64, userName, actEmail string,
 	repoId int64, repoName string, refName string, commit *base.PushCommits) error {
-	log.Trace("action.CommitRepoAction(start): %d/%s", userId, repoName)
+	// log.Trace("action.CommitRepoAction(start): %d/%s", userId, repoName)
+
+	opType := OP_COMMIT_REPO
+	// Check it's tag push or branch.
+	if strings.HasPrefix(refName, "refs/tags/") {
+		opType = OP_PUSH_TAG
+		commit = &base.PushCommits{}
+	}
+
+	refName = git.RefEndName(refName)
 
 	bs, err := json.Marshal(commit)
 	if err != nil {
@@ -76,7 +89,7 @@ func CommitRepoAction(userId int64, userName, actEmail string,
 	}
 
 	if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, ActEmail: actEmail,
-		OpType: OP_COMMIT_REPO, Content: string(bs), RepoId: repoId, RepoName: repoName, RefName: refName}); err != nil {
+		OpType: opType, Content: string(bs), RepoId: repoId, RepoName: repoName, RefName: refName}); err != nil {
 		log.Error("action.CommitRepoAction(notify watchers): %d/%s", userId, repoName)
 		return err
 	}

+ 212 - 0
models/git_diff.go

@@ -0,0 +1,212 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	"bufio"
+	"io"
+	"os"
+	"os/exec"
+	"strings"
+
+	"github.com/gogits/git"
+
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/log"
+)
+
+// Diff line types.
+const (
+	DIFF_LINE_PLAIN = iota + 1
+	DIFF_LINE_ADD
+	DIFF_LINE_DEL
+	DIFF_LINE_SECTION
+)
+
+const (
+	DIFF_FILE_ADD = iota + 1
+	DIFF_FILE_CHANGE
+	DIFF_FILE_DEL
+)
+
+type DiffLine struct {
+	LeftIdx  int
+	RightIdx int
+	Type     int
+	Content  string
+}
+
+func (d DiffLine) GetType() int {
+	return d.Type
+}
+
+type DiffSection struct {
+	Name  string
+	Lines []*DiffLine
+}
+
+type DiffFile struct {
+	Name               string
+	Addition, Deletion int
+	Type               int
+	IsBin              bool
+	Sections           []*DiffSection
+}
+
+type Diff struct {
+	TotalAddition, TotalDeletion int
+	Files                        []*DiffFile
+}
+
+func (diff *Diff) NumFiles() int {
+	return len(diff.Files)
+}
+
+const DIFF_HEAD = "diff --git "
+
+func ParsePatch(reader io.Reader) (*Diff, error) {
+	scanner := bufio.NewScanner(reader)
+	var (
+		curFile    *DiffFile
+		curSection = &DiffSection{
+			Lines: make([]*DiffLine, 0, 10),
+		}
+
+		leftLine, rightLine int
+	)
+
+	diff := &Diff{Files: make([]*DiffFile, 0)}
+	var i int
+	for scanner.Scan() {
+		line := scanner.Text()
+		// fmt.Println(i, line)
+		if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") {
+			continue
+		}
+
+		i = i + 1
+
+		// Diff data too large.
+		if i == 5000 {
+			log.Warn("Diff data too large")
+			return &Diff{}, nil
+		}
+
+		if line == "" {
+			continue
+		}
+
+		switch {
+		case line[0] == ' ':
+			diffLine := &DiffLine{Type: DIFF_LINE_PLAIN, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
+			leftLine++
+			rightLine++
+			curSection.Lines = append(curSection.Lines, diffLine)
+			continue
+		case line[0] == '@':
+			curSection = &DiffSection{}
+			curFile.Sections = append(curFile.Sections, curSection)
+			ss := strings.Split(line, "@@")
+			diffLine := &DiffLine{Type: DIFF_LINE_SECTION, Content: line}
+			curSection.Lines = append(curSection.Lines, diffLine)
+
+			// Parse line number.
+			ranges := strings.Split(ss[len(ss)-2][1:], " ")
+			leftLine, _ = base.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int()
+			rightLine, _ = base.StrTo(strings.Split(ranges[1], ",")[0]).Int()
+			continue
+		case line[0] == '+':
+			curFile.Addition++
+			diff.TotalAddition++
+			diffLine := &DiffLine{Type: DIFF_LINE_ADD, Content: line, RightIdx: rightLine}
+			rightLine++
+			curSection.Lines = append(curSection.Lines, diffLine)
+			continue
+		case line[0] == '-':
+			curFile.Deletion++
+			diff.TotalDeletion++
+			diffLine := &DiffLine{Type: DIFF_LINE_DEL, Content: line, LeftIdx: leftLine}
+			if leftLine > 0 {
+				leftLine++
+			}
+			curSection.Lines = append(curSection.Lines, diffLine)
+		case strings.HasPrefix(line, "Binary"):
+			curFile.IsBin = true
+			continue
+		}
+
+		// Get new file.
+		if strings.HasPrefix(line, DIFF_HEAD) {
+			fs := strings.Split(line[len(DIFF_HEAD):], " ")
+			a := fs[0]
+
+			curFile = &DiffFile{
+				Name:     a[strings.Index(a, "/")+1:],
+				Type:     DIFF_FILE_CHANGE,
+				Sections: make([]*DiffSection, 0, 10),
+			}
+			diff.Files = append(diff.Files, curFile)
+
+			// Check file diff type.
+			for scanner.Scan() {
+				switch {
+				case strings.HasPrefix(scanner.Text(), "new file"):
+					curFile.Type = DIFF_FILE_ADD
+				case strings.HasPrefix(scanner.Text(), "deleted"):
+					curFile.Type = DIFF_FILE_DEL
+				case strings.HasPrefix(scanner.Text(), "index"):
+					curFile.Type = DIFF_FILE_CHANGE
+				}
+				if curFile.Type > 0 {
+					break
+				}
+			}
+		}
+	}
+
+	return diff, nil
+}
+
+func GetDiff(repoPath, commitid string) (*Diff, error) {
+	repo, err := git.OpenRepository(repoPath)
+	if err != nil {
+		return nil, err
+	}
+
+	commit, err := repo.GetCommit(commitid)
+	if err != nil {
+		return nil, err
+	}
+
+	// First commit of repository.
+	if commit.ParentCount() == 0 {
+		rd, wr := io.Pipe()
+		go func() {
+			cmd := exec.Command("git", "show", commitid)
+			cmd.Dir = repoPath
+			cmd.Stdout = wr
+			cmd.Stdin = os.Stdin
+			cmd.Stderr = os.Stderr
+			cmd.Run()
+			wr.Close()
+		}()
+		defer rd.Close()
+		return ParsePatch(rd)
+	}
+
+	rd, wr := io.Pipe()
+	go func() {
+		c, _ := commit.Parent(0)
+		cmd := exec.Command("git", "diff", c.Id.String(), commitid)
+		cmd.Dir = repoPath
+		cmd.Stdout = wr
+		cmd.Stdin = os.Stdin
+		cmd.Stderr = os.Stderr
+		cmd.Run()
+		wr.Close()
+	}()
+	defer rd.Close()
+	return ParsePatch(rd)
+}

+ 55 - 19
models/models.go

@@ -8,26 +8,35 @@ import (
 	"fmt"
 	"os"
 	"path"
+	"strings"
 
 	_ "github.com/go-sql-driver/mysql"
+	"github.com/go-xorm/xorm"
 	_ "github.com/lib/pq"
-	"github.com/lunny/xorm"
-	// _ "github.com/mattn/go-sqlite3"
 
 	"github.com/gogits/gogs/modules/base"
 )
 
 var (
-	orm       *xorm.Engine
+	orm    *xorm.Engine
+	tables []interface{}
+
 	HasEngine bool
 
 	DbCfg struct {
 		Type, Host, Name, User, Pwd, Path, SslMode string
 	}
 
-	UseSQLite3 bool
+	EnableSQLite3 bool
+	UseSQLite3    bool
 )
 
+func init() {
+	tables = append(tables, new(User), new(PublicKey), new(Repository), new(Watch),
+		new(Action), new(Access), new(Issue), new(Comment), new(Oauth2), new(Follow),
+		new(Mirror), new(Release))
+}
+
 func LoadModelsConfig() {
 	DbCfg.Type = base.Cfg.MustValue("database", "DB_TYPE")
 	if DbCfg.Type == "sqlite3" {
@@ -47,20 +56,31 @@ func NewTestEngine(x *xorm.Engine) (err error) {
 		x, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8",
 			DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name))
 	case "postgres":
-		x, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s",
-			DbCfg.User, DbCfg.Pwd, DbCfg.Name, DbCfg.SslMode))
-	// case "sqlite3":
-	// 	os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
-	// 	x, err = xorm.NewEngine("sqlite3", DbCfg.Path)
+		var host, port = "127.0.0.1", "5432"
+		fields := strings.Split(DbCfg.Host, ":")
+		if len(fields) > 0 {
+			host = fields[0]
+		}
+		if len(fields) > 1 {
+			port = fields[1]
+		}
+		cnnstr := fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s",
+			DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)
+		//fmt.Println(cnnstr)
+		x, err = xorm.NewEngine("postgres", cnnstr)
+	case "sqlite3":
+		if !EnableSQLite3 {
+			return fmt.Errorf("Unknown database type: %s", DbCfg.Type)
+		}
+		os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
+		x, err = xorm.NewEngine("sqlite3", DbCfg.Path)
 	default:
 		return fmt.Errorf("Unknown database type: %s", DbCfg.Type)
 	}
 	if err != nil {
 		return fmt.Errorf("models.init(fail to conntect database): %v", err)
 	}
-
-	return x.Sync(new(User), new(PublicKey), new(Repository), new(Watch),
-		new(Action), new(Access), new(Issue), new(Comment))
+	return x.Sync(tables...)
 }
 
 func SetEngine() (err error) {
@@ -69,8 +89,16 @@ func SetEngine() (err error) {
 		orm, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8",
 			DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name))
 	case "postgres":
-		orm, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s",
-			DbCfg.User, DbCfg.Pwd, DbCfg.Name, DbCfg.SslMode))
+		var host, port = "127.0.0.1", "5432"
+		fields := strings.Split(DbCfg.Host, ":")
+		if len(fields) > 0 {
+			host = fields[0]
+		}
+		if len(fields) > 1 {
+			port = fields[1]
+		}
+		orm, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s",
+			DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode))
 	case "sqlite3":
 		os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
 		orm, err = xorm.NewEngine("sqlite3", DbCfg.Path)
@@ -91,7 +119,7 @@ func SetEngine() (err error) {
 	if err != nil {
 		return fmt.Errorf("models.init(fail to create xorm.log): %v", err)
 	}
-	orm.Logger = f
+	orm.Logger = xorm.NewSimpleLogger(f)
 
 	orm.ShowSQL = true
 	orm.ShowDebug = true
@@ -102,16 +130,19 @@ func SetEngine() (err error) {
 func NewEngine() (err error) {
 	if err = SetEngine(); err != nil {
 		return err
-	} else if err = orm.Sync(new(User), new(PublicKey), new(Repository), new(Watch),
-		new(Action), new(Access), new(Issue), new(Comment)); err != nil {
-		return fmt.Errorf("sync database struct error: %v", err)
+	}
+	if err = orm.Sync(tables...); err != nil {
+		return fmt.Errorf("sync database struct error: %v\n", err)
 	}
 	return nil
 }
 
 type Statistic struct {
 	Counter struct {
-		User, PublicKey, Repo, Watch, Action, Access int64
+		User, PublicKey, Repo,
+		Watch, Action, Access,
+		Issue, Comment,
+		Mirror, Oauth, Release int64
 	}
 }
 
@@ -122,5 +153,10 @@ 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))
+	stats.Counter.Issue, _ = orm.Count(new(Issue))
+	stats.Counter.Comment, _ = orm.Count(new(Comment))
+	stats.Counter.Mirror, _ = orm.Count(new(Mirror))
+	stats.Counter.Oauth, _ = orm.Count(new(Oauth2))
+	stats.Counter.Release, _ = orm.Count(new(Release))
 	return
 }

+ 15 - 0
models/models_sqlite.go

@@ -0,0 +1,15 @@
+// +build sqlite
+
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	_ "github.com/mattn/go-sqlite3"
+)
+
+func init() {
+	EnableSQLite3 = true
+}

+ 64 - 6
models/oauth2.go

@@ -1,18 +1,76 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
 package models
 
-import "time"
+import (
+	"errors"
+)
 
 // OT: Oauth2 Type
 const (
 	OT_GITHUB = iota + 1
 	OT_GOOGLE
 	OT_TWITTER
+	OT_QQ
+	OT_WEIBO
+	OT_BITBUCKET
+	OT_OSCHINA
+	OT_FACEBOOK
+)
+
+var (
+	ErrOauth2RecordNotExist = errors.New("OAuth2 record does not exist")
+	ErrOauth2NotAssociated  = errors.New("OAuth2 is not associated with user")
 )
 
 type Oauth2 struct {
-	Uid         int64     `xorm:"pk"`               // userId
-	Type        int       `xorm:"pk unique(oauth)"` // twitter,github,google...
-	Identity    string    `xorm:"pk unique(oauth)"` // id..
-	Token       string    `xorm:"VARCHAR(200) not null"`
-	RefreshTime time.Time `xorm:"created"`
+	Id       int64
+	Uid      int64  `xorm:"unique(s)"` // userId
+	User     *User  `xorm:"-"`
+	Type     int    `xorm:"unique(s) unique(oauth)"` // twitter,github,google...
+	Identity string `xorm:"unique(s) unique(oauth)"` // id..
+	Token    string `xorm:"TEXT not null"`
+}
+
+func BindUserOauth2(userId, oauthId int64) error {
+	_, err := orm.Id(oauthId).Update(&Oauth2{Uid: userId})
+	return err
+}
+
+func AddOauth2(oa *Oauth2) error {
+	_, err := orm.Insert(oa)
+	return err
+}
+
+func GetOauth2(identity string) (oa *Oauth2, err error) {
+	oa = &Oauth2{Identity: identity}
+	isExist, err := orm.Get(oa)
+	if err != nil {
+		return
+	} else if !isExist {
+		return nil, ErrOauth2RecordNotExist
+	} else if oa.Uid == -1 {
+		return oa, ErrOauth2NotAssociated
+	}
+	oa.User, err = GetUserById(oa.Uid)
+	return oa, err
+}
+
+func GetOauth2ById(id int64) (oa *Oauth2, err error) {
+	oa = new(Oauth2)
+	has, err := orm.Id(id).Get(oa)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrOauth2RecordNotExist
+	}
+	return oa, nil
+}
+
+// GetOauthByUserId returns list of oauthes that are releated to given user.
+func GetOauthByUserId(uid int64) (oas []*Oauth2, err error) {
+	err = orm.Find(&oas, Oauth2{Uid: uid})
+	return oas, err
 }

+ 2 - 2
models/publickey.go

@@ -77,8 +77,8 @@ func init() {
 // PublicKey represents a SSH key of user.
 type PublicKey struct {
 	Id          int64
-	OwnerId     int64  `xorm:" index not null"`
-	Name        string `xorm:" not null"` //UNIQUE(s)
+	OwnerId     int64  `xorm:"unique(s) index not null"`
+	Name        string `xorm:"unique(s) not null"`
 	Fingerprint string
 	Content     string    `xorm:"TEXT not null"`
 	Created     time.Time `xorm:"created"`

+ 83 - 0
models/release.go

@@ -0,0 +1,83 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	"errors"
+	"strings"
+	"time"
+
+	"github.com/Unknwon/com"
+	"github.com/gogits/git"
+)
+
+var (
+	ErrReleaseAlreadyExist = errors.New("Release already exist")
+)
+
+// Release represents a release of repository.
+type Release struct {
+	Id               int64
+	RepoId           int64
+	PublisherId      int64
+	Publisher        *User `xorm:"-"`
+	Title            string
+	TagName          string
+	LowerTagName     string
+	SHA1             string
+	NumCommits       int
+	NumCommitsBehind int    `xorm:"-"`
+	Note             string `xorm:"TEXT"`
+	IsPrerelease     bool
+	Created          time.Time `xorm:"created"`
+}
+
+// GetReleasesByRepoId returns a list of releases of repository.
+func GetReleasesByRepoId(repoId int64) (rels []*Release, err error) {
+	err = orm.Desc("created").Find(&rels, Release{RepoId: repoId})
+	return rels, err
+}
+
+// IsReleaseExist returns true if release with given tag name already exists.
+func IsReleaseExist(repoId int64, tagName string) (bool, error) {
+	if len(tagName) == 0 {
+		return false, nil
+	}
+
+	return orm.Get(&Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)})
+}
+
+// CreateRelease creates a new release of repository.
+func CreateRelease(repoPath string, rel *Release, gitRepo *git.Repository) error {
+	isExist, err := IsReleaseExist(rel.RepoId, rel.TagName)
+	if err != nil {
+		return err
+	} else if isExist {
+		return ErrReleaseAlreadyExist
+	}
+
+	if !git.IsTagExist(repoPath, rel.TagName) {
+		_, stderr, err := com.ExecCmdDir(repoPath, "git", "tag", rel.TagName, "-m", rel.Title)
+		if err != nil {
+			return err
+		} else if strings.Contains(stderr, "fatal:") {
+			return errors.New(stderr)
+		}
+	} else {
+		commit, err := gitRepo.GetCommitOfTag(rel.TagName)
+		if err != nil {
+			return err
+		}
+
+		rel.NumCommits, err = commit.CommitsCount()
+		if err != nil {
+			return err
+		}
+	}
+
+	rel.LowerTagName = strings.ToLower(rel.TagName)
+	_, err = orm.InsertOne(rel)
+	return err
+}

+ 250 - 52
models/repo.go

@@ -30,7 +30,8 @@ var (
 	ErrRepoNotExist      = errors.New("Repository does not exist")
 	ErrRepoFileNotExist  = errors.New("Target Repo file does not exist")
 	ErrRepoNameIllegal   = errors.New("Repository name contains illegal characters")
-	ErrRepoFileNotLoaded = fmt.Errorf("repo file not loaded")
+	ErrRepoFileNotLoaded = errors.New("repo file not loaded")
+	ErrMirrorNotExist    = errors.New("Mirror does not exist")
 )
 
 var (
@@ -65,6 +66,7 @@ func NewRepoContext() {
 type Repository struct {
 	Id              int64
 	OwnerId         int64 `xorm:"unique(s)"`
+	Owner           *User `xorm:"-"`
 	ForkId          int64
 	LowerName       string `xorm:"unique(s) index not null"`
 	Name            string `xorm:"index not null"`
@@ -74,11 +76,14 @@ type Repository struct {
 	NumStars        int
 	NumForks        int
 	NumIssues       int
-	NumReleases     int `xorm:"NOT NULL"`
 	NumClosedIssues int
 	NumOpenIssues   int `xorm:"-"`
+	NumTags         int `xorm:"-"`
 	IsPrivate       bool
+	IsMirror        bool
 	IsBare          bool
+	IsGoget         bool
+	DefaultBranch   string
 	Created         time.Time `xorm:"created"`
 	Updated         time.Time `xorm:"updated"`
 }
@@ -117,13 +122,133 @@ func IsLegalName(repoName string) bool {
 	return true
 }
 
+// Mirror represents a mirror information of repository.
+type Mirror struct {
+	Id         int64
+	RepoId     int64
+	RepoName   string    // <user name>/<repo name>
+	Interval   int       // Hour.
+	Updated    time.Time `xorm:"UPDATED"`
+	NextUpdate time.Time
+}
+
+func GetMirror(repoId int64) (*Mirror, error) {
+	m := &Mirror{RepoId: repoId}
+	has, err := orm.Get(m)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrMirrorNotExist
+	}
+	return m, nil
+}
+
+func UpdateMirror(m *Mirror) error {
+	_, err := orm.Id(m.Id).Update(m)
+	return err
+}
+
+// MirrorUpdate checks and updates mirror repositories.
+func MirrorUpdate() {
+	if err := orm.Iterate(new(Mirror), func(idx int, bean interface{}) error {
+		m := bean.(*Mirror)
+		if m.NextUpdate.After(time.Now()) {
+			return nil
+		}
+
+		repoPath := filepath.Join(base.RepoRootPath, m.RepoName+".git")
+		_, stderr, err := com.ExecCmdDir(repoPath, "git", "remote", "update")
+		if err != nil {
+			return err
+		} else if strings.Contains(stderr, "fatal:") {
+			return errors.New(stderr)
+		} else if err = git.UnpackRefs(repoPath); err != nil {
+			return err
+		}
+
+		m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour)
+		return UpdateMirror(m)
+	}); err != nil {
+		log.Error("repo.MirrorUpdate: %v", err)
+	}
+}
+
+// MirrorRepository creates a mirror repository from source.
+func MirrorRepository(repoId int64, userName, repoName, repoPath, url string) error {
+	_, stderr, err := com.ExecCmd("git", "clone", "--mirror", url, repoPath)
+	if err != nil {
+		return err
+	} else if strings.Contains(stderr, "fatal:") {
+		return errors.New(stderr)
+	}
+
+	if _, err = orm.InsertOne(&Mirror{
+		RepoId:     repoId,
+		RepoName:   strings.ToLower(userName + "/" + repoName),
+		Interval:   24,
+		NextUpdate: time.Now().Add(24 * time.Hour),
+	}); err != nil {
+		return err
+	}
+
+	return git.UnpackRefs(repoPath)
+}
+
+// MigrateRepository migrates a existing repository from other project hosting.
+func MigrateRepository(user *User, name, desc string, private, mirror bool, url string) (*Repository, error) {
+	repo, err := CreateRepository(user, name, desc, "", "", private, mirror, false)
+	if err != nil {
+		return nil, err
+	}
+
+	// Clone to temprory path and do the init commit.
+	tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()))
+	os.MkdirAll(tmpDir, os.ModePerm)
+
+	repoPath := RepoPath(user.Name, name)
+
+	repo.IsBare = false
+	if mirror {
+		if err = MirrorRepository(repo.Id, user.Name, repo.Name, repoPath, url); err != nil {
+			return repo, err
+		}
+		repo.IsMirror = true
+		return repo, UpdateRepository(repo)
+	}
+
+	// Clone from local repository.
+	_, stderr, err := com.ExecCmd("git", "clone", repoPath, tmpDir)
+	if err != nil {
+		return repo, err
+	} else if strings.Contains(stderr, "fatal:") {
+		return repo, errors.New("git clone: " + stderr)
+	}
+
+	// Pull data from source.
+	_, stderr, err = com.ExecCmdDir(tmpDir, "git", "pull", url)
+	if err != nil {
+		return repo, err
+	} else if strings.Contains(stderr, "fatal:") {
+		return repo, errors.New("git pull: " + stderr)
+	}
+
+	// Push data to local repository.
+	if _, stderr, err = com.ExecCmdDir(tmpDir, "git", "push", "origin", "master"); err != nil {
+		return repo, err
+	} else if strings.Contains(stderr, "fatal:") {
+		return repo, errors.New("git push: " + stderr)
+	}
+
+	return repo, UpdateRepository(repo)
+}
+
 // CreateRepository creates a repository for given user or orgnaziation.
-func CreateRepository(user *User, repoName, desc, repoLang, license string, private bool, initReadme bool) (*Repository, error) {
-	if !IsLegalName(repoName) {
+func CreateRepository(user *User, name, desc, lang, license string, private, mirror, initReadme bool) (*Repository, error) {
+	if !IsLegalName(name) {
 		return nil, ErrRepoNameIllegal
 	}
 
-	isExist, err := IsRepositoryExist(user, repoName)
+	isExist, err := IsRepositoryExist(user, name)
 	if err != nil {
 		return nil, err
 	} else if isExist {
@@ -131,18 +256,16 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv
 	}
 
 	repo := &Repository{
-		OwnerId:     user.Id,
-		Name:        repoName,
-		LowerName:   strings.ToLower(repoName),
-		Description: desc,
-		IsPrivate:   private,
-		IsBare:      repoLang == "" && license == "" && !initReadme,
+		OwnerId:       user.Id,
+		Name:          name,
+		LowerName:     strings.ToLower(name),
+		Description:   desc,
+		IsPrivate:     private,
+		IsBare:        lang == "" && license == "" && !initReadme,
+		DefaultBranch: "master",
 	}
+	repoPath := RepoPath(user.Name, repo.Name)
 
-	repoPath := RepoPath(user.Name, repoName)
-	if err = initRepository(repoPath, user, repo, initReadme, repoLang, license); err != nil {
-		return nil, err
-	}
 	sess := orm.NewSession()
 	defer sess.Close()
 	sess.Begin()
@@ -151,23 +274,27 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv
 		if err2 := os.RemoveAll(repoPath); err2 != nil {
 			log.Error("repo.CreateRepository(repo): %v", err)
 			return nil, errors.New(fmt.Sprintf(
-				"delete repo directory %s/%s failed(1): %v", user.Name, repoName, err2))
+				"delete repo directory %s/%s failed(1): %v", user.Name, repo.Name, err2))
 		}
 		sess.Rollback()
 		return nil, err
 	}
 
+	mode := AU_WRITABLE
+	if mirror {
+		mode = AU_READABLE
+	}
 	access := Access{
 		UserName: user.LowerName,
 		RepoName: strings.ToLower(path.Join(user.Name, repo.Name)),
-		Mode:     AU_WRITABLE,
+		Mode:     mode,
 	}
 	if _, err = sess.Insert(&access); err != nil {
 		sess.Rollback()
 		if err2 := os.RemoveAll(repoPath); err2 != nil {
 			log.Error("repo.CreateRepository(access): %v", err)
 			return nil, errors.New(fmt.Sprintf(
-				"delete repo directory %s/%s failed(2): %v", user.Name, repoName, err2))
+				"delete repo directory %s/%s failed(2): %v", user.Name, repo.Name, err2))
 		}
 		return nil, err
 	}
@@ -178,7 +305,7 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv
 		if err2 := os.RemoveAll(repoPath); err2 != nil {
 			log.Error("repo.CreateRepository(repo count): %v", err)
 			return nil, errors.New(fmt.Sprintf(
-				"delete repo directory %s/%s failed(3): %v", user.Name, repoName, err2))
+				"delete repo directory %s/%s failed(3): %v", user.Name, repo.Name, err2))
 		}
 		return nil, err
 	}
@@ -188,25 +315,36 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv
 		if err2 := os.RemoveAll(repoPath); err2 != nil {
 			log.Error("repo.CreateRepository(commit): %v", err)
 			return nil, errors.New(fmt.Sprintf(
-				"delete repo directory %s/%s failed(3): %v", user.Name, repoName, err2))
+				"delete repo directory %s/%s failed(3): %v", user.Name, repo.Name, err2))
 		}
 		return nil, err
 	}
 
-	c := exec.Command("git", "update-server-info")
-	c.Dir = repoPath
-	if err = c.Run(); err != nil {
-		log.Error("repo.CreateRepository(exec update-server-info): %v", err)
-	}
-
-	if err = NewRepoAction(user, repo); err != nil {
-		log.Error("repo.CreateRepository(NewRepoAction): %v", err)
+	if !repo.IsPrivate {
+		if err = NewRepoAction(user, repo); err != nil {
+			log.Error("repo.CreateRepository(NewRepoAction): %v", err)
+		}
 	}
 
 	if err = WatchRepo(user.Id, repo.Id, true); err != nil {
 		log.Error("repo.CreateRepository(WatchRepo): %v", err)
 	}
 
+	// No need for init for mirror.
+	if mirror {
+		return repo, nil
+	}
+
+	if err = initRepository(repoPath, user, repo, initReadme, lang, license); err != nil {
+		return nil, err
+	}
+
+	c := exec.Command("git", "update-server-info")
+	c.Dir = repoPath
+	if err = c.Run(); err != nil {
+		log.Error("repo.CreateRepository(exec update-server-info): %v", err)
+	}
+
 	return repo, nil
 }
 
@@ -227,24 +365,21 @@ func initRepoCommit(tmpPath string, sig *git.Signature) (err error) {
 	var stderr string
 	if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "add", "--all"); err != nil {
 		return err
-	}
-	if len(stderr) > 0 {
-		log.Trace("stderr(1): %s", stderr)
+	} else if strings.Contains(stderr, "fatal:") {
+		return errors.New("git add: " + stderr)
 	}
 
 	if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
 		"-m", "Init commit"); err != nil {
 		return err
-	}
-	if len(stderr) > 0 {
-		log.Trace("stderr(2): %s", stderr)
+	} else if strings.Contains(stderr, "fatal:") {
+		return errors.New("git commit: " + stderr)
 	}
 
 	if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "push", "origin", "master"); err != nil {
 		return err
-	}
-	if len(stderr) > 0 {
-		log.Trace("stderr(3): %s", stderr)
+	} else if strings.Contains(stderr, "fatal:") {
+		return errors.New("git push: " + stderr)
 	}
 	return nil
 }
@@ -260,6 +395,13 @@ func createHookUpdate(hookPath, content string) error {
 	return err
 }
 
+// SetRepoEnvs sets environment variables for command update.
+func SetRepoEnvs(userId int64, userName, repoName string) {
+	os.Setenv("userId", base.ToStr(userId))
+	os.Setenv("userName", userName)
+	os.Setenv("repoName", repoName)
+}
+
 // InitRepository initializes README and .gitignore if needed.
 func initRepository(f string, user *User, repo *Repository, initReadme bool, repoLang, license string) error {
 	repoPath := RepoPath(user.Name, repo.Name)
@@ -271,7 +413,7 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
 
 	// hook/post-update
 	if err := createHookUpdate(filepath.Join(repoPath, "hooks", "update"),
-		fmt.Sprintf("#!/usr/bin/env bash\n%s update $1 $2 $3\n",
+		fmt.Sprintf("#!/usr/bin/env %s\n%s update $1 $2 $3\n", base.ScriptType,
 			strings.Replace(appPath, "\\", "/", -1))); err != nil {
 		return err
 	}
@@ -292,8 +434,11 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
 	tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()))
 	os.MkdirAll(tmpDir, os.ModePerm)
 
-	if _, _, err := com.ExecCmd("git", "clone", repoPath, tmpDir); err != nil {
+	_, stderr, err := com.ExecCmd("git", "clone", repoPath, tmpDir)
+	if err != nil {
 		return err
+	} else if strings.Contains(stderr, "fatal:") {
+		return errors.New("git clone: " + stderr)
 	}
 
 	// README
@@ -310,7 +455,7 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
 	if repoLang != "" {
 		filePath := "conf/gitignore/" + repoLang
 		if com.IsFile(filePath) {
-			if _, err := com.Copy(filePath,
+			if err := com.Copy(filePath,
 				filepath.Join(tmpDir, fileName["gitign"])); err != nil {
 				return err
 			}
@@ -321,7 +466,7 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
 	if license != "" {
 		filePath := "conf/license/" + license
 		if com.IsFile(filePath) {
-			if _, err := com.Copy(filePath,
+			if err := com.Copy(filePath,
 				filepath.Join(tmpDir, fileName["license"])); err != nil {
 				return err
 			}
@@ -332,6 +477,8 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
 		return nil
 	}
 
+	SetRepoEnvs(user.Id, user.Name, repo.Name)
+
 	// Apply changes and commit.
 	return initRepoCommit(tmpDir, user.NewGitSig())
 }
@@ -365,6 +512,7 @@ func GetRepos(num, offset int) ([]UserRepo, error) {
 	return urepos, nil
 }
 
+// RepoPath returns repository path by given user and repository name.
 func RepoPath(userName, repoName string) string {
 	return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git")
 }
@@ -381,45 +529,62 @@ func TransferOwnership(user *User, newOwner string, repo *Repository) (err error
 	if err = orm.Find(&accesses, &Access{RepoName: user.LowerName + "/" + repo.LowerName}); err != nil {
 		return err
 	}
+
+	sess := orm.NewSession()
+	defer sess.Close()
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
 	for i := range accesses {
 		accesses[i].RepoName = newUser.LowerName + "/" + repo.LowerName
 		if accesses[i].UserName == user.LowerName {
 			accesses[i].UserName = newUser.LowerName
 		}
-		if err = UpdateAccess(&accesses[i]); err != nil {
+		if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil {
 			return err
 		}
 	}
 
 	// Update repository.
 	repo.OwnerId = newUser.Id
-	if _, err := orm.Id(repo.Id).Update(repo); err != nil {
+	if _, err := sess.Id(repo.Id).Update(repo); err != nil {
+		sess.Rollback()
 		return err
 	}
 
 	// Update user repository number.
 	rawSql := "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?"
-	if _, err = orm.Exec(rawSql, newUser.Id); err != nil {
+	if _, err = sess.Exec(rawSql, newUser.Id); err != nil {
+		sess.Rollback()
 		return err
 	}
 	rawSql = "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?"
-	if _, err = orm.Exec(rawSql, user.Id); err != nil {
+	if _, err = sess.Exec(rawSql, user.Id); err != nil {
+		sess.Rollback()
 		return err
 	}
 
 	// Add watch of new owner to repository.
 	if !IsWatching(newUser.Id, repo.Id) {
 		if err = WatchRepo(newUser.Id, repo.Id, true); err != nil {
+			sess.Rollback()
 			return err
 		}
 	}
 
 	if err = TransferRepoAction(user, newUser, repo); err != nil {
+		sess.Rollback()
 		return err
 	}
 
 	// Change repository directory name.
-	return os.Rename(RepoPath(user.Name, repo.Name), RepoPath(newUser.Name, repo.Name))
+	if err = os.Rename(RepoPath(user.Name, repo.Name), RepoPath(newUser.Name, repo.Name)); err != nil {
+		sess.Rollback()
+		return err
+	}
+
+	return sess.Commit()
 }
 
 // ChangeRepositoryName changes all corresponding setting from old repository name to new one.
@@ -429,15 +594,27 @@ func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error)
 	if err = orm.Find(&accesses, &Access{RepoName: strings.ToLower(userName + "/" + oldRepoName)}); err != nil {
 		return err
 	}
+
+	sess := orm.NewSession()
+	defer sess.Close()
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
 	for i := range accesses {
 		accesses[i].RepoName = userName + "/" + newRepoName
-		if err = UpdateAccess(&accesses[i]); err != nil {
+		if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil {
 			return err
 		}
 	}
 
 	// Change repository directory name.
-	return os.Rename(RepoPath(userName, oldRepoName), RepoPath(userName, newRepoName))
+	if err = os.Rename(RepoPath(userName, oldRepoName), RepoPath(userName, newRepoName)); err != nil {
+		sess.Rollback()
+		return err
+	}
+
+	return sess.Commit()
 }
 
 func UpdateRepository(repo *Repository) error {
@@ -476,8 +653,7 @@ func DeleteRepository(userId, repoId int64, userName string) (err error) {
 		sess.Rollback()
 		return err
 	}
-	rawSql := "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?"
-	if _, err = sess.Exec(rawSql, userId); err != nil {
+	if _, err := sess.Delete(&Action{RepoId: repo.Id}); err != nil {
 		sess.Rollback()
 		return err
 	}
@@ -485,6 +661,16 @@ func DeleteRepository(userId, repoId int64, userName string) (err error) {
 		sess.Rollback()
 		return err
 	}
+	if _, err = sess.Delete(&Mirror{RepoId: repoId}); err != nil {
+		sess.Rollback()
+		return err
+	}
+
+	rawSql := "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?"
+	if _, err = sess.Exec(rawSql, userId); err != nil {
+		sess.Rollback()
+		return err
+	}
 	if err = sess.Commit(); err != nil {
 		sess.Rollback()
 		return err
@@ -525,12 +711,24 @@ func GetRepositoryById(id int64) (*Repository, error) {
 }
 
 // GetRepositories returns the list of repositories of given user.
-func GetRepositories(user *User) ([]Repository, error) {
+func GetRepositories(user *User, private bool) ([]Repository, error) {
 	repos := make([]Repository, 0, 10)
-	err := orm.Desc("updated").Find(&repos, &Repository{OwnerId: user.Id})
+	sess := orm.Desc("updated")
+	if !private {
+		sess.Where("is_private=?", false)
+	}
+
+	err := sess.Find(&repos, &Repository{OwnerId: user.Id})
+	return repos, err
+}
+
+// GetRecentUpdatedRepositories returns the list of repositories that are recently updated.
+func GetRecentUpdatedRepositories() (repos []*Repository, err error) {
+	err = orm.Where("is_private=?", false).Limit(5).Desc("updated").Find(&repos)
 	return repos, err
 }
 
+// GetRepositoryCount returns the total number of repositories of user.
 func GetRepositoryCount(user *User) (int64, error) {
 	return orm.Count(&Repository{OwnerId: user.Id})
 }

+ 84 - 0
models/update.go

@@ -0,0 +1,84 @@
+package models
+
+import (
+	"container/list"
+	"os/exec"
+	"strings"
+
+	"github.com/gogits/git"
+	"github.com/gogits/gogs/modules/base"
+	qlog "github.com/qiniu/log"
+)
+
+func Update(refName, oldCommitId, newCommitId, userName, repoName string, userId int64) {
+	isNew := strings.HasPrefix(oldCommitId, "0000000")
+	if isNew &&
+		strings.HasPrefix(newCommitId, "0000000") {
+		qlog.Fatal("old rev and new rev both 000000")
+	}
+
+	f := RepoPath(userName, repoName)
+
+	gitUpdate := exec.Command("git", "update-server-info")
+	gitUpdate.Dir = f
+	gitUpdate.Run()
+
+	repo, err := git.OpenRepository(f)
+	if err != nil {
+		qlog.Fatalf("runUpdate.Open repoId: %v", err)
+	}
+
+	newCommit, err := repo.GetCommit(newCommitId)
+	if err != nil {
+		qlog.Fatalf("runUpdate GetCommit of newCommitId: %v", err)
+		return
+	}
+
+	var l *list.List
+	// if a new branch
+	if isNew {
+		l, err = newCommit.CommitsBefore()
+		if err != nil {
+			qlog.Fatalf("Find CommitsBefore erro: %v", err)
+		}
+	} else {
+		l, err = newCommit.CommitsBeforeUntil(oldCommitId)
+		if err != nil {
+			qlog.Fatalf("Find CommitsBeforeUntil erro: %v", err)
+			return
+		}
+	}
+
+	if err != nil {
+		qlog.Fatalf("runUpdate.Commit repoId: %v", err)
+	}
+
+	repos, err := GetRepositoryByName(userId, repoName)
+	if err != nil {
+		qlog.Fatalf("runUpdate.GetRepositoryByName userId: %v", err)
+	}
+
+	commits := make([]*base.PushCommit, 0)
+	var maxCommits = 3
+	var actEmail string
+	for e := l.Front(); e != nil; e = e.Next() {
+		commit := e.Value.(*git.Commit)
+		if actEmail == "" {
+			actEmail = commit.Committer.Email
+		}
+		commits = append(commits,
+			&base.PushCommit{commit.Id.String(),
+				commit.Message(),
+				commit.Author.Email,
+				commit.Author.Name})
+		if len(commits) >= maxCommits {
+			break
+		}
+	}
+
+	//commits = append(commits, []string{lastCommit.Id().String(), lastCommit.Message()})
+	if err = CommitRepoAction(userId, userName, actEmail,
+		repos.Id, repoName, refName, &base.PushCommits{l.Len(), commits}); err != nil {
+		qlog.Fatalf("runUpdate.models.CommitRepoAction: %v", err)
+	}
+}

+ 75 - 20
models/user.go

@@ -5,6 +5,7 @@
 package models
 
 import (
+	"crypto/sha256"
 	"encoding/hex"
 	"errors"
 	"fmt"
@@ -13,8 +14,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/dchest/scrypt"
-
 	"github.com/gogits/git"
 
 	"github.com/gogits/gogs/modules/base"
@@ -62,6 +61,7 @@ type User struct {
 	IsActive      bool
 	IsAdmin       bool
 	Rands         string    `xorm:"VARCHAR(10)"`
+	Salt          string    `xorm:"VARCHAR(10)"`
 	Created       time.Time `xorm:"created"`
 	Updated       time.Time `xorm:"updated"`
 }
@@ -76,7 +76,7 @@ func (user *User) AvatarLink() string {
 	if base.Service.EnableCacheAvatar {
 		return "/avatar/" + user.Avatar
 	}
-	return "http://1.gravatar.com/avatar/" + user.Avatar
+	return "//1.gravatar.com/avatar/" + user.Avatar
 }
 
 // NewGitSig generates and returns the signature of given user.
@@ -89,10 +89,9 @@ func (user *User) NewGitSig() *git.Signature {
 }
 
 // EncodePasswd encodes password to safe format.
-func (user *User) EncodePasswd() error {
-	newPasswd, err := scrypt.Key([]byte(user.Passwd), []byte(base.SecretKey), 16384, 8, 1, 64)
+func (user *User) EncodePasswd() {
+	newPasswd := base.PBKDF2([]byte(user.Passwd), []byte(user.Salt), 10000, 50, sha256.New)
 	user.Passwd = fmt.Sprintf("%x", newPasswd)
-	return err
 }
 
 // Member represents user is member of organization.
@@ -148,9 +147,9 @@ func RegisterUser(user *User) (*User, error) {
 	user.Avatar = base.EncodeMd5(user.Email)
 	user.AvatarEmail = user.Email
 	user.Rands = GetUserSalt()
-	if err = user.EncodePasswd(); err != nil {
-		return nil, err
-	} else if _, err = orm.Insert(user); err != nil {
+	user.Salt = GetUserSalt()
+	user.EncodePasswd()
+	if _, err = orm.Insert(user); err != nil {
 		return nil, err
 	} else if err = os.MkdirAll(UserPath(user.Name), os.ModePerm); err != nil {
 		if _, err := orm.Id(user.Id).Delete(&User{}); err != nil {
@@ -218,17 +217,24 @@ func ChangeUserName(user *User, newUserName string) (err error) {
 	if err = orm.Find(&accesses, &Access{UserName: user.LowerName}); err != nil {
 		return err
 	}
+
+	sess := orm.NewSession()
+	defer sess.Close()
+	if err = sess.Begin(); err != nil {
+		return err
+	}
+
 	for i := range accesses {
 		accesses[i].UserName = newUserName
 		if strings.HasPrefix(accesses[i].RepoName, user.LowerName+"/") {
 			accesses[i].RepoName = strings.Replace(accesses[i].RepoName, user.LowerName, newUserName, 1)
-			if err = UpdateAccess(&accesses[i]); err != nil {
+			if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil {
 				return err
 			}
 		}
 	}
 
-	repos, err := GetRepositories(user)
+	repos, err := GetRepositories(user, true)
 	if err != nil {
 		return err
 	}
@@ -241,14 +247,19 @@ func ChangeUserName(user *User, newUserName string) (err error) {
 
 		for j := range accesses {
 			accesses[j].RepoName = newUserName + "/" + repos[i].LowerName
-			if err = UpdateAccess(&accesses[j]); err != nil {
+			if err = UpdateAccessWithSession(sess, &accesses[j]); err != nil {
 				return err
 			}
 		}
 	}
 
 	// Change user directory name.
-	return os.Rename(UserPath(user.LowerName), UserPath(newUserName))
+	if err = os.Rename(UserPath(user.LowerName), UserPath(newUserName)); err != nil {
+		sess.Rollback()
+		return err
+	}
+
+	return sess.Commit()
 }
 
 // UpdateUser updates user's information.
@@ -278,11 +289,26 @@ func DeleteUser(user *User) error {
 
 	// TODO: check issues, other repos' commits
 
+	// Delete all followers.
+	if _, err = orm.Delete(&Follow{FollowId: user.Id}); err != nil {
+		return err
+	}
+
+	// Delete oauth2.
+	if _, err = orm.Delete(&Oauth2{Uid: user.Id}); err != nil {
+		return err
+	}
+
 	// Delete all feeds.
 	if _, err = orm.Delete(&Action{UserId: user.Id}); err != nil {
 		return err
 	}
 
+	// Delete all watches.
+	if _, err = orm.Delete(&Watch{UserId: user.Id}); err != nil {
+		return err
+	}
+
 	// Delete all accesses.
 	if _, err = orm.Delete(&Access{UserName: user.LowerName}); err != nil {
 		return err
@@ -305,7 +331,6 @@ func DeleteUser(user *User) error {
 	}
 
 	_, err = orm.Delete(user)
-	// TODO: delete and update follower information.
 	return err
 }
 
@@ -355,20 +380,50 @@ func GetUserByName(name string) (*User, error) {
 	return user, nil
 }
 
-// LoginUserPlain validates user by raw user name and password.
-func LoginUserPlain(name, passwd string) (*User, error) {
-	user := User{LowerName: strings.ToLower(name), Passwd: passwd}
-	if err := user.EncodePasswd(); err != nil {
+// GetUserEmailsByNames returns a slice of e-mails corresponds to names.
+func GetUserEmailsByNames(names []string) []string {
+	mails := make([]string, 0, len(names))
+	for _, name := range names {
+		u, err := GetUserByName(name)
+		if err != nil {
+			continue
+		}
+		mails = append(mails, u.Email)
+	}
+	return mails
+}
+
+// GetUserByEmail returns the user object by given e-mail if exists.
+func GetUserByEmail(email string) (*User, error) {
+	if len(email) == 0 {
+		return nil, ErrUserNotExist
+	}
+	user := &User{Email: strings.ToLower(email)}
+	has, err := orm.Get(user)
+	if err != nil {
 		return nil, err
+	} else if !has {
+		return nil, ErrUserNotExist
 	}
+	return user, nil
+}
 
+// LoginUserPlain validates user by raw user name and password.
+func LoginUserPlain(name, passwd string) (*User, error) {
+	user := User{LowerName: strings.ToLower(name)}
 	has, err := orm.Get(&user)
 	if err != nil {
 		return nil, err
 	} else if !has {
-		err = ErrUserNotExist
+		return nil, ErrUserNotExist
+	}
+
+	newUser := &User{Passwd: passwd, Salt: user.Salt}
+	newUser.EncodePasswd()
+	if user.Passwd != newUser.Passwd {
+		return nil, ErrUserNotExist
 	}
-	return &user, err
+	return &user, nil
 }
 
 // Follow is connection request for receiving user notifycation.

+ 1 - 3
modules/auth/admin.go

@@ -10,8 +10,6 @@ import (
 
 	"github.com/go-martini/martini"
 
-	"github.com/gogits/binding"
-
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
 )
@@ -35,7 +33,7 @@ func (f *AdminEditUserForm) Name(field string) string {
 	return names[field]
 }
 
-func (f *AdminEditUserForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) {
+func (f *AdminEditUserForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) {
 	if req.Method == "GET" || errors.Count() == 0 {
 		return
 	}

+ 12 - 12
modules/auth/auth.go

@@ -11,8 +11,6 @@ import (
 
 	"github.com/go-martini/martini"
 
-	"github.com/gogits/binding"
-
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
 )
@@ -39,7 +37,7 @@ func (f *RegisterForm) Name(field string) string {
 	return names[field]
 }
 
-func (f *RegisterForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) {
+func (f *RegisterForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) {
 	if req.Method == "GET" || errors.Count() == 0 {
 		return
 	}
@@ -72,7 +70,7 @@ func (f *LogInForm) Name(field string) string {
 	return names[field]
 }
 
-func (f *LogInForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) {
+func (f *LogInForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) {
 	if req.Method == "GET" || errors.Count() == 0 {
 		return
 	}
@@ -100,7 +98,7 @@ func getMinMaxSize(field reflect.StructField) string {
 	return ""
 }
 
-func validate(errors *binding.Errors, data base.TmplData, form Form) {
+func validate(errors *base.BindingErrors, data base.TmplData, form Form) {
 	typ := reflect.TypeOf(form)
 	val := reflect.ValueOf(form)
 
@@ -121,16 +119,18 @@ func validate(errors *binding.Errors, data base.TmplData, form Form) {
 		if err, ok := errors.Fields[field.Name]; ok {
 			data["Err_"+field.Name] = true
 			switch err {
-			case binding.RequireError:
+			case base.BindingRequireError:
 				data["ErrorMsg"] = form.Name(field.Name) + " cannot be empty"
-			case binding.AlphaDashError:
+			case base.BindingAlphaDashError:
 				data["ErrorMsg"] = form.Name(field.Name) + " must be valid alpha or numeric or dash(-_) characters"
-			case binding.MinSizeError:
+			case base.BindingMinSizeError:
 				data["ErrorMsg"] = form.Name(field.Name) + " must contain at least " + getMinMaxSize(field) + " characters"
-			case binding.MaxSizeError:
+			case base.BindingMaxSizeError:
 				data["ErrorMsg"] = form.Name(field.Name) + " must contain at most " + getMinMaxSize(field) + " characters"
-			case binding.EmailError:
-				data["ErrorMsg"] = form.Name(field.Name) + " is not valid"
+			case base.BindingEmailError:
+				data["ErrorMsg"] = form.Name(field.Name) + " is not a valid e-mail address"
+			case base.BindingUrlError:
+				data["ErrorMsg"] = form.Name(field.Name) + " is not a valid URL"
 			default:
 				data["ErrorMsg"] = "Unknown error: " + err
 			}
@@ -194,7 +194,7 @@ func (f *InstallForm) Name(field string) string {
 	return names[field]
 }
 
-func (f *InstallForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) {
+func (f *InstallForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) {
 	if req.Method == "GET" || errors.Count() == 0 {
 		return
 	}

+ 1 - 3
modules/auth/issue.go

@@ -10,8 +10,6 @@ import (
 
 	"github.com/go-martini/martini"
 
-	"github.com/gogits/binding"
-
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
 )
@@ -31,7 +29,7 @@ func (f *CreateIssueForm) Name(field string) string {
 	return names[field]
 }
 
-func (f *CreateIssueForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) {
+func (f *CreateIssueForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) {
 	if req.Method == "GET" || errors.Count() == 0 {
 		return
 	}

+ 50 - 0
modules/auth/release.go

@@ -0,0 +1,50 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package auth
+
+import (
+	"net/http"
+	"reflect"
+
+	"github.com/go-martini/martini"
+
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/log"
+)
+
+type NewReleaseForm struct {
+	TagName    string `form:"tag_name" binding:"Required"`
+	Title      string `form:"title" binding:"Required"`
+	Content    string `form:"content" binding:"Required"`
+	Prerelease bool   `form:"prerelease"`
+}
+
+func (f *NewReleaseForm) Name(field string) string {
+	names := map[string]string{
+		"TagName": "Tag name",
+		"Title":   "Release title",
+		"Content": "Release content",
+	}
+	return names[field]
+}
+
+func (f *NewReleaseForm) Validate(errors *base.BindingErrors, 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("NewReleaseForm.Validate: %v", err)
+		}
+		return
+	}
+
+	validate(errors, data, f)
+}

+ 41 - 5
modules/auth/repo.go

@@ -10,19 +10,17 @@ import (
 
 	"github.com/go-martini/martini"
 
-	"github.com/gogits/binding"
-
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
 )
 
 type CreateRepoForm struct {
 	RepoName    string `form:"repo" binding:"Required;AlphaDash"`
-	Visibility  string `form:"visibility"`
+	Private     bool   `form:"private"`
 	Description string `form:"desc" binding:"MaxSize(100)"`
 	Language    string `form:"language"`
 	License     string `form:"license"`
-	InitReadme  string `form:"initReadme"`
+	InitReadme  bool   `form:"initReadme"`
 }
 
 func (f *CreateRepoForm) Name(field string) string {
@@ -33,7 +31,7 @@ func (f *CreateRepoForm) Name(field string) string {
 	return names[field]
 }
 
-func (f *CreateRepoForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) {
+func (f *CreateRepoForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) {
 	if req.Method == "GET" || errors.Count() == 0 {
 		return
 	}
@@ -51,3 +49,41 @@ func (f *CreateRepoForm) Validate(errors *binding.Errors, req *http.Request, con
 
 	validate(errors, data, f)
 }
+
+type MigrateRepoForm struct {
+	Url          string `form:"url" binding:"Url"`
+	AuthUserName string `form:"auth_username"`
+	AuthPasswd   string `form:"auth_password"`
+	RepoName     string `form:"repo" binding:"Required;AlphaDash"`
+	Mirror       bool   `form:"mirror"`
+	Private      bool   `form:"private"`
+	Description  string `form:"desc" binding:"MaxSize(100)"`
+}
+
+func (f *MigrateRepoForm) Name(field string) string {
+	names := map[string]string{
+		"Url":         "Migration URL",
+		"RepoName":    "Repository name",
+		"Description": "Description",
+	}
+	return names[field]
+}
+
+func (f *MigrateRepoForm) Validate(errors *base.BindingErrors, 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("MigrateRepoForm.Validate: %v", err)
+		}
+		return
+	}
+
+	validate(errors, data, f)
+}

+ 1 - 3
modules/auth/setting.go

@@ -11,8 +11,6 @@ import (
 
 	"github.com/go-martini/martini"
 
-	"github.com/gogits/binding"
-
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
 )
@@ -30,7 +28,7 @@ func (f *AddSSHKeyForm) Name(field string) string {
 	return names[field]
 }
 
-func (f *AddSSHKeyForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) {
+func (f *AddSSHKeyForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) {
 	data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData)
 	AssignForm(f, data)
 

+ 2 - 3
modules/auth/user.go

@@ -10,7 +10,6 @@ import (
 
 	"github.com/go-martini/martini"
 
-	"github.com/gogits/binding"
 	"github.com/gogits/session"
 
 	"github.com/gogits/gogs/models"
@@ -93,7 +92,7 @@ func (f *UpdateProfileForm) Name(field string) string {
 	return names[field]
 }
 
-func (f *UpdateProfileForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) {
+func (f *UpdateProfileForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) {
 	if req.Method == "GET" || errors.Count() == 0 {
 		return
 	}
@@ -126,7 +125,7 @@ func (f *UpdatePasswdForm) Name(field string) string {
 	return names[field]
 }
 
-func (f *UpdatePasswdForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) {
+func (f *UpdatePasswdForm) Validate(errors *base.BindingErrors, req *http.Request, context martini.Context) {
 	if req.Method == "GET" || errors.Count() == 0 {
 		return
 	}

+ 3 - 2
modules/avatar/avatar.go

@@ -157,9 +157,9 @@ func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	avatar := New(hash, this.cacheDir)
 	avatar.AlterImage = this.altImage
 	if avatar.Expired() {
-		err := avatar.UpdateTimeout(time.Millisecond * 500)
-		if err != nil {
+		if err := avatar.UpdateTimeout(time.Millisecond * 1000); err != nil {
 			log.Trace("avatar update error: %v", err)
+			return
 		}
 	}
 	if modtime, err := avatar.Modtime(); err == nil {
@@ -250,6 +250,7 @@ func (this *thunderTask) Fetch() {
 var client = &http.Client{}
 
 func (this *thunderTask) fetch() error {
+	log.Debug("avatar.fetch(fetch new avatar): %s", this.Url)
 	req, _ := http.NewRequest("GET", this.Url, nil)
 	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/jpeg,image/png,*/*;q=0.8")
 	req.Header.Set("Accept-Encoding", "deflate,sdch")

+ 48 - 0
modules/base/base.go

@@ -8,3 +8,51 @@ type (
 	// Type TmplData represents data in the templates.
 	TmplData map[string]interface{}
 )
+
+// __________.__            .___.__
+// \______   \__| ____    __| _/|__| ____    ____
+//  |    |  _/  |/    \  / __ | |  |/    \  / ___\
+//  |    |   \  |   |  \/ /_/ | |  |   |  \/ /_/  >
+//  |______  /__|___|  /\____ | |__|___|  /\___  /
+//         \/        \/      \/         \//_____/
+
+// Errors represents the contract of the response body when the
+// binding step fails before getting to the application.
+type BindingErrors struct {
+	Overall map[string]string `json:"overall"`
+	Fields  map[string]string `json:"fields"`
+}
+
+// Total errors is the sum of errors with the request overall
+// and errors on individual fields.
+func (err BindingErrors) Count() int {
+	return len(err.Overall) + len(err.Fields)
+}
+
+func (this *BindingErrors) Combine(other BindingErrors) {
+	for key, val := range other.Fields {
+		if _, exists := this.Fields[key]; !exists {
+			this.Fields[key] = val
+		}
+	}
+	for key, val := range other.Overall {
+		if _, exists := this.Overall[key]; !exists {
+			this.Overall[key] = val
+		}
+	}
+}
+
+const (
+	BindingRequireError         string = "Required"
+	BindingAlphaDashError       string = "AlphaDash"
+	BindingMinSizeError         string = "MinSize"
+	BindingMaxSizeError         string = "MaxSize"
+	BindingEmailError           string = "Email"
+	BindingUrlError             string = "Url"
+	BindingDeserializationError string = "DeserializationError"
+	BindingIntegerTypeError     string = "IntegerTypeError"
+	BindingBooleanTypeError     string = "BooleanTypeError"
+	BindingFloatTypeError       string = "FloatTypeError"
+)
+
+var GoGetMetas = make(map[string]bool)

+ 11 - 0
modules/base/base_memcache.go

@@ -0,0 +1,11 @@
+// +build memcache
+
+package base
+
+import (
+	_ "github.com/gogits/cache/memcache"
+)
+
+func init() {
+	EnableMemcache = true
+}

+ 11 - 0
modules/base/base_redis.go

@@ -0,0 +1,11 @@
+// +build redis
+
+package base
+
+import (
+	_ "github.com/gogits/cache/redis"
+)
+
+func init() {
+	EnableRedis = true
+}

+ 66 - 46
modules/base/conf.go

@@ -14,6 +14,7 @@ import (
 
 	"github.com/Unknwon/com"
 	"github.com/Unknwon/goconfig"
+	qlog "github.com/qiniu/log"
 
 	"github.com/gogits/cache"
 	"github.com/gogits/session"
@@ -21,22 +22,38 @@ import (
 	"github.com/gogits/gogs/modules/log"
 )
 
-// Mailer represents a mail service.
+// Mailer represents mail service.
 type Mailer struct {
 	Name         string
 	Host         string
 	User, Passwd string
 }
 
+type OauthInfo struct {
+	ClientId, ClientSecret string
+	Scopes                 string
+	AuthUrl, TokenUrl      string
+}
+
+// Oauther represents oauth service.
+type Oauther struct {
+	GitHub, Google, Tencent,
+	Twitter, Weibo bool
+	OauthInfos map[string]*OauthInfo
+}
+
 var (
-	AppVer       string
-	AppName      string
-	AppLogo      string
-	AppUrl       string
-	Domain       string
-	SecretKey    string
-	RunUser      string
+	AppVer     string
+	AppName    string
+	AppLogo    string
+	AppUrl     string
+	IsProdMode bool
+	Domain     string
+	SecretKey  string
+	RunUser    string
+
 	RepoRootPath string
+	ScriptType   string
 
 	InstallLock bool
 
@@ -44,8 +61,9 @@ var (
 	CookieUserName     string
 	CookieRememberName string
 
-	Cfg         *goconfig.ConfigFile
-	MailService *Mailer
+	Cfg          *goconfig.ConfigFile
+	MailService  *Mailer
+	OauthService *Oauther
 
 	LogMode   string
 	LogConfig string
@@ -59,11 +77,14 @@ var (
 	SessionManager  *session.Manager
 
 	PictureService string
+
+	EnableRedis    bool
+	EnableMemcache bool
 )
 
 var Service struct {
 	RegisterEmailConfirm   bool
-	DisenableRegisteration bool
+	DisableRegistration    bool
 	RequireSignInView      bool
 	EnableCacheAvatar      bool
 	NotifyMail             bool
@@ -95,7 +116,7 @@ var logLevels = map[string]string{
 func newService() {
 	Service.ActiveCodeLives = Cfg.MustInt("service", "ACTIVE_CODE_LIVE_MINUTES", 180)
 	Service.ResetPwdCodeLives = Cfg.MustInt("service", "RESET_PASSWD_CODE_LIVE_MINUTES", 180)
-	Service.DisenableRegisteration = Cfg.MustBool("service", "DISENABLE_REGISTERATION", false)
+	Service.DisableRegistration = Cfg.MustBool("service", "DISABLE_REGISTRATION", false)
 	Service.RequireSignInView = Cfg.MustBool("service", "REQUIRE_SIGNIN_VIEW", false)
 	Service.EnableCacheAvatar = Cfg.MustBool("service", "ENABLE_CACHE_AVATAR", false)
 }
@@ -105,16 +126,14 @@ func newLogService() {
 	LogMode = Cfg.MustValue("log", "MODE", "console")
 	modeSec := "log." + LogMode
 	if _, err := Cfg.GetSection(modeSec); err != nil {
-		fmt.Printf("Unknown log mode: %s\n", LogMode)
-		os.Exit(2)
+		qlog.Fatalf("Unknown log mode: %s\n", LogMode)
 	}
 
 	// Log level.
 	levelName := Cfg.MustValue("log."+LogMode, "LEVEL", "Trace")
 	level, ok := logLevels[levelName]
 	if !ok {
-		fmt.Printf("Unknown log level: %s\n", levelName)
-		os.Exit(2)
+		qlog.Fatalf("Unknown log level: %s\n", levelName)
 	}
 
 	// Generate log configuration.
@@ -151,12 +170,19 @@ func newLogService() {
 			Cfg.MustValue(modeSec, "CONN"))
 	}
 
+	log.Info("%s %s", AppName, AppVer)
 	log.NewLogger(Cfg.MustInt64("log", "BUFFER_LEN", 10000), LogMode, LogConfig)
 	log.Info("Log Mode: %s(%s)", strings.Title(LogMode), levelName)
 }
 
 func newCacheService() {
 	CacheAdapter = Cfg.MustValue("cache", "ADAPTER", "memory")
+	if EnableRedis {
+		log.Info("Redis Enabled")
+	}
+	if EnableMemcache {
+		log.Info("Memcache Enabled")
+	}
 
 	switch CacheAdapter {
 	case "memory":
@@ -164,16 +190,14 @@ func newCacheService() {
 	case "redis", "memcache":
 		CacheConfig = fmt.Sprintf(`{"conn":"%s"}`, Cfg.MustValue("cache", "HOST"))
 	default:
-		fmt.Printf("Unknown cache adapter: %s\n", CacheAdapter)
-		os.Exit(2)
+		qlog.Fatalf("Unknown cache adapter: %s\n", CacheAdapter)
 	}
 
 	var err error
 	Cache, err = cache.NewCache(CacheAdapter, CacheConfig)
 	if err != nil {
-		fmt.Printf("Init cache system failed, adapter: %s, config: %s, %v\n",
+		qlog.Fatalf("Init cache system failed, adapter: %s, config: %s, %v\n",
 			CacheAdapter, CacheConfig, err)
-		os.Exit(2)
 	}
 
 	log.Info("Cache Service Enabled")
@@ -199,9 +223,8 @@ func newSessionService() {
 	var err error
 	SessionManager, err = session.NewManager(SessionProvider, *SessionConfig)
 	if err != nil {
-		fmt.Printf("Init session system failed, provider: %s, %v\n",
+		qlog.Fatalf("Init session system failed, provider: %s, %v\n",
 			SessionProvider, err)
-		os.Exit(2)
 	}
 
 	log.Info("Session Service Enabled")
@@ -209,15 +232,17 @@ func newSessionService() {
 
 func newMailService() {
 	// Check mailer setting.
-	if Cfg.MustBool("mailer", "ENABLED") {
-		MailService = &Mailer{
-			Name:   Cfg.MustValue("mailer", "NAME", AppName),
-			Host:   Cfg.MustValue("mailer", "HOST"),
-			User:   Cfg.MustValue("mailer", "USER"),
-			Passwd: Cfg.MustValue("mailer", "PASSWD"),
-		}
-		log.Info("Mail Service Enabled")
+	if !Cfg.MustBool("mailer", "ENABLED") {
+		return
+	}
+
+	MailService = &Mailer{
+		Name:   Cfg.MustValue("mailer", "NAME", AppName),
+		Host:   Cfg.MustValue("mailer", "HOST"),
+		User:   Cfg.MustValue("mailer", "USER"),
+		Passwd: Cfg.MustValue("mailer", "PASSWD"),
 	}
+	log.Info("Mail Service Enabled")
 }
 
 func newRegisterMailService() {
@@ -246,23 +271,20 @@ func NewConfigContext() {
 	//var err error
 	workDir, err := ExecDir()
 	if err != nil {
-		fmt.Printf("Fail to get work directory: %s\n", err)
-		os.Exit(2)
+		qlog.Fatalf("Fail to get work directory: %s\n", err)
 	}
 
 	cfgPath := filepath.Join(workDir, "conf/app.ini")
 	Cfg, err = goconfig.LoadConfigFile(cfgPath)
 	if err != nil {
-		fmt.Printf("Cannot load config file(%s): %v\n", cfgPath, err)
-		os.Exit(2)
+		qlog.Fatalf("Cannot load config file(%s): %v\n", cfgPath, err)
 	}
 	Cfg.BlockMode = false
 
 	cfgPath = filepath.Join(workDir, "custom/conf/app.ini")
 	if com.IsFile(cfgPath) {
 		if err = Cfg.AppendFiles(cfgPath); err != nil {
-			fmt.Printf("Cannot load config file(%s): %v\n", cfgPath, err)
-			os.Exit(2)
+			qlog.Fatalf("Cannot load config file(%s): %v\n", cfgPath, err)
 		}
 	}
 
@@ -275,14 +297,13 @@ func NewConfigContext() {
 	InstallLock = Cfg.MustBool("security", "INSTALL_LOCK", false)
 
 	RunUser = Cfg.MustValue("", "RUN_USER")
-	curUser := os.Getenv("USERNAME")
+	curUser := os.Getenv("USER")
 	if len(curUser) == 0 {
-		curUser = os.Getenv("USER")
+		curUser = os.Getenv("USERNAME")
 	}
 	// Does not check run user when the install lock is off.
 	if InstallLock && RunUser != curUser {
-		fmt.Printf("Expect user(%s) but current user is: %s\n", RunUser, curUser)
-		os.Exit(2)
+		qlog.Fatalf("Expect user(%s) but current user is: %s\n", RunUser, curUser)
 	}
 
 	LogInRememberDays = Cfg.MustInt("security", "LOGIN_REMEMBER_DAYS")
@@ -294,17 +315,16 @@ func NewConfigContext() {
 	// Determine and create root git reposiroty path.
 	homeDir, err := com.HomeDir()
 	if err != nil {
-		fmt.Printf("Fail to get home directory): %v\n", err)
-		os.Exit(2)
+		qlog.Fatalf("Fail to get home directory): %v\n", err)
 	}
-	RepoRootPath = Cfg.MustValue("repository", "ROOT", filepath.Join(homeDir, "git/gogs-repositories"))
+	RepoRootPath = Cfg.MustValue("repository", "ROOT", filepath.Join(homeDir, "gogs-repositories"))
 	if err = os.MkdirAll(RepoRootPath, os.ModePerm); err != nil {
-		fmt.Printf("Fail to create RepoRootPath(%s): %v\n", RepoRootPath, err)
-		os.Exit(2)
+		qlog.Fatalf("Fail to create RepoRootPath(%s): %v\n", RepoRootPath, err)
 	}
+	ScriptType = Cfg.MustValue("repository", "SCRIPT_TYPE", "bash")
 }
 
-func NewServices() {
+func NewBaseServices() {
 	newService()
 	newLogService()
 	newCacheService()

+ 54 - 3
modules/base/markdown.go

@@ -6,9 +6,11 @@ package base
 
 import (
 	"bytes"
+	"fmt"
 	"net/http"
 	"path"
 	"path/filepath"
+	"regexp"
 	"strings"
 
 	"github.com/gogits/gfm"
@@ -87,13 +89,58 @@ func (options *CustomRender) Link(out *bytes.Buffer, link []byte, title []byte,
 	options.Renderer.Link(out, link, title, content)
 }
 
+var (
+	MentionPattern    = regexp.MustCompile(`@[0-9a-zA-Z_]{1,}`)
+	commitPattern     = regexp.MustCompile(`(\s|^)https?.*commit/[0-9a-zA-Z]+(#+[0-9a-zA-Z-]*)?`)
+	issueFullPattern  = regexp.MustCompile(`(\s|^)https?.*issues/[0-9]+(#+[0-9a-zA-Z-]*)?`)
+	issueIndexPattern = regexp.MustCompile(`#[0-9]+`)
+)
+
+func RenderSpecialLink(rawBytes []byte, urlPrefix string) []byte {
+	ms := MentionPattern.FindAll(rawBytes, -1)
+	for _, m := range ms {
+		rawBytes = bytes.Replace(rawBytes, m,
+			[]byte(fmt.Sprintf(`<a href="/user/%s">%s</a>`, m[1:], m)), -1)
+	}
+	ms = commitPattern.FindAll(rawBytes, -1)
+	for _, m := range ms {
+		m = bytes.TrimSpace(m)
+		i := strings.Index(string(m), "commit/")
+		j := strings.Index(string(m), "#")
+		if j == -1 {
+			j = len(m)
+		}
+		rawBytes = bytes.Replace(rawBytes, m, []byte(fmt.Sprintf(
+			` <code><a href="%s">%s</a></code>`, m, ShortSha(string(m[i+7:j])))), -1)
+	}
+	ms = issueFullPattern.FindAll(rawBytes, -1)
+	for _, m := range ms {
+		m = bytes.TrimSpace(m)
+		i := strings.Index(string(m), "issues/")
+		j := strings.Index(string(m), "#")
+		if j == -1 {
+			j = len(m)
+		}
+		rawBytes = bytes.Replace(rawBytes, m, []byte(fmt.Sprintf(
+			` <a href="%s">#%s</a>`, m, ShortSha(string(m[i+7:j])))), -1)
+	}
+	ms = issueIndexPattern.FindAll(rawBytes, -1)
+	for _, m := range ms {
+		rawBytes = bytes.Replace(rawBytes, m, []byte(fmt.Sprintf(
+			`<a href="%s/issues/%s">%s</a>`, urlPrefix, m[1:], m)), -1)
+	}
+	return rawBytes
+}
+
 func RenderMarkdown(rawBytes []byte, urlPrefix string) []byte {
+	body := RenderSpecialLink(rawBytes, urlPrefix)
+	// fmt.Println(string(body))
 	htmlFlags := 0
 	// htmlFlags |= gfm.HTML_USE_XHTML
 	// htmlFlags |= gfm.HTML_USE_SMARTYPANTS
 	// htmlFlags |= gfm.HTML_SMARTYPANTS_FRACTIONS
 	// htmlFlags |= gfm.HTML_SMARTYPANTS_LATEX_DASHES
-	htmlFlags |= gfm.HTML_SKIP_HTML
+	// htmlFlags |= gfm.HTML_SKIP_HTML
 	htmlFlags |= gfm.HTML_SKIP_STYLE
 	htmlFlags |= gfm.HTML_SKIP_SCRIPT
 	htmlFlags |= gfm.HTML_GITHUB_BLOCKCODE
@@ -115,7 +162,11 @@ func RenderMarkdown(rawBytes []byte, urlPrefix string) []byte {
 	extensions |= gfm.EXTENSION_SPACE_HEADERS
 	extensions |= gfm.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK
 
-	body := gfm.Markdown(rawBytes, renderer, extensions)
-
+	body = gfm.Markdown(body, renderer, extensions)
+	// fmt.Println(string(body))
 	return body
 }
+
+func RenderMarkdownString(raw, urlPrefix string) string {
+	return string(RenderMarkdown([]byte(raw), urlPrefix))
+}

+ 136 - 0
modules/base/template.go

@@ -5,7 +5,9 @@
 package base
 
 import (
+	"bytes"
 	"container/list"
+	"encoding/json"
 	"fmt"
 	"html/template"
 	"strings"
@@ -54,6 +56,9 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{
 	"AppDomain": func() string {
 		return Domain
 	},
+	"IsProdMode": func() bool {
+		return IsProdMode
+	},
 	"LoadTimes": func(startTime time.Time) string {
 		return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
 	},
@@ -62,11 +67,18 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{
 	"TimeSince":  TimeSince,
 	"FileSize":   FileSize,
 	"Subtract":   Subtract,
+	"Add": func(a, b int) int {
+		return a + b
+	},
 	"ActionIcon": ActionIcon,
 	"ActionDesc": ActionDesc,
 	"DateFormat": DateFormat,
 	"List":       List,
 	"Mail2Domain": func(mail string) string {
+		if !strings.Contains(mail, "@") {
+			return "try.gogits.org"
+		}
+
 		suffix := strings.SplitN(mail, "@", 2)[1]
 		domain, ok := mailDomains[suffix]
 		if !ok {
@@ -80,4 +92,128 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{
 	"DiffTypeToStr":     DiffTypeToStr,
 	"DiffLineTypeToStr": DiffLineTypeToStr,
 	"ShortSha":          ShortSha,
+	"Oauth2Icon":        Oauth2Icon,
+}
+
+type Actioner interface {
+	GetOpType() int
+	GetActUserName() string
+	GetActEmail() string
+	GetRepoName() string
+	GetBranch() string
+	GetContent() string
+}
+
+// ActionIcon accepts a int that represents action operation type
+// and returns a icon class name.
+func ActionIcon(opType int) string {
+	switch opType {
+	case 1: // Create repository.
+		return "plus-circle"
+	case 5, 9: // Commit repository.
+		return "arrow-circle-o-right"
+	case 6: // Create issue.
+		return "exclamation-circle"
+	case 8: // Transfer repository.
+		return "share"
+	default:
+		return "invalid type"
+	}
+}
+
+const (
+	TPL_CREATE_REPO    = `<a href="/user/%s">%s</a> created repository <a href="/%s">%s</a>`
+	TPL_COMMIT_REPO    = `<a href="/user/%s">%s</a> pushed to <a href="/%s/src/%s">%s</a> at <a href="/%s">%s</a>%s`
+	TPL_COMMIT_REPO_LI = `<div><img src="%s?s=16" alt="user-avatar"/> <a href="/%s/commit/%s">%s</a> %s</div>`
+	TPL_CREATE_ISSUE   = `<a href="/user/%s">%s</a> opened issue <a href="/%s/issues/%s">%s#%s</a>
+<div><img src="%s?s=16" alt="user-avatar"/> %s</div>`
+	TPL_TRANSFER_REPO = `<a href="/user/%s">%s</a> transfered repository <code>%s</code> to <a href="/%s">%s</a>`
+	TPL_PUSH_TAG      = `<a href="/user/%s">%s</a> pushed tag <a href="/%s/src/%s">%s</a> at <a href="/%s">%s</a>`
+)
+
+type PushCommit struct {
+	Sha1        string
+	Message     string
+	AuthorEmail string
+	AuthorName  string
+}
+
+type PushCommits struct {
+	Len     int
+	Commits []*PushCommit
+}
+
+// ActionDesc accepts int that represents action operation type
+// and returns the description.
+func ActionDesc(act Actioner) string {
+	actUserName := act.GetActUserName()
+	email := act.GetActEmail()
+	repoName := act.GetRepoName()
+	repoLink := actUserName + "/" + repoName
+	branch := act.GetBranch()
+	content := act.GetContent()
+	switch act.GetOpType() {
+	case 1: // Create repository.
+		return fmt.Sprintf(TPL_CREATE_REPO, actUserName, actUserName, repoLink, repoName)
+	case 5: // Commit repository.
+		var push *PushCommits
+		if err := json.Unmarshal([]byte(content), &push); err != nil {
+			return err.Error()
+		}
+		buf := bytes.NewBuffer([]byte("\n"))
+		for _, commit := range push.Commits {
+			buf.WriteString(fmt.Sprintf(TPL_COMMIT_REPO_LI, AvatarLink(commit.AuthorEmail), repoLink, commit.Sha1, commit.Sha1[:7], commit.Message) + "\n")
+		}
+		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, repoLink, branch, branch, repoLink, repoLink,
+			buf.String())
+	case 6: // Create issue.
+		infos := strings.SplitN(content, "|", 2)
+		return fmt.Sprintf(TPL_CREATE_ISSUE, actUserName, actUserName, repoLink, infos[0], repoLink, infos[0],
+			AvatarLink(email), infos[1])
+	case 8: // Transfer repository.
+		newRepoLink := content + "/" + repoName
+		return fmt.Sprintf(TPL_TRANSFER_REPO, actUserName, actUserName, repoLink, newRepoLink, newRepoLink)
+	case 9: // Push tag.
+		return fmt.Sprintf(TPL_PUSH_TAG, actUserName, actUserName, repoLink, branch, branch, repoLink, repoLink)
+	default:
+		return "invalid type"
+	}
+}
+
+func DiffTypeToStr(diffType int) string {
+	diffTypes := map[int]string{
+		1: "add", 2: "modify", 3: "del",
+	}
+	return diffTypes[diffType]
+}
+
+func DiffLineTypeToStr(diffType int) string {
+	switch diffType {
+	case 2:
+		return "add"
+	case 3:
+		return "del"
+	case 4:
+		return "tag"
+	}
+	return "same"
+}
+
+func Oauth2Icon(t int) string {
+	switch t {
+	case 1:
+		return "fa-github-square"
+	case 2:
+		return "fa-google-plus-square"
+	case 3:
+		return "fa-twitter-square"
+	case 4:
+		return "fa-linux"
+	case 5:
+		return "fa-weibo"
+	}
+	return ""
 }

+ 41 - 108
modules/base/tool.go

@@ -5,13 +5,13 @@
 package base
 
 import (
-	"bytes"
+	"crypto/hmac"
 	"crypto/md5"
 	"crypto/rand"
 	"crypto/sha1"
 	"encoding/hex"
-	"encoding/json"
 	"fmt"
+	"hash"
 	"math"
 	"strconv"
 	"strings"
@@ -40,6 +40,44 @@ func GetRandomString(n int, alphabets ...byte) string {
 	return string(bytes)
 }
 
+// http://code.google.com/p/go/source/browse/pbkdf2/pbkdf2.go?repo=crypto
+func PBKDF2(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
+	prf := hmac.New(h, password)
+	hashLen := prf.Size()
+	numBlocks := (keyLen + hashLen - 1) / hashLen
+
+	var buf [4]byte
+	dk := make([]byte, 0, numBlocks*hashLen)
+	U := make([]byte, hashLen)
+	for block := 1; block <= numBlocks; block++ {
+		// N.B.: || means concatenation, ^ means XOR
+		// for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter
+		// U_1 = PRF(password, salt || uint(i))
+		prf.Reset()
+		prf.Write(salt)
+		buf[0] = byte(block >> 24)
+		buf[1] = byte(block >> 16)
+		buf[2] = byte(block >> 8)
+		buf[3] = byte(block)
+		prf.Write(buf[:4])
+		dk = prf.Sum(dk)
+		T := dk[len(dk)-hashLen:]
+		copy(U, T)
+
+		// U_n = PRF(password, U_(n-1))
+		for n := 2; n <= iter; n++ {
+			prf.Reset()
+			prf.Write(U)
+			U = U[:0]
+			U = prf.Sum(U)
+			for x := range U {
+				T[x] ^= U[x]
+			}
+		}
+	}
+	return dk[:keyLen]
+}
+
 // verify time limit code
 func VerifyTimeLimitCode(data string, minutes int, code string) bool {
 	if len(code) <= 18 {
@@ -105,7 +143,7 @@ func AvatarLink(email string) string {
 	if Service.EnableCacheAvatar {
 		return "/avatar/" + EncodeMd5(email)
 	}
-	return "http://1.gravatar.com/avatar/" + EncodeMd5(email)
+	return "//1.gravatar.com/avatar/" + EncodeMd5(email)
 }
 
 // Seconds-based time units
@@ -246,7 +284,6 @@ func TimeSince(then time.Time) string {
 	default:
 		return fmt.Sprintf("%d years %s", diff/Year, lbl)
 	}
-	return then.String()
 }
 
 const (
@@ -474,107 +511,3 @@ func (a argInt) Get(i int, args ...int) (r int) {
 	}
 	return
 }
-
-type Actioner interface {
-	GetOpType() int
-	GetActUserName() string
-	GetActEmail() string
-	GetRepoName() string
-	GetBranch() string
-	GetContent() string
-}
-
-// ActionIcon accepts a int that represents action operation type
-// and returns a icon class name.
-func ActionIcon(opType int) string {
-	switch opType {
-	case 1: // Create repository.
-		return "plus-circle"
-	case 5: // Commit repository.
-		return "arrow-circle-o-right"
-	case 6: // Create issue.
-		return "exclamation-circle"
-	case 8: // Transfer repository.
-		return "share"
-	default:
-		return "invalid type"
-	}
-}
-
-const (
-	TPL_CREATE_REPO    = `<a href="/user/%s">%s</a> created repository <a href="/%s">%s</a>`
-	TPL_COMMIT_REPO    = `<a href="/user/%s">%s</a> pushed to <a href="/%s/src/%s">%s</a> at <a href="/%s">%s</a>%s`
-	TPL_COMMIT_REPO_LI = `<div><img src="%s?s=16" alt="user-avatar"/> <a href="/%s/commit/%s">%s</a> %s</div>`
-	TPL_CREATE_ISSUE   = `<a href="/user/%s">%s</a> opened issue <a href="/%s/issues/%s">%s#%s</a>
-<div><img src="%s?s=16" alt="user-avatar"/> %s</div>`
-	TPL_TRANSFER_REPO = `<a href="/user/%s">%s</a> transfered repository <code>%s</code> to <a href="/%s">%s</a>`
-)
-
-type PushCommit struct {
-	Sha1        string
-	Message     string
-	AuthorEmail string
-	AuthorName  string
-}
-
-type PushCommits struct {
-	Len     int
-	Commits []*PushCommit
-}
-
-// ActionDesc accepts int that represents action operation type
-// and returns the description.
-func ActionDesc(act Actioner) string {
-	actUserName := act.GetActUserName()
-	email := act.GetActEmail()
-	repoName := act.GetRepoName()
-	repoLink := actUserName + "/" + repoName
-	branch := act.GetBranch()
-	content := act.GetContent()
-	switch act.GetOpType() {
-	case 1: // Create repository.
-		return fmt.Sprintf(TPL_CREATE_REPO, actUserName, actUserName, repoLink, repoName)
-	case 5: // Commit repository.
-		var push *PushCommits
-		if err := json.Unmarshal([]byte(content), &push); err != nil {
-			return err.Error()
-		}
-		buf := bytes.NewBuffer([]byte("\n"))
-		for _, commit := range push.Commits {
-			buf.WriteString(fmt.Sprintf(TPL_COMMIT_REPO_LI, AvatarLink(commit.AuthorEmail), repoLink, commit.Sha1, commit.Sha1[:7], commit.Message) + "\n")
-		}
-		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, repoLink, branch, branch, repoLink, repoLink,
-			buf.String())
-	case 6: // Create issue.
-		infos := strings.SplitN(content, "|", 2)
-		return fmt.Sprintf(TPL_CREATE_ISSUE, actUserName, actUserName, repoLink, infos[0], repoLink, infos[0],
-			AvatarLink(email), infos[1])
-	case 8: // Transfer repository.
-		newRepoLink := content + "/" + repoName
-		return fmt.Sprintf(TPL_TRANSFER_REPO, actUserName, actUserName, repoLink, newRepoLink, newRepoLink)
-	default:
-		return "invalid type"
-	}
-}
-
-func DiffTypeToStr(diffType int) string {
-	diffTypes := map[int]string{
-		1: "add", 2: "modify", 3: "del",
-	}
-	return diffTypes[diffType]
-}
-
-func DiffLineTypeToStr(diffType int) string {
-	switch diffType {
-	case 2:
-		return "add"
-	case 3:
-		return "del"
-	case 4:
-		return "tag"
-	}
-	return "same"
-}

+ 17 - 0
modules/cron/cron.go

@@ -0,0 +1,17 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package cron
+
+import (
+	"github.com/robfig/cron"
+
+	"github.com/gogits/gogs/models"
+)
+
+func NewCronContext() {
+	c := cron.New()
+	c.AddFunc("@every 1h", models.MirrorUpdate)
+	c.Start()
+}

+ 1 - 2
modules/log/log.go

@@ -21,8 +21,7 @@ func init() {
 func NewLogger(bufLen int64, mode, config string) {
 	Mode, Config = mode, config
 	logger = logs.NewLogger(bufLen)
-	logger.EnableFuncCallDepth(true)
-	logger.SetLogFuncCallDepth(4)
+	logger.SetLogFuncCallDepth(3)
 	logger.SetLogger(mode, config)
 }
 

+ 45 - 8
modules/mailer/mail.go

@@ -86,16 +86,36 @@ func SendActiveMail(r *middleware.Render, user *models.User) {
 	}
 
 	msg := NewMailMessage([]string{user.Email}, subject, body)
-	msg.Info = fmt.Sprintf("UID: %d, send email verify mail", user.Id)
+	msg.Info = fmt.Sprintf("UID: %d, send active mail", user.Id)
 
 	SendAsync(&msg)
 }
 
-// SendNotifyMail sends mail notification of all watchers.
-func SendNotifyMail(user, owner *models.User, repo *models.Repository, issue *models.Issue) error {
+// Send reset password email.
+func SendResetPasswdMail(r *middleware.Render, user *models.User) {
+	code := CreateUserActiveCode(user, nil)
+
+	subject := "Reset your password"
+
+	data := GetMailTmplData(user)
+	data["Code"] = code
+	body, err := r.HTMLString("mail/auth/reset_passwd", data)
+	if err != nil {
+		log.Error("mail.SendResetPasswdMail(fail to render): %v", err)
+		return
+	}
+
+	msg := NewMailMessage([]string{user.Email}, subject, body)
+	msg.Info = fmt.Sprintf("UID: %d, send reset password email", user.Id)
+
+	SendAsync(&msg)
+}
+
+// SendIssueNotifyMail sends mail notification of all watchers of repository.
+func SendIssueNotifyMail(user, owner *models.User, repo *models.Repository, issue *models.Issue) ([]string, error) {
 	watches, err := models.GetWatches(repo.Id)
 	if err != nil {
-		return errors.New("mail.NotifyWatchers(get watches): " + err.Error())
+		return nil, errors.New("mail.NotifyWatchers(get watches): " + err.Error())
 	}
 
 	tos := make([]string, 0, len(watches))
@@ -106,20 +126,37 @@ func SendNotifyMail(user, owner *models.User, repo *models.Repository, issue *mo
 		}
 		u, err := models.GetUserById(uid)
 		if err != nil {
-			return errors.New("mail.NotifyWatchers(get user): " + err.Error())
+			return nil, errors.New("mail.NotifyWatchers(get user): " + err.Error())
 		}
 		tos = append(tos, u.Email)
 	}
 
 	if len(tos) == 0 {
-		return nil
+		return tos, nil
 	}
 
 	subject := fmt.Sprintf("[%s] %s", repo.Name, issue.Name)
 	content := fmt.Sprintf("%s<br>-<br> <a href=\"%s%s/%s/issues/%d\">View it on Gogs</a>.",
-		issue.Content, base.AppUrl, owner.Name, repo.Name, issue.Index)
+		base.RenderSpecialLink([]byte(issue.Content), owner.Name+"/"+repo.Name),
+		base.AppUrl, owner.Name, repo.Name, issue.Index)
+	msg := NewMailMessageFrom(tos, user.Name, subject, content)
+	msg.Info = fmt.Sprintf("Subject: %s, send issue notify emails", subject)
+	SendAsync(&msg)
+	return tos, nil
+}
+
+// SendIssueMentionMail sends mail notification for who are mentioned in issue.
+func SendIssueMentionMail(user, owner *models.User, repo *models.Repository, issue *models.Issue, tos []string) error {
+	if len(tos) == 0 {
+		return nil
+	}
+
+	issueLink := fmt.Sprintf("%s%s/%s/issues/%d", base.AppUrl, owner.Name, repo.Name, issue.Index)
+	body := fmt.Sprintf(`%s mentioned you.`, user.Name)
+	subject := fmt.Sprintf("[%s] %s", repo.Name, issue.Name)
+	content := fmt.Sprintf("%s<br>-<br> <a href=\"%s\">View it on Gogs</a>.", body, issueLink)
 	msg := NewMailMessageFrom(tos, user.Name, subject, content)
-	msg.Info = fmt.Sprintf("Subject: %s, send notify emails", subject)
+	msg.Info = fmt.Sprintf("Subject: %s, send issue mention emails", subject)
 	SendAsync(&msg)
 	return nil
 }

+ 1 - 1
modules/middleware/auth.go

@@ -47,7 +47,7 @@ func Toggle(options *ToggleOptions) martini.Handler {
 				return
 			} else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm {
 				ctx.Data["Title"] = "Activate Your Account"
-				ctx.HTML(200, "user/active")
+				ctx.HTML(200, "user/activate")
 				return
 			}
 		}

+ 426 - 0
modules/middleware/binding.go

@@ -0,0 +1,426 @@
+// Copyright 2013 The Martini Contrib Authors. All rights reserved.
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package middleware
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"reflect"
+	"regexp"
+	"strconv"
+	"strings"
+	"unicode/utf8"
+
+	"github.com/go-martini/martini"
+
+	"github.com/gogits/gogs/modules/base"
+)
+
+/*
+	To the land of Middle-ware Earth:
+
+		One func to rule them all,
+		One func to find them,
+		One func to bring them all,
+		And in this package BIND them.
+*/
+
+// Bind accepts a copy of an empty struct and populates it with
+// values from the request (if deserialization is successful). It
+// wraps up the functionality of the Form and Json middleware
+// according to the Content-Type of the request, and it guesses
+// if no Content-Type is specified. Bind invokes the ErrorHandler
+// middleware to bail out if errors occurred. If you want to perform
+// your own error handling, use Form or Json middleware directly.
+// An interface pointer can be added as a second argument in order
+// to map the struct to a specific interface.
+func Bind(obj interface{}, ifacePtr ...interface{}) martini.Handler {
+	return func(context martini.Context, req *http.Request) {
+		contentType := req.Header.Get("Content-Type")
+
+		if strings.Contains(contentType, "form-urlencoded") {
+			context.Invoke(Form(obj, ifacePtr...))
+		} else if strings.Contains(contentType, "multipart/form-data") {
+			context.Invoke(MultipartForm(obj, ifacePtr...))
+		} else if strings.Contains(contentType, "json") {
+			context.Invoke(Json(obj, ifacePtr...))
+		} else {
+			context.Invoke(Json(obj, ifacePtr...))
+			if getErrors(context).Count() > 0 {
+				context.Invoke(Form(obj, ifacePtr...))
+			}
+		}
+
+		context.Invoke(ErrorHandler)
+	}
+}
+
+// BindIgnErr will do the exactly same thing as Bind but without any
+// error handling, which user has freedom to deal with them.
+// This allows user take advantages of validation.
+func BindIgnErr(obj interface{}, ifacePtr ...interface{}) martini.Handler {
+	return func(context martini.Context, req *http.Request) {
+		contentType := req.Header.Get("Content-Type")
+
+		if strings.Contains(contentType, "form-urlencoded") {
+			context.Invoke(Form(obj, ifacePtr...))
+		} else if strings.Contains(contentType, "multipart/form-data") {
+			context.Invoke(MultipartForm(obj, ifacePtr...))
+		} else if strings.Contains(contentType, "json") {
+			context.Invoke(Json(obj, ifacePtr...))
+		} else {
+			context.Invoke(Json(obj, ifacePtr...))
+			if getErrors(context).Count() > 0 {
+				context.Invoke(Form(obj, ifacePtr...))
+			}
+		}
+	}
+}
+
+// Form is middleware to deserialize form-urlencoded data from the request.
+// It gets data from the form-urlencoded body, if present, or from the
+// query string. It uses the http.Request.ParseForm() method
+// to perform deserialization, then reflection is used to map each field
+// into the struct with the proper type. Structs with primitive slice types
+// (bool, float, int, string) can support deserialization of repeated form
+// keys, for example: key=val1&key=val2&key=val3
+// An interface pointer can be added as a second argument in order
+// to map the struct to a specific interface.
+func Form(formStruct interface{}, ifacePtr ...interface{}) martini.Handler {
+	return func(context martini.Context, req *http.Request) {
+		ensureNotPointer(formStruct)
+		formStruct := reflect.New(reflect.TypeOf(formStruct))
+		errors := newErrors()
+		parseErr := req.ParseForm()
+
+		// Format validation of the request body or the URL would add considerable overhead,
+		// and ParseForm does not complain when URL encoding is off.
+		// Because an empty request body or url can also mean absence of all needed values,
+		// it is not in all cases a bad request, so let's return 422.
+		if parseErr != nil {
+			errors.Overall[base.BindingDeserializationError] = parseErr.Error()
+		}
+
+		mapForm(formStruct, req.Form, errors)
+
+		validateAndMap(formStruct, context, errors, ifacePtr...)
+	}
+}
+
+func MultipartForm(formStruct interface{}, ifacePtr ...interface{}) martini.Handler {
+	return func(context martini.Context, req *http.Request) {
+		ensureNotPointer(formStruct)
+		formStruct := reflect.New(reflect.TypeOf(formStruct))
+		errors := newErrors()
+
+		// Workaround for multipart forms returning nil instead of an error
+		// when content is not multipart
+		// https://code.google.com/p/go/issues/detail?id=6334
+		multipartReader, err := req.MultipartReader()
+		if err != nil {
+			errors.Overall[base.BindingDeserializationError] = err.Error()
+		} else {
+			form, parseErr := multipartReader.ReadForm(MaxMemory)
+
+			if parseErr != nil {
+				errors.Overall[base.BindingDeserializationError] = parseErr.Error()
+			}
+
+			req.MultipartForm = form
+		}
+
+		mapForm(formStruct, req.MultipartForm.Value, errors)
+
+		validateAndMap(formStruct, context, errors, ifacePtr...)
+	}
+}
+
+// Json is middleware to deserialize a JSON payload from the request
+// into the struct that is passed in. The resulting struct is then
+// validated, but no error handling is actually performed here.
+// An interface pointer can be added as a second argument in order
+// to map the struct to a specific interface.
+func Json(jsonStruct interface{}, ifacePtr ...interface{}) martini.Handler {
+	return func(context martini.Context, req *http.Request) {
+		ensureNotPointer(jsonStruct)
+		jsonStruct := reflect.New(reflect.TypeOf(jsonStruct))
+		errors := newErrors()
+
+		if req.Body != nil {
+			defer req.Body.Close()
+		}
+
+		if err := json.NewDecoder(req.Body).Decode(jsonStruct.Interface()); err != nil && err != io.EOF {
+			errors.Overall[base.BindingDeserializationError] = err.Error()
+		}
+
+		validateAndMap(jsonStruct, context, errors, ifacePtr...)
+	}
+}
+
+// Validate is middleware to enforce required fields. If the struct
+// passed in is a Validator, then the user-defined Validate method
+// is executed, and its errors are mapped to the context. This middleware
+// performs no error handling: it merely detects them and maps them.
+func Validate(obj interface{}) martini.Handler {
+	return func(context martini.Context, req *http.Request) {
+		errors := newErrors()
+		validateStruct(errors, obj)
+
+		if validator, ok := obj.(Validator); ok {
+			validator.Validate(errors, req, context)
+		}
+		context.Map(*errors)
+	}
+}
+
+var (
+	alphaDashPattern = regexp.MustCompile("[^\\d\\w-_]")
+	emailPattern     = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?")
+	urlPattern       = regexp.MustCompile(`(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?`)
+)
+
+func validateStruct(errors *base.BindingErrors, obj interface{}) {
+	typ := reflect.TypeOf(obj)
+	val := reflect.ValueOf(obj)
+
+	if typ.Kind() == reflect.Ptr {
+		typ = typ.Elem()
+		val = val.Elem()
+	}
+
+	for i := 0; i < typ.NumField(); i++ {
+		field := typ.Field(i)
+
+		// Allow ignored fields in the struct
+		if field.Tag.Get("form") == "-" {
+			continue
+		}
+
+		fieldValue := val.Field(i).Interface()
+		if field.Type.Kind() == reflect.Struct {
+			validateStruct(errors, fieldValue)
+			continue
+		}
+
+		zero := reflect.Zero(field.Type).Interface()
+
+		// Match rules.
+		for _, rule := range strings.Split(field.Tag.Get("binding"), ";") {
+			if len(rule) == 0 {
+				continue
+			}
+
+			switch {
+			case rule == "Required":
+				if reflect.DeepEqual(zero, fieldValue) {
+					errors.Fields[field.Name] = base.BindingRequireError
+					break
+				}
+			case rule == "AlphaDash":
+				if alphaDashPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
+					errors.Fields[field.Name] = base.BindingAlphaDashError
+					break
+				}
+			case strings.HasPrefix(rule, "MinSize("):
+				min, err := strconv.Atoi(rule[8 : len(rule)-1])
+				if err != nil {
+					errors.Overall["MinSize"] = err.Error()
+					break
+				}
+				if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) < min {
+					errors.Fields[field.Name] = base.BindingMinSizeError
+					break
+				}
+				v := reflect.ValueOf(fieldValue)
+				if v.Kind() == reflect.Slice && v.Len() < min {
+					errors.Fields[field.Name] = base.BindingMinSizeError
+					break
+				}
+			case strings.HasPrefix(rule, "MaxSize("):
+				max, err := strconv.Atoi(rule[8 : len(rule)-1])
+				if err != nil {
+					errors.Overall["MaxSize"] = err.Error()
+					break
+				}
+				if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) > max {
+					errors.Fields[field.Name] = base.BindingMaxSizeError
+					break
+				}
+				v := reflect.ValueOf(fieldValue)
+				if v.Kind() == reflect.Slice && v.Len() > max {
+					errors.Fields[field.Name] = base.BindingMinSizeError
+					break
+				}
+			case rule == "Email":
+				if !emailPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
+					errors.Fields[field.Name] = base.BindingEmailError
+					break
+				}
+			case rule == "Url":
+				if !urlPattern.MatchString(fmt.Sprintf("%v", fieldValue)) {
+					errors.Fields[field.Name] = base.BindingUrlError
+					break
+				}
+			}
+		}
+	}
+}
+
+func mapForm(formStruct reflect.Value, form map[string][]string, errors *base.BindingErrors) {
+	typ := formStruct.Elem().Type()
+
+	for i := 0; i < typ.NumField(); i++ {
+		typeField := typ.Field(i)
+		if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" {
+			structField := formStruct.Elem().Field(i)
+			if !structField.CanSet() {
+				continue
+			}
+
+			inputValue, exists := form[inputFieldName]
+
+			if !exists {
+				continue
+			}
+
+			numElems := len(inputValue)
+			if structField.Kind() == reflect.Slice && numElems > 0 {
+				sliceOf := structField.Type().Elem().Kind()
+				slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
+				for i := 0; i < numElems; i++ {
+					setWithProperType(sliceOf, inputValue[i], slice.Index(i), inputFieldName, errors)
+				}
+				formStruct.Elem().Field(i).Set(slice)
+			} else {
+				setWithProperType(typeField.Type.Kind(), inputValue[0], structField, inputFieldName, errors)
+			}
+		}
+	}
+}
+
+// ErrorHandler simply counts the number of errors in the
+// context and, if more than 0, writes a 400 Bad Request
+// response and a JSON payload describing the errors with
+// the "Content-Type" set to "application/json".
+// Middleware remaining on the stack will not even see the request
+// if, by this point, there are any errors.
+// This is a "default" handler, of sorts, and you are
+// welcome to use your own instead. The Bind middleware
+// invokes this automatically for convenience.
+func ErrorHandler(errs base.BindingErrors, resp http.ResponseWriter) {
+	if errs.Count() > 0 {
+		resp.Header().Set("Content-Type", "application/json; charset=utf-8")
+		if _, ok := errs.Overall[base.BindingDeserializationError]; ok {
+			resp.WriteHeader(http.StatusBadRequest)
+		} else {
+			resp.WriteHeader(422)
+		}
+		errOutput, _ := json.Marshal(errs)
+		resp.Write(errOutput)
+		return
+	}
+}
+
+// This sets the value in a struct of an indeterminate type to the
+// matching value from the request (via Form middleware) in the
+// same type, so that not all deserialized values have to be strings.
+// Supported types are string, int, float, and bool.
+func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value, nameInTag string, errors *base.BindingErrors) {
+	switch valueKind {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		if val == "" {
+			val = "0"
+		}
+		intVal, err := strconv.ParseInt(val, 10, 64)
+		if err != nil {
+			errors.Fields[nameInTag] = base.BindingIntegerTypeError
+		} else {
+			structField.SetInt(intVal)
+		}
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		if val == "" {
+			val = "0"
+		}
+		uintVal, err := strconv.ParseUint(val, 10, 64)
+		if err != nil {
+			errors.Fields[nameInTag] = base.BindingIntegerTypeError
+		} else {
+			structField.SetUint(uintVal)
+		}
+	case reflect.Bool:
+		structField.SetBool(val == "on")
+	case reflect.Float32:
+		if val == "" {
+			val = "0.0"
+		}
+		floatVal, err := strconv.ParseFloat(val, 32)
+		if err != nil {
+			errors.Fields[nameInTag] = base.BindingFloatTypeError
+		} else {
+			structField.SetFloat(floatVal)
+		}
+	case reflect.Float64:
+		if val == "" {
+			val = "0.0"
+		}
+		floatVal, err := strconv.ParseFloat(val, 64)
+		if err != nil {
+			errors.Fields[nameInTag] = base.BindingFloatTypeError
+		} else {
+			structField.SetFloat(floatVal)
+		}
+	case reflect.String:
+		structField.SetString(val)
+	}
+}
+
+// Don't pass in pointers to bind to. Can lead to bugs. See:
+// https://github.com/codegangsta/martini-contrib/issues/40
+// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
+func ensureNotPointer(obj interface{}) {
+	if reflect.TypeOf(obj).Kind() == reflect.Ptr {
+		panic("Pointers are not accepted as binding models")
+	}
+}
+
+// Performs validation and combines errors from validation
+// with errors from deserialization, then maps both the
+// resulting struct and the errors to the context.
+func validateAndMap(obj reflect.Value, context martini.Context, errors *base.BindingErrors, ifacePtr ...interface{}) {
+	context.Invoke(Validate(obj.Interface()))
+	errors.Combine(getErrors(context))
+	context.Map(*errors)
+	context.Map(obj.Elem().Interface())
+	if len(ifacePtr) > 0 {
+		context.MapTo(obj.Elem().Interface(), ifacePtr[0])
+	}
+}
+
+func newErrors() *base.BindingErrors {
+	return &base.BindingErrors{make(map[string]string), make(map[string]string)}
+}
+
+func getErrors(context martini.Context) base.BindingErrors {
+	return context.Get(reflect.TypeOf(base.BindingErrors{})).Interface().(base.BindingErrors)
+}
+
+type (
+	// Implement the Validator interface to define your own input
+	// validation before the request even gets to your application.
+	// The Validate method will be executed during the validation phase.
+	Validator interface {
+		Validate(*base.BindingErrors, *http.Request, martini.Context)
+	}
+)
+
+var (
+	// Maximum amount of memory to use when parsing a multipart form.
+	// Set this to whatever value you prefer; default is 10 MB.
+	MaxMemory = int64(1024 * 1024 * 10)
+)

+ 701 - 0
modules/middleware/binding_test.go

@@ -0,0 +1,701 @@
+// Copyright 2013 The Martini Contrib Authors. All rights reserved.
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package middleware
+
+import (
+	"bytes"
+	"mime/multipart"
+	"net/http"
+	"net/http/httptest"
+	"strconv"
+	"strings"
+	"testing"
+
+	"github.com/codegangsta/martini"
+)
+
+func TestBind(t *testing.T) {
+	testBind(t, false)
+}
+
+func TestBindWithInterface(t *testing.T) {
+	testBind(t, true)
+}
+
+func TestMultipartBind(t *testing.T) {
+	index := 0
+	for test, expectStatus := range bindMultipartTests {
+		handler := func(post BlogPost, errors Errors) {
+			handle(test, t, index, post, errors)
+		}
+		recorder := testMultipart(t, test, Bind(BlogPost{}), handler, index)
+
+		if recorder.Code != expectStatus {
+			t.Errorf("On test case %v, got status code %d but expected %d", test, recorder.Code, expectStatus)
+		}
+
+		index++
+	}
+}
+
+func TestForm(t *testing.T) {
+	testForm(t, false)
+}
+
+func TestFormWithInterface(t *testing.T) {
+	testForm(t, true)
+}
+
+func TestEmptyForm(t *testing.T) {
+	testEmptyForm(t)
+}
+
+func TestMultipartForm(t *testing.T) {
+	for index, test := range multipartformTests {
+		handler := func(post BlogPost, errors Errors) {
+			handle(test, t, index, post, errors)
+		}
+		testMultipart(t, test, MultipartForm(BlogPost{}), handler, index)
+	}
+}
+
+func TestMultipartFormWithInterface(t *testing.T) {
+	for index, test := range multipartformTests {
+		handler := func(post Modeler, errors Errors) {
+			post.Create(test, t, index)
+		}
+		testMultipart(t, test, MultipartForm(BlogPost{}, (*Modeler)(nil)), handler, index)
+	}
+}
+
+func TestJson(t *testing.T) {
+	testJson(t, false)
+}
+
+func TestJsonWithInterface(t *testing.T) {
+	testJson(t, true)
+}
+
+func TestEmptyJson(t *testing.T) {
+	testEmptyJson(t)
+}
+
+func TestValidate(t *testing.T) {
+	handlerMustErr := func(errors Errors) {
+		if errors.Count() == 0 {
+			t.Error("Expected at least one error, got 0")
+		}
+	}
+	handlerNoErr := func(errors Errors) {
+		if errors.Count() > 0 {
+			t.Error("Expected no errors, got", errors.Count())
+		}
+	}
+
+	performValidationTest(&BlogPost{"", "...", 0, 0, []int{}}, handlerMustErr, t)
+	performValidationTest(&BlogPost{"Good Title", "Good content", 0, 0, []int{}}, handlerNoErr, t)
+
+	performValidationTest(&User{Name: "Jim", Home: Address{"", ""}}, handlerMustErr, t)
+	performValidationTest(&User{Name: "Jim", Home: Address{"required", ""}}, handlerNoErr, t)
+}
+
+func handle(test testCase, t *testing.T, index int, post BlogPost, errors Errors) {
+	assertEqualField(t, "Title", index, test.ref.Title, post.Title)
+	assertEqualField(t, "Content", index, test.ref.Content, post.Content)
+	assertEqualField(t, "Views", index, test.ref.Views, post.Views)
+
+	for i := range test.ref.Multiple {
+		if i >= len(post.Multiple) {
+			t.Errorf("Expected: %v (size %d) to have same size as: %v (size %d)", post.Multiple, len(post.Multiple), test.ref.Multiple, len(test.ref.Multiple))
+			break
+		}
+		if test.ref.Multiple[i] != post.Multiple[i] {
+			t.Errorf("Expected: %v to deep equal: %v", post.Multiple, test.ref.Multiple)
+			break
+		}
+	}
+
+	if test.ok && errors.Count() > 0 {
+		t.Errorf("%+v should be OK (0 errors), but had errors: %+v", test, errors)
+	} else if !test.ok && errors.Count() == 0 {
+		t.Errorf("%+v should have errors, but was OK (0 errors)", test)
+	}
+}
+
+func handleEmpty(test emptyPayloadTestCase, t *testing.T, index int, section BlogSection, errors Errors) {
+	assertEqualField(t, "Title", index, test.ref.Title, section.Title)
+	assertEqualField(t, "Content", index, test.ref.Content, section.Content)
+
+	if test.ok && errors.Count() > 0 {
+		t.Errorf("%+v should be OK (0 errors), but had errors: %+v", test, errors)
+	} else if !test.ok && errors.Count() == 0 {
+		t.Errorf("%+v should have errors, but was OK (0 errors)", test)
+	}
+}
+
+func testBind(t *testing.T, withInterface bool) {
+	index := 0
+	for test, expectStatus := range bindTests {
+		m := martini.Classic()
+		recorder := httptest.NewRecorder()
+		handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) }
+		binding := Bind(BlogPost{})
+
+		if withInterface {
+			handler = func(post BlogPost, errors Errors) {
+				post.Create(test, t, index)
+			}
+			binding = Bind(BlogPost{}, (*Modeler)(nil))
+		}
+
+		switch test.method {
+		case "GET":
+			m.Get(route, binding, handler)
+		case "POST":
+			m.Post(route, binding, handler)
+		}
+
+		req, err := http.NewRequest(test.method, test.path, strings.NewReader(test.payload))
+		req.Header.Add("Content-Type", test.contentType)
+
+		if err != nil {
+			t.Error(err)
+		}
+		m.ServeHTTP(recorder, req)
+
+		if recorder.Code != expectStatus {
+			t.Errorf("On test case %v, got status code %d but expected %d", test, recorder.Code, expectStatus)
+		}
+
+		index++
+	}
+}
+
+func testJson(t *testing.T, withInterface bool) {
+	for index, test := range jsonTests {
+		recorder := httptest.NewRecorder()
+		handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) }
+		binding := Json(BlogPost{})
+
+		if withInterface {
+			handler = func(post BlogPost, errors Errors) {
+				post.Create(test, t, index)
+			}
+			binding = Bind(BlogPost{}, (*Modeler)(nil))
+		}
+
+		m := martini.Classic()
+		switch test.method {
+		case "GET":
+			m.Get(route, binding, handler)
+		case "POST":
+			m.Post(route, binding, handler)
+		case "PUT":
+			m.Put(route, binding, handler)
+		case "DELETE":
+			m.Delete(route, binding, handler)
+		}
+
+		req, err := http.NewRequest(test.method, route, strings.NewReader(test.payload))
+		if err != nil {
+			t.Error(err)
+		}
+		m.ServeHTTP(recorder, req)
+	}
+}
+
+func testEmptyJson(t *testing.T) {
+	for index, test := range emptyPayloadTests {
+		recorder := httptest.NewRecorder()
+		handler := func(section BlogSection, errors Errors) { handleEmpty(test, t, index, section, errors) }
+		binding := Json(BlogSection{})
+
+		m := martini.Classic()
+		switch test.method {
+		case "GET":
+			m.Get(route, binding, handler)
+		case "POST":
+			m.Post(route, binding, handler)
+		case "PUT":
+			m.Put(route, binding, handler)
+		case "DELETE":
+			m.Delete(route, binding, handler)
+		}
+
+		req, err := http.NewRequest(test.method, route, strings.NewReader(test.payload))
+		if err != nil {
+			t.Error(err)
+		}
+		m.ServeHTTP(recorder, req)
+	}
+}
+
+func testForm(t *testing.T, withInterface bool) {
+	for index, test := range formTests {
+		recorder := httptest.NewRecorder()
+		handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) }
+		binding := Form(BlogPost{})
+
+		if withInterface {
+			handler = func(post BlogPost, errors Errors) {
+				post.Create(test, t, index)
+			}
+			binding = Form(BlogPost{}, (*Modeler)(nil))
+		}
+
+		m := martini.Classic()
+		switch test.method {
+		case "GET":
+			m.Get(route, binding, handler)
+		case "POST":
+			m.Post(route, binding, handler)
+		}
+
+		req, err := http.NewRequest(test.method, test.path, nil)
+		if err != nil {
+			t.Error(err)
+		}
+		m.ServeHTTP(recorder, req)
+	}
+}
+
+func testEmptyForm(t *testing.T) {
+	for index, test := range emptyPayloadTests {
+		recorder := httptest.NewRecorder()
+		handler := func(section BlogSection, errors Errors) { handleEmpty(test, t, index, section, errors) }
+		binding := Form(BlogSection{})
+
+		m := martini.Classic()
+		switch test.method {
+		case "GET":
+			m.Get(route, binding, handler)
+		case "POST":
+			m.Post(route, binding, handler)
+		}
+
+		req, err := http.NewRequest(test.method, test.path, nil)
+		if err != nil {
+			t.Error(err)
+		}
+		m.ServeHTTP(recorder, req)
+	}
+}
+
+func testMultipart(t *testing.T, test testCase, middleware martini.Handler, handler martini.Handler, index int) *httptest.ResponseRecorder {
+	recorder := httptest.NewRecorder()
+
+	m := martini.Classic()
+	m.Post(route, middleware, handler)
+
+	body := &bytes.Buffer{}
+	writer := multipart.NewWriter(body)
+	writer.WriteField("title", test.ref.Title)
+	writer.WriteField("content", test.ref.Content)
+	writer.WriteField("views", strconv.Itoa(test.ref.Views))
+	if len(test.ref.Multiple) != 0 {
+		for _, value := range test.ref.Multiple {
+			writer.WriteField("multiple", strconv.Itoa(value))
+		}
+	}
+
+	req, err := http.NewRequest(test.method, test.path, body)
+	req.Header.Add("Content-Type", writer.FormDataContentType())
+
+	if err != nil {
+		t.Error(err)
+	}
+
+	err = writer.Close()
+	if err != nil {
+		t.Error(err)
+	}
+
+	m.ServeHTTP(recorder, req)
+
+	return recorder
+}
+
+func assertEqualField(t *testing.T, fieldname string, testcasenumber int, expected interface{}, got interface{}) {
+	if expected != got {
+		t.Errorf("%s: expected=%s, got=%s in test case %d\n", fieldname, expected, got, testcasenumber)
+	}
+}
+
+func performValidationTest(data interface{}, handler func(Errors), t *testing.T) {
+	recorder := httptest.NewRecorder()
+	m := martini.Classic()
+	m.Get(route, Validate(data), handler)
+
+	req, err := http.NewRequest("GET", route, nil)
+	if err != nil {
+		t.Error("HTTP error:", err)
+	}
+
+	m.ServeHTTP(recorder, req)
+}
+
+func (self BlogPost) Validate(errors *Errors, req *http.Request) {
+	if len(self.Title) < 4 {
+		errors.Fields["Title"] = "Too short; minimum 4 characters"
+	}
+	if len(self.Content) > 1024 {
+		errors.Fields["Content"] = "Too long; maximum 1024 characters"
+	}
+	if len(self.Content) < 5 {
+		errors.Fields["Content"] = "Too short; minimum 5 characters"
+	}
+}
+
+func (self BlogPost) Create(test testCase, t *testing.T, index int) {
+	assertEqualField(t, "Title", index, test.ref.Title, self.Title)
+	assertEqualField(t, "Content", index, test.ref.Content, self.Content)
+	assertEqualField(t, "Views", index, test.ref.Views, self.Views)
+
+	for i := range test.ref.Multiple {
+		if i >= len(self.Multiple) {
+			t.Errorf("Expected: %v (size %d) to have same size as: %v (size %d)", self.Multiple, len(self.Multiple), test.ref.Multiple, len(test.ref.Multiple))
+			break
+		}
+		if test.ref.Multiple[i] != self.Multiple[i] {
+			t.Errorf("Expected: %v to deep equal: %v", self.Multiple, test.ref.Multiple)
+			break
+		}
+	}
+}
+
+func (self BlogSection) Create(test emptyPayloadTestCase, t *testing.T, index int) {
+	// intentionally left empty
+}
+
+type (
+	testCase struct {
+		method      string
+		path        string
+		payload     string
+		contentType string
+		ok          bool
+		ref         *BlogPost
+	}
+
+	emptyPayloadTestCase struct {
+		method      string
+		path        string
+		payload     string
+		contentType string
+		ok          bool
+		ref         *BlogSection
+	}
+
+	Modeler interface {
+		Create(test testCase, t *testing.T, index int)
+	}
+
+	BlogPost struct {
+		Title    string `form:"title" json:"title" binding:"required"`
+		Content  string `form:"content" json:"content"`
+		Views    int    `form:"views" json:"views"`
+		internal int    `form:"-"`
+		Multiple []int  `form:"multiple"`
+	}
+
+	BlogSection struct {
+		Title   string `form:"title" json:"title"`
+		Content string `form:"content" json:"content"`
+	}
+
+	User struct {
+		Name string  `json:"name" binding:"required"`
+		Home Address `json:"address" binding:"required"`
+	}
+
+	Address struct {
+		Street1 string `json:"street1" binding:"required"`
+		Street2 string `json:"street2"`
+	}
+)
+
+var (
+	bindTests = map[testCase]int{
+		// These should bail at the deserialization/binding phase
+		testCase{
+			"POST",
+			path,
+			`{ bad JSON `,
+			"application/json",
+			false,
+			new(BlogPost),
+		}: http.StatusBadRequest,
+		testCase{
+			"POST",
+			path,
+			`not multipart but has content-type`,
+			"multipart/form-data",
+			false,
+			new(BlogPost),
+		}: http.StatusBadRequest,
+		testCase{
+			"POST",
+			path,
+			`no content-type and not URL-encoded or JSON"`,
+			"",
+			false,
+			new(BlogPost),
+		}: http.StatusBadRequest,
+
+		// These should deserialize, then bail at the validation phase
+		testCase{
+			"POST",
+			path + "?title= This is wrong  ",
+			`not URL-encoded but has content-type`,
+			"x-www-form-urlencoded",
+			false,
+			new(BlogPost),
+		}: 422, // according to comments in Form() -> although the request is not url encoded, ParseForm does not complain
+		testCase{
+			"GET",
+			path + "?content=This+is+the+content",
+			``,
+			"x-www-form-urlencoded",
+			false,
+			&BlogPost{Title: "", Content: "This is the content"},
+		}: 422,
+		testCase{
+			"GET",
+			path + "",
+			`{"content":"", "title":"Blog Post Title"}`,
+			"application/json",
+			false,
+			&BlogPost{Title: "Blog Post Title", Content: ""},
+		}: 422,
+
+		// These should succeed
+		testCase{
+			"GET",
+			path + "",
+			`{"content":"This is the content", "title":"Blog Post Title"}`,
+			"application/json",
+			true,
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
+		}: http.StatusOK,
+		testCase{
+			"GET",
+			path + "?content=This+is+the+content&title=Blog+Post+Title",
+			``,
+			"",
+			true,
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
+		}: http.StatusOK,
+		testCase{
+			"GET",
+			path + "?content=This is the content&title=Blog+Post+Title",
+			`{"content":"This is the content", "title":"Blog Post Title"}`,
+			"",
+			true,
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
+		}: http.StatusOK,
+		testCase{
+			"GET",
+			path + "",
+			`{"content":"This is the content", "title":"Blog Post Title"}`,
+			"",
+			true,
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
+		}: http.StatusOK,
+	}
+
+	bindMultipartTests = map[testCase]int{
+		// This should deserialize, then bail at the validation phase
+		testCase{
+			"POST",
+			path,
+			"",
+			"multipart/form-data",
+			false,
+			&BlogPost{Title: "", Content: "This is the content"},
+		}: 422,
+		// This should succeed
+		testCase{
+			"POST",
+			path,
+			"",
+			"multipart/form-data",
+			true,
+			&BlogPost{Title: "This is the Title", Content: "This is the content"},
+		}: http.StatusOK,
+	}
+
+	formTests = []testCase{
+		{
+			"GET",
+			path + "?content=This is the content",
+			"",
+			"",
+			false,
+			&BlogPost{Title: "", Content: "This is the content"},
+		},
+		{
+			"POST",
+			path + "?content=This+is+the+content&title=Blog+Post+Title&views=3",
+			"",
+			"",
+			false, // false because POST requests should have a body, not just a query string
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3},
+		},
+		{
+			"GET",
+			path + "?content=This+is+the+content&title=Blog+Post+Title&views=3&multiple=5&multiple=10&multiple=15&multiple=20",
+			"",
+			"",
+			true,
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3, Multiple: []int{5, 10, 15, 20}},
+		},
+	}
+
+	multipartformTests = []testCase{
+		{
+			"POST",
+			path,
+			"",
+			"multipart/form-data",
+			false,
+			&BlogPost{Title: "", Content: "This is the content"},
+		},
+		{
+			"POST",
+			path,
+			"",
+			"multipart/form-data",
+			false,
+			&BlogPost{Title: "Blog Post Title", Views: 3},
+		},
+		{
+			"POST",
+			path,
+			"",
+			"multipart/form-data",
+			true,
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3, Multiple: []int{5, 10, 15, 20}},
+		},
+	}
+
+	emptyPayloadTests = []emptyPayloadTestCase{
+		{
+			"GET",
+			"",
+			"",
+			"",
+			true,
+			&BlogSection{},
+		},
+		{
+			"POST",
+			"",
+			"",
+			"",
+			true,
+			&BlogSection{},
+		},
+		{
+			"PUT",
+			"",
+			"",
+			"",
+			true,
+			&BlogSection{},
+		},
+		{
+			"DELETE",
+			"",
+			"",
+			"",
+			true,
+			&BlogSection{},
+		},
+	}
+
+	jsonTests = []testCase{
+		// bad requests
+		{
+			"GET",
+			"",
+			`{blah blah blah}`,
+			"",
+			false,
+			&BlogPost{},
+		},
+		{
+			"POST",
+			"",
+			`{asdf}`,
+			"",
+			false,
+			&BlogPost{},
+		},
+		{
+			"PUT",
+			"",
+			`{blah blah blah}`,
+			"",
+			false,
+			&BlogPost{},
+		},
+		{
+			"DELETE",
+			"",
+			`{;sdf _SDf- }`,
+			"",
+			false,
+			&BlogPost{},
+		},
+
+		// Valid-JSON requests
+		{
+			"GET",
+			"",
+			`{"content":"This is the content"}`,
+			"",
+			false,
+			&BlogPost{Title: "", Content: "This is the content"},
+		},
+		{
+			"POST",
+			"",
+			`{}`,
+			"application/json",
+			false,
+			&BlogPost{Title: "", Content: ""},
+		},
+		{
+			"POST",
+			"",
+			`{"content":"This is the content", "title":"Blog Post Title"}`,
+			"",
+			true,
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
+		},
+		{
+			"PUT",
+			"",
+			`{"content":"This is the content", "title":"Blog Post Title"}`,
+			"",
+			true,
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
+		},
+		{
+			"DELETE",
+			"",
+			`{"content":"This is the content", "title":"Blog Post Title"}`,
+			"",
+			true,
+			&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
+		},
+	}
+)
+
+const (
+	route = "/blogposts/create"
+	path  = "http://localhost:3000" + route
+)

+ 80 - 6
modules/middleware/context.go

@@ -10,7 +10,10 @@ import (
 	"encoding/base64"
 	"fmt"
 	"html/template"
+	"io"
 	"net/http"
+	"net/url"
+	"path/filepath"
 	"strconv"
 	"strings"
 	"time"
@@ -34,6 +37,7 @@ type Context struct {
 	p        martini.Params
 	Req      *http.Request
 	Res      http.ResponseWriter
+	Flash    *Flash
 	Session  session.SessionStore
 	Cache    cache.Cache
 	User     *models.User
@@ -47,6 +51,7 @@ type Context struct {
 		IsBranch   bool
 		IsTag      bool
 		IsCommit   bool
+		HasAccess  bool
 		Repository *models.Repository
 		Owner      *models.User
 		Commit     *git.Commit
@@ -59,6 +64,7 @@ type Context struct {
 			HTTPS string
 			Git   string
 		}
+		Mirror *models.Mirror
 	}
 }
 
@@ -78,6 +84,8 @@ func (ctx *Context) HasError() bool {
 	if !ok {
 		return false
 	}
+	ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string)
+	ctx.Data["Flash"] = ctx.Flash
 	return hasErr.(bool)
 }
 
@@ -88,23 +96,21 @@ func (ctx *Context) HTML(status int, name string, htmlOpt ...HTMLOptions) {
 
 // RenderWithErr used for page has form validation but need to prompt error to users.
 func (ctx *Context) RenderWithErr(msg, tpl string, form auth.Form) {
-	ctx.Data["HasError"] = true
-	ctx.Data["ErrorMsg"] = msg
 	if form != nil {
 		auth.AssignForm(form, ctx.Data)
 	}
+	ctx.Flash.ErrorMsg = msg
+	ctx.Data["Flash"] = ctx.Flash
 	ctx.HTML(200, tpl)
 }
 
 // Handle handles and logs error by given status.
 func (ctx *Context) Handle(status int, title string, err error) {
 	log.Error("%s: %v", title, err)
-	if martini.Dev == martini.Prod {
-		ctx.HTML(500, "status/500")
-		return
+	if martini.Dev != martini.Prod {
+		ctx.Data["ErrorMsg"] = err
 	}
 
-	ctx.Data["ErrorMsg"] = err
 	ctx.HTML(status, fmt.Sprintf("status/%d", status))
 }
 
@@ -239,6 +245,56 @@ func (ctx *Context) CsrfTokenValid() bool {
 	return true
 }
 
+func (ctx *Context) ServeFile(file string, names ...string) {
+	var name string
+	if len(names) > 0 {
+		name = names[0]
+	} else {
+		name = filepath.Base(file)
+	}
+	ctx.Res.Header().Set("Content-Description", "File Transfer")
+	ctx.Res.Header().Set("Content-Type", "application/octet-stream")
+	ctx.Res.Header().Set("Content-Disposition", "attachment; filename="+name)
+	ctx.Res.Header().Set("Content-Transfer-Encoding", "binary")
+	ctx.Res.Header().Set("Expires", "0")
+	ctx.Res.Header().Set("Cache-Control", "must-revalidate")
+	ctx.Res.Header().Set("Pragma", "public")
+	http.ServeFile(ctx.Res, ctx.Req, file)
+}
+
+func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
+	modtime := time.Now()
+	for _, p := range params {
+		switch v := p.(type) {
+		case time.Time:
+			modtime = v
+		}
+	}
+	ctx.Res.Header().Set("Content-Description", "File Transfer")
+	ctx.Res.Header().Set("Content-Type", "application/octet-stream")
+	ctx.Res.Header().Set("Content-Disposition", "attachment; filename="+name)
+	ctx.Res.Header().Set("Content-Transfer-Encoding", "binary")
+	ctx.Res.Header().Set("Expires", "0")
+	ctx.Res.Header().Set("Cache-Control", "must-revalidate")
+	ctx.Res.Header().Set("Pragma", "public")
+	http.ServeContent(ctx.Res, ctx.Req, name, modtime, r)
+}
+
+type Flash struct {
+	url.Values
+	ErrorMsg, SuccessMsg string
+}
+
+func (f *Flash) Error(msg string) {
+	f.Set("error", msg)
+	f.ErrorMsg = msg
+}
+
+func (f *Flash) Success(msg string) {
+	f.Set("success", msg)
+	f.SuccessMsg = msg
+}
+
 // InitContext initializes a classic context for a request.
 func InitContext() martini.Handler {
 	return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) {
@@ -256,9 +312,27 @@ func InitContext() martini.Handler {
 
 		// start session
 		ctx.Session = base.SessionManager.SessionStart(res, r)
+
+		// Get flash.
+		values, err := url.ParseQuery(ctx.GetCookie("gogs_flash"))
+		if err != nil {
+			log.Error("InitContext.ParseQuery(flash): %v", err)
+		} else if len(values) > 0 {
+			ctx.Flash = &Flash{Values: values}
+			ctx.Flash.ErrorMsg = ctx.Flash.Get("error")
+			ctx.Flash.SuccessMsg = ctx.Flash.Get("success")
+			ctx.Data["Flash"] = ctx.Flash
+			ctx.SetCookie("gogs_flash", "", -1)
+		}
+		ctx.Flash = &Flash{Values: url.Values{}}
+
 		rw := res.(martini.ResponseWriter)
 		rw.Before(func(martini.ResponseWriter) {
 			ctx.Session.SessionRelease(res)
+
+			if flash := ctx.Flash.Encode(); len(flash) > 0 {
+				ctx.SetCookie("gogs_flash", ctx.Flash.Encode(), 0)
+			}
 		})
 
 		// Get user from session if logined.

+ 1 - 1
modules/middleware/render.go

@@ -146,7 +146,7 @@ func compile(options RenderOptions) *template.Template {
 				tmpl := t.New(filepath.ToSlash(name))
 
 				for _, funcs := range options.Funcs {
-					tmpl.Funcs(funcs)
+					tmpl = tmpl.Funcs(funcs)
 				}
 
 				template.Must(tmpl.Funcs(helperFuncs).Parse(string(buf)))

+ 79 - 17
modules/middleware/repo.go

@@ -15,6 +15,7 @@ import (
 
 	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/log"
 )
 
 func RepoAssignment(redirect bool, args ...bool) martini.Handler {
@@ -39,7 +40,7 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler {
 
 		userName := params["username"]
 		repoName := params["reponame"]
-		branchName := params["branchname"]
+		refName := params["branchname"]
 
 		// get repository owner
 		ctx.Repo.IsOwner = ctx.IsSigned && ctx.User.LowerName == strings.ToLower(userName)
@@ -66,34 +67,69 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler {
 			ctx.Handle(200, "RepoAssignment", errors.New("invliad user account for single repository"))
 			return
 		}
+		ctx.Repo.Owner = user
 
 		// get repository
 		repo, err := models.GetRepositoryByName(user.Id, repoName)
 		if err != nil {
 			if err == models.ErrRepoNotExist {
 				ctx.Handle(404, "RepoAssignment", err)
+				return
 			} else if redirect {
 				ctx.Redirect("/")
 				return
 			}
-			ctx.Handle(404, "RepoAssignment", err)
+			ctx.Handle(500, "RepoAssignment", err)
 			return
 		}
+
+		// Check access.
+		if repo.IsPrivate {
+			if ctx.User == nil {
+				ctx.Handle(404, "RepoAssignment(HasAccess)", nil)
+				return
+			}
+
+			hasAccess, err := models.HasAccess(ctx.User.Name, ctx.Repo.Owner.Name+"/"+repo.Name, models.AU_READABLE)
+			if err != nil {
+				ctx.Handle(500, "RepoAssignment(HasAccess)", err)
+				return
+			} else if !hasAccess {
+				ctx.Handle(404, "RepoAssignment(HasAccess)", nil)
+				return
+			}
+		}
+		ctx.Repo.HasAccess = true
+		ctx.Data["HasAccess"] = true
+
+		if repo.IsMirror {
+			ctx.Repo.Mirror, err = models.GetMirror(repo.Id)
+			if err != nil {
+				ctx.Handle(500, "RepoAssignment(GetMirror)", err)
+				return
+			}
+			ctx.Data["MirrorInterval"] = ctx.Repo.Mirror.Interval
+		}
+
 		repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
 		ctx.Repo.Repository = repo
-
 		ctx.Data["IsBareRepo"] = ctx.Repo.Repository.IsBare
 
 		gitRepo, err := git.OpenRepository(models.RepoPath(userName, repoName))
 		if err != nil {
-			ctx.Handle(404, "RepoAssignment Invalid repo "+models.RepoPath(userName, repoName), err)
+			ctx.Handle(500, "RepoAssignment Invalid repo "+models.RepoPath(userName, repoName), err)
 			return
 		}
 		ctx.Repo.GitRepo = gitRepo
-
-		ctx.Repo.Owner = user
 		ctx.Repo.RepoLink = "/" + user.Name + "/" + repo.Name
 
+		tags, err := ctx.Repo.GitRepo.GetTags()
+		if err != nil {
+			ctx.Handle(500, "RepoAssignment(GetTags))", err)
+			return
+		}
+		ctx.Repo.Repository.NumTags = len(tags)
+
 		ctx.Data["Title"] = user.Name + "/" + repo.Name
 		ctx.Data["Repository"] = repo
 		ctx.Data["Owner"] = user
@@ -105,29 +141,43 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler {
 		ctx.Repo.CloneLink.HTTPS = fmt.Sprintf("%s%s/%s.git", base.AppUrl, user.LowerName, repo.LowerName)
 		ctx.Data["CloneLink"] = ctx.Repo.CloneLink
 
+		if ctx.Repo.Repository.IsGoget {
+			ctx.Data["GoGetLink"] = fmt.Sprintf("%s%s/%s", base.AppUrl, user.LowerName, repo.LowerName)
+			ctx.Data["GoGetImport"] = fmt.Sprintf("%s/%s/%s", base.Domain, user.LowerName, repo.LowerName)
+		}
+
 		// when repo is bare, not valid branch
 		if !ctx.Repo.Repository.IsBare && validBranch {
 		detect:
-			if len(branchName) > 0 {
-				// TODO check tag
-				if models.IsBranchExist(user.Name, repoName, branchName) {
+			if len(refName) > 0 {
+				if gitRepo.IsBranchExist(refName) {
 					ctx.Repo.IsBranch = true
-					ctx.Repo.BranchName = branchName
+					ctx.Repo.BranchName = refName
 
-					ctx.Repo.Commit, err = gitRepo.GetCommitOfBranch(branchName)
+					ctx.Repo.Commit, err = gitRepo.GetCommitOfBranch(refName)
 					if err != nil {
 						ctx.Handle(404, "RepoAssignment invalid branch", nil)
 						return
 					}
+					ctx.Repo.CommitId = ctx.Repo.Commit.Id.String()
 
-					ctx.Repo.CommitId = ctx.Repo.Commit.Oid.String()
+				} else if gitRepo.IsTagExist(refName) {
+					ctx.Repo.IsBranch = true
+					ctx.Repo.BranchName = refName
 
-				} else if len(branchName) == 40 {
+					ctx.Repo.Commit, err = gitRepo.GetCommitOfTag(refName)
+					if err != nil {
+						ctx.Handle(404, "RepoAssignment invalid tag", nil)
+						return
+					}
+					ctx.Repo.CommitId = ctx.Repo.Commit.Id.String()
+
+				} else if len(refName) == 40 {
 					ctx.Repo.IsCommit = true
-					ctx.Repo.CommitId = branchName
-					ctx.Repo.BranchName = branchName
+					ctx.Repo.CommitId = refName
+					ctx.Repo.BranchName = refName
 
-					ctx.Repo.Commit, err = gitRepo.GetCommit(branchName)
+					ctx.Repo.Commit, err = gitRepo.GetCommit(refName)
 					if err != nil {
 						ctx.Handle(404, "RepoAssignment invalid commit", nil)
 						return
@@ -138,16 +188,23 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler {
 				}
 
 			} else {
-				branchName = "master"
+				refName = ctx.Repo.Repository.DefaultBranch
+				if len(refName) == 0 {
+					refName = "master"
+				}
 				goto detect
 			}
 
 			ctx.Data["IsBranch"] = ctx.Repo.IsBranch
 			ctx.Data["IsCommit"] = ctx.Repo.IsCommit
+			log.Debug("Repo.Commit: %v", ctx.Repo.Commit)
 		}
 
+		log.Debug("displayBare: %v; IsBare: %v", displayBare, ctx.Repo.Repository.IsBare)
+
 		// repo is bare and display enable
 		if displayBare && ctx.Repo.Repository.IsBare {
+			log.Debug("Bare repository: %s", ctx.Repo.RepoLink)
 			ctx.HTML(200, "repo/single_bare")
 			return
 		}
@@ -157,6 +214,11 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler {
 		}
 
 		ctx.Data["BranchName"] = ctx.Repo.BranchName
+		brs, err := ctx.Repo.GitRepo.GetBranches()
+		if err != nil {
+			log.Error("RepoAssignment(GetBranches): %v", err)
+		}
+		ctx.Data["Branches"] = brs
 		ctx.Data["CommitId"] = ctx.Repo.CommitId
 		ctx.Data["IsRepositoryWatching"] = ctx.Repo.IsWatching
 	}

+ 396 - 0
modules/social/social.go

@@ -0,0 +1,396 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package social
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
+	oauth "github.com/gogits/oauth2"
+
+	"github.com/gogits/gogs/models"
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/log"
+)
+
+type BasicUserInfo struct {
+	Identity string
+	Name     string
+	Email    string
+}
+
+type SocialConnector interface {
+	Type() int
+	SetRedirectUrl(string)
+	UserInfo(*oauth.Token, *url.URL) (*BasicUserInfo, error)
+
+	AuthCodeURL(string) string
+	Exchange(string) (*oauth.Token, error)
+}
+
+var (
+	SocialBaseUrl = "/user/login"
+	SocialMap     = make(map[string]SocialConnector)
+)
+
+func NewOauthService() {
+	if !base.Cfg.MustBool("oauth", "ENABLED") {
+		return
+	}
+
+	base.OauthService = &base.Oauther{}
+	base.OauthService.OauthInfos = make(map[string]*base.OauthInfo)
+
+	socialConfigs := make(map[string]*oauth.Config)
+	allOauthes := []string{"github", "google", "qq", "twitter", "weibo"}
+	// Load all OAuth config data.
+	for _, name := range allOauthes {
+		base.OauthService.OauthInfos[name] = &base.OauthInfo{
+			ClientId:     base.Cfg.MustValue("oauth."+name, "CLIENT_ID"),
+			ClientSecret: base.Cfg.MustValue("oauth."+name, "CLIENT_SECRET"),
+			Scopes:       base.Cfg.MustValue("oauth."+name, "SCOPES"),
+			AuthUrl:      base.Cfg.MustValue("oauth."+name, "AUTH_URL"),
+			TokenUrl:     base.Cfg.MustValue("oauth."+name, "TOKEN_URL"),
+		}
+		socialConfigs[name] = &oauth.Config{
+			ClientId:     base.OauthService.OauthInfos[name].ClientId,
+			ClientSecret: base.OauthService.OauthInfos[name].ClientSecret,
+			RedirectURL:  strings.TrimSuffix(base.AppUrl, "/") + SocialBaseUrl + name,
+			Scope:        base.OauthService.OauthInfos[name].Scopes,
+			AuthURL:      base.OauthService.OauthInfos[name].AuthUrl,
+			TokenURL:     base.OauthService.OauthInfos[name].TokenUrl,
+		}
+	}
+
+	enabledOauths := make([]string, 0, 10)
+
+	// GitHub.
+	if base.Cfg.MustBool("oauth.github", "ENABLED") {
+		base.OauthService.GitHub = true
+		newGitHubOauth(socialConfigs["github"])
+		enabledOauths = append(enabledOauths, "GitHub")
+	}
+
+	// Google.
+	if base.Cfg.MustBool("oauth.google", "ENABLED") {
+		base.OauthService.Google = true
+		newGoogleOauth(socialConfigs["google"])
+		enabledOauths = append(enabledOauths, "Google")
+	}
+
+	// QQ.
+	if base.Cfg.MustBool("oauth.qq", "ENABLED") {
+		base.OauthService.Tencent = true
+		newTencentOauth(socialConfigs["qq"])
+		enabledOauths = append(enabledOauths, "QQ")
+	}
+
+	// Twitter.
+	if base.Cfg.MustBool("oauth.twitter", "ENABLED") {
+		base.OauthService.Twitter = true
+		newTwitterOauth(socialConfigs["twitter"])
+		enabledOauths = append(enabledOauths, "Twitter")
+	}
+
+	// Weibo.
+	if base.Cfg.MustBool("oauth.weibo", "ENABLED") {
+		base.OauthService.Weibo = true
+		newWeiboOauth(socialConfigs["weibo"])
+		enabledOauths = append(enabledOauths, "Weibo")
+	}
+
+	log.Info("Oauth Service Enabled %s", enabledOauths)
+}
+
+//   ________.__  __     ___ ___      ___.
+//  /  _____/|__|/  |_  /   |   \ __ _\_ |__
+// /   \  ___|  \   __\/    ~    \  |  \ __ \
+// \    \_\  \  ||  |  \    Y    /  |  / \_\ \
+//  \______  /__||__|   \___|_  /|____/|___  /
+//         \/                 \/           \/
+
+type SocialGithub struct {
+	Token *oauth.Token
+	*oauth.Transport
+}
+
+func (s *SocialGithub) Type() int {
+	return models.OT_GITHUB
+}
+
+func newGitHubOauth(config *oauth.Config) {
+	SocialMap["github"] = &SocialGithub{
+		Transport: &oauth.Transport{
+			Config:    config,
+			Transport: http.DefaultTransport,
+		},
+	}
+}
+
+func (s *SocialGithub) SetRedirectUrl(url string) {
+	s.Transport.Config.RedirectURL = url
+}
+
+func (s *SocialGithub) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) {
+	transport := &oauth.Transport{
+		Token: token,
+	}
+	var data struct {
+		Id    int    `json:"id"`
+		Name  string `json:"login"`
+		Email string `json:"email"`
+	}
+	var err error
+	r, err := transport.Client().Get(s.Transport.Scope)
+	if err != nil {
+		return nil, err
+	}
+	defer r.Body.Close()
+	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+		return nil, err
+	}
+	return &BasicUserInfo{
+		Identity: strconv.Itoa(data.Id),
+		Name:     data.Name,
+		Email:    data.Email,
+	}, nil
+}
+
+//   ________                     .__
+//  /  _____/  ____   ____   ____ |  |   ____
+// /   \  ___ /  _ \ /  _ \ / ___\|  | _/ __ \
+// \    \_\  (  <_> |  <_> ) /_/  >  |_\  ___/
+//  \______  /\____/ \____/\___  /|____/\___  >
+//         \/             /_____/           \/
+
+type SocialGoogle struct {
+	Token *oauth.Token
+	*oauth.Transport
+}
+
+func (s *SocialGoogle) Type() int {
+	return models.OT_GOOGLE
+}
+
+func newGoogleOauth(config *oauth.Config) {
+	SocialMap["google"] = &SocialGoogle{
+		Transport: &oauth.Transport{
+			Config:    config,
+			Transport: http.DefaultTransport,
+		},
+	}
+}
+
+func (s *SocialGoogle) SetRedirectUrl(url string) {
+	s.Transport.Config.RedirectURL = url
+}
+
+func (s *SocialGoogle) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) {
+	transport := &oauth.Transport{Token: token}
+	var data struct {
+		Id    string `json:"id"`
+		Name  string `json:"name"`
+		Email string `json:"email"`
+	}
+	var err error
+
+	reqUrl := "https://www.googleapis.com/oauth2/v1/userinfo"
+	r, err := transport.Client().Get(reqUrl)
+	if err != nil {
+		return nil, err
+	}
+	defer r.Body.Close()
+	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+		return nil, err
+	}
+	return &BasicUserInfo{
+		Identity: data.Id,
+		Name:     data.Name,
+		Email:    data.Email,
+	}, nil
+}
+
+// ________   ________
+// \_____  \  \_____  \
+//  /  / \  \  /  / \  \
+// /   \_/.  \/   \_/.  \
+// \_____\ \_/\_____\ \_/
+//        \__>       \__>
+
+type SocialTencent struct {
+	Token *oauth.Token
+	*oauth.Transport
+	reqUrl string
+}
+
+func (s *SocialTencent) Type() int {
+	return models.OT_QQ
+}
+
+func newTencentOauth(config *oauth.Config) {
+	SocialMap["qq"] = &SocialTencent{
+		reqUrl: "https://open.t.qq.com/api/user/info",
+		Transport: &oauth.Transport{
+			Config:    config,
+			Transport: http.DefaultTransport,
+		},
+	}
+}
+
+func (s *SocialTencent) SetRedirectUrl(url string) {
+	s.Transport.Config.RedirectURL = url
+}
+
+func (s *SocialTencent) UserInfo(token *oauth.Token, URL *url.URL) (*BasicUserInfo, error) {
+	var data struct {
+		Data struct {
+			Id    string `json:"openid"`
+			Name  string `json:"name"`
+			Email string `json:"email"`
+		} `json:"data"`
+	}
+	var err error
+	// https://open.t.qq.com/api/user/info?
+	//oauth_consumer_key=APP_KEY&
+	//access_token=ACCESSTOKEN&openid=openid
+	//clientip=CLIENTIP&oauth_version=2.a
+	//scope=all
+	var urls = url.Values{
+		"oauth_consumer_key": {s.Transport.Config.ClientId},
+		"access_token":       {token.AccessToken},
+		"openid":             URL.Query()["openid"],
+		"oauth_version":      {"2.a"},
+		"scope":              {"all"},
+	}
+	r, err := http.Get(s.reqUrl + "?" + urls.Encode())
+	if err != nil {
+		return nil, err
+	}
+	defer r.Body.Close()
+	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+		return nil, err
+	}
+	return &BasicUserInfo{
+		Identity: data.Data.Id,
+		Name:     data.Data.Name,
+		Email:    data.Data.Email,
+	}, nil
+}
+
+// ___________       .__  __    __
+// \__    ___/_  _  _|__|/  |__/  |_  ___________
+//   |    |  \ \/ \/ /  \   __\   __\/ __ \_  __ \
+//   |    |   \     /|  ||  |  |  | \  ___/|  | \/
+//   |____|    \/\_/ |__||__|  |__|  \___  >__|
+//                                       \/
+
+type SocialTwitter struct {
+	Token *oauth.Token
+	*oauth.Transport
+}
+
+func (s *SocialTwitter) Type() int {
+	return models.OT_TWITTER
+}
+
+func newTwitterOauth(config *oauth.Config) {
+	SocialMap["twitter"] = &SocialTwitter{
+		Transport: &oauth.Transport{
+			Config:    config,
+			Transport: http.DefaultTransport,
+		},
+	}
+}
+
+func (s *SocialTwitter) SetRedirectUrl(url string) {
+	s.Transport.Config.RedirectURL = url
+}
+
+//https://github.com/mrjones/oauth
+func (s *SocialTwitter) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) {
+	// transport := &oauth.Transport{Token: token}
+	// var data struct {
+	// 	Id    string `json:"id"`
+	// 	Name  string `json:"name"`
+	// 	Email string `json:"email"`
+	// }
+	// var err error
+
+	// reqUrl := "https://www.googleapis.com/oauth2/v1/userinfo"
+	// r, err := transport.Client().Get(reqUrl)
+	// if err != nil {
+	// 	return nil, err
+	// }
+	// defer r.Body.Close()
+	// if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+	// 	return nil, err
+	// }
+	// return &BasicUserInfo{
+	// 	Identity: data.Id,
+	// 	Name:     data.Name,
+	// 	Email:    data.Email,
+	// }, nil
+	return nil, nil
+}
+
+//  __      __       ._____.
+// /  \    /  \ ____ |__\_ |__   ____
+// \   \/\/   // __ \|  || __ \ /  _ \
+//  \        /\  ___/|  || \_\ (  <_> )
+//   \__/\  /  \___  >__||___  /\____/
+//        \/       \/        \/
+
+type SocialWeibo struct {
+	Token *oauth.Token
+	*oauth.Transport
+}
+
+func (s *SocialWeibo) Type() int {
+	return models.OT_WEIBO
+}
+
+func newWeiboOauth(config *oauth.Config) {
+	SocialMap["weibo"] = &SocialWeibo{
+		Transport: &oauth.Transport{
+			Config:    config,
+			Transport: http.DefaultTransport,
+		},
+	}
+}
+
+func (s *SocialWeibo) SetRedirectUrl(url string) {
+	s.Transport.Config.RedirectURL = url
+}
+
+func (s *SocialWeibo) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) {
+	transport := &oauth.Transport{Token: token}
+	var data struct {
+		Name string `json:"name"`
+	}
+	var err error
+
+	var urls = url.Values{
+		"access_token": {token.AccessToken},
+		"uid":          {token.Extra["id_token"]},
+	}
+	reqUrl := "https://api.weibo.com/2/users/show.json"
+	r, err := transport.Client().Get(reqUrl + "?" + urls.Encode())
+	if err != nil {
+		return nil, err
+	}
+	defer r.Body.Close()
+	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+		return nil, err
+	}
+	return &BasicUserInfo{
+		Identity: token.Extra["id_token"],
+		Name:     data.Name,
+	}, nil
+	return nil, nil
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
public/css/bootstrap.css.map


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
public/css/bootstrap.min.css


+ 198 - 11
public/css/gogs.css

@@ -67,12 +67,14 @@ html, body {
     color: #EEE;
     font-size: 100%;
     height: 46px;
+    margin-top: 3px;
 }
 
 #nav-logo {
     padding-left: 0;
     padding-right: 0;
     margin-right: 10px;
+    margin-top: 0;
 }
 
 .nav-item:hover,
@@ -81,10 +83,6 @@ html, body {
     text-decoration: none;
 }
 
-.nav-item.navbar-right {
-    margin-top: 3px;
-}
-
 .nav-item.navbar-btn {
     cursor: pointer;
     margin-top: 8px;
@@ -96,6 +94,30 @@ html, body {
     margin: 0;
 }
 
+#nav-search-form {
+    width: 300px;
+    margin-top: 0;
+}
+
+#nav-search-form button {
+    margin-top: 0;
+    background-image: none;
+    background-color: #F6F6F6;
+}
+
+#nav-search-form input[type=search] {
+    background-color: #F6F6F6;
+    border-bottom-right-radius: 3px;
+    border-top-right-radius: 3px;
+    -webkit-transition: width linear .25s;
+}
+
+#nav-search-form input[type=search]:focus {
+    background-color: #FFF;
+    border-color: #D9D9D9;
+    width: 320px;
+}
+
 /* gogits nav item active status */
 #masthead .nav .active {
     color: #fff;
@@ -239,14 +261,40 @@ html, body {
 }
 
 #social-login {
-    margin-top: 30px;
-    padding-top: 20px;
+    margin-top: 40px;
+    padding-top: 40px;
     border-top: 1px solid #ccc;
+    position: relative;
 }
 
 #social-login .btn {
     float: none;
-    margin: auto;
+    margin: auto 4px;
+}
+
+#social-login .btn .fa {
+    margin-left: 0;
+    margin-right: 4px;
+}
+
+#social-login .btn span {
+    display: inline-block;
+    vertical-align: top;
+    font-size: 16px;
+    margin-top: 5px;
+}
+
+#social-login h4 {
+    position: absolute;
+    top: -20px;
+    width: 100%;
+    text-align: center;
+    background-color: transparent;
+}
+
+#social-login h4 span {
+    background-color: #FFF;
+    padding: 0 12px;
 }
 
 /* gogs-user-profile */
@@ -291,6 +339,22 @@ html, body {
     padding-right: 18px;
 }
 
+#user-profile .profile-rel .col-md-6 {
+    text-align: center;
+    padding-bottom: 12px;
+}
+
+#user-profile .profile-rel strong {
+    font-size: 24px;
+    color: #444;
+    display: block;
+}
+
+#user-profile .profile-rel p {
+    margin-right: 0;
+    color: #888;
+}
+
 #user-activity .tab-pane {
     padding: 20px;
 }
@@ -309,6 +373,18 @@ html, body {
     height: 8em;
 }
 
+#repo-import-auth {
+    width: 100%;
+    margin-top: 48px;
+    box-sizing: border-box;
+}
+
+#repo-import-auth .form-group {
+    box-sizing: border-box;
+    margin-left: 0;
+    margin-right: 0;
+}
+
 /* gogits user setting */
 
 #user-setting-nav > h4, #user-setting-container > h4, #user-setting-container > div > h4,
@@ -444,6 +520,43 @@ html, body {
     margin-right: 1em;
 }
 
+#user-dashboard-repo-new .btn-sm.dropdown-toggle {
+    padding: 3px 8px;
+}
+
+#user-dashboard-repo-new .dropdown-menu, #nav-repo-new .dropdown-menu {
+    padding: 0;
+    margin: 0;
+}
+
+#user-dashboard-repo-new ul, #nav-repo-new ul {
+    margin: 0;
+    width: 200px;
+}
+
+#user-dashboard-repo-new li a, #nav-repo-new li a {
+    line-height: 36px;
+    display: block;
+    padding: 0 18px;
+    color: #444;
+}
+
+#user-dashboard-repo-new li a:hover, #nav-repo-new li a:hover {
+    background: #0093c4;
+    color: #FFF;
+}
+
+#nav-repo-new button {
+    border: none;
+    background: transparent;
+    padding: 0;
+    width: 15px;
+}
+
+#nav-repo-new li .fa {
+    margin: 0 .5em;
+}
+
 /* gogits repo single page */
 
 #body-nav.repo-nav {
@@ -614,6 +727,10 @@ html, body {
     margin-top: -20px;
 }
 
+#commits-pager {
+    margin-top: 0;
+}
+
 #source .source-toolbar:after {
     clear: both;
 }
@@ -831,6 +948,10 @@ html, body {
     margin-left: .5em;
 }
 
+#commits-search-form {
+    margin-top: 4px;
+}
+
 .commit-box .avatar, .diff-head-box .avatar {
     width: 20px;
     height: 20px;
@@ -838,10 +959,6 @@ html, body {
     vertical-align: top;
 }
 
-.commit-box .search {
-    margin-top: 3px;
-}
-
 .commit-box td {
     background-color: #FFF;
 }
@@ -1304,4 +1421,74 @@ html, body {
 
 #release .release-item .info .avatar {
     vertical-align: middle;
+}
+
+#release-new-form {
+    margin-top: 24px;
+}
+
+#release-new-form .target-at {
+    margin: 0 1em;
+}
+
+#release-new-form .target-text {
+    color: #888;
+}
+
+#release-new-target-branch-list {
+    padding-top: 0;
+    padding-bottom: 0;
+    min-width: 200px;
+}
+
+#release-new-target-branch-list ul {
+    margin-bottom: 0;
+}
+
+#release-new-target-branch-list li {
+    padding: 8px 20px;
+}
+
+#release-new-target-branch-list li a {
+    margin-left: 0;
+    background-color: transparent;
+    padding: 0;
+}
+
+#release-new-target-branch-list li a:hover {
+    background-image: none;
+}
+
+#release-new-target-branch-list li:hover {
+    background-color: #0093c4;
+}
+
+#release-new-target-branch-list li:hover a {
+    color: #FFF;
+}
+
+#release-new-title {
+    width: 50%;
+}
+
+#release-new-content-div {
+    margin-top: 16px;
+    padding-left: 0;
+}
+
+#release-new-content-div .md-help {
+    margin-top: 6px;
+}
+
+#release-textarea .form-group {
+    display: block;
+}
+
+#release-new-content {
+    width: 100%;
+    margin: 16px 0;
+}
+
+#release-preview {
+    margin: 6px 0;
 }

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
public/css/todc-bootstrap.css.map


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 1
public/css/todc-bootstrap.min.css


BIN
public/img/favicon.png


+ 54 - 1
public/js/app.js

@@ -354,6 +354,7 @@ function initRegister() {
 }
 
 function initUserSetting() {
+    // ssh confirmation
     $('#ssh-keys .delete').confirmation({
         singleton: true,
         onConfirm: function (e, $this) {
@@ -366,6 +367,18 @@ function initUserSetting() {
             });
         }
     });
+
+    // profile form
+    (function () {
+        $('#user-setting-username').on("keyup", function () {
+            var $this = $(this);
+            if ($this.val() != $this.attr('title')) {
+                $this.next('.help-block').toggleShow();
+            } else {
+                $this.next('.help-block').toggleHide();
+            }
+        });
+    }())
 }
 
 function initRepository() {
@@ -383,7 +396,7 @@ function initRepository() {
                     $clone.find('span.clone-url').text($this.data('link'));
                 }
             }).eq(0).trigger("click");
-            $("#repo-clone").on("shown.bs.dropdown",function () {
+            $("#repo-clone").on("shown.bs.dropdown", function () {
                 Gogits.bindCopy("[data-init=copy]");
             });
             Gogits.bindCopy("[data-init=copy]:visible");
@@ -438,6 +451,18 @@ function initRepository() {
             $item.find(".bar .add").css("width", addPercent + "%");
         });
     }());
+
+    // repo setting form
+    (function () {
+        $('#repo-setting-name').on("keyup", function () {
+            var $this = $(this);
+            if ($this.val() != $this.attr('title')) {
+                $this.next('.help-block').toggleShow();
+            } else {
+                $this.next('.help-block').toggleHide();
+            }
+        });
+    }())
 }
 
 function initInstall() {
@@ -520,6 +545,31 @@ function initIssue() {
 
 }
 
+function initRelease() {
+// release new ajax preview
+    (function () {
+        $('[data-ajax-name=release-preview]').on("click", function () {
+            var $this = $(this);
+            $this.toggleAjax(function (json) {
+                if (json.ok) {
+                    $($this.data("preview")).html(json.content);
+                }
+            })
+        });
+        $('.release-write a[data-toggle]').on("click", function () {
+            $('.release-preview-content').html("loading...");
+        });
+    }());
+
+    // release new target selection
+    (function () {
+        $('#release-new-target-branch-list').on('click', 'a', function () {
+            $('#tag-target').val($(this).text());
+            $('#release-new-target-name').text(" " + $(this).text());
+        });
+    }());
+}
+
 (function ($) {
     $(function () {
         initCore();
@@ -539,5 +589,8 @@ function initIssue() {
         if ($('#issue').length) {
             initIssue();
         }
+        if ($('#release').length) {
+            initRelease();
+        }
     });
 })(jQuery);

+ 6 - 0
routers/admin/admin.go

@@ -153,6 +153,12 @@ func Config(ctx *middleware.Context) {
 		ctx.Data["Mailer"] = base.MailService
 	}
 
+	ctx.Data["OauthEnabled"] = false
+	if base.OauthService != nil {
+		ctx.Data["OauthEnabled"] = true
+		ctx.Data["Oauther"] = base.OauthService
+	}
+
 	ctx.Data["CacheAdapter"] = base.CacheAdapter
 	ctx.Data["CacheConfig"] = base.CacheConfig
 

+ 39 - 27
routers/admin/user.go

@@ -16,14 +16,15 @@ import (
 	"github.com/gogits/gogs/modules/middleware"
 )
 
-func NewUser(ctx *middleware.Context, form auth.RegisterForm) {
+func NewUser(ctx *middleware.Context) {
 	ctx.Data["Title"] = "New Account"
 	ctx.Data["PageIsUsers"] = true
+	ctx.HTML(200, "admin/users/new")
+}
 
-	if ctx.Req.Method == "GET" {
-		ctx.HTML(200, "admin/users/new")
-		return
-	}
+func NewUserPost(ctx *middleware.Context, form auth.RegisterForm) {
+	ctx.Data["Title"] = "New Account"
+	ctx.Data["PageIsUsers"] = true
 
 	if form.Password != form.RetypePasswd {
 		ctx.Data["HasError"] = true
@@ -55,7 +56,7 @@ func NewUser(ctx *middleware.Context, form auth.RegisterForm) {
 		case models.ErrUserNameIllegal:
 			ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "admin/users/new", &form)
 		default:
-			ctx.Handle(200, "admin.user.NewUser", err)
+			ctx.Handle(500, "admin.user.NewUser", err)
 		}
 		return
 	}
@@ -66,25 +67,39 @@ func NewUser(ctx *middleware.Context, form auth.RegisterForm) {
 	ctx.Redirect("/admin/users")
 }
 
-func EditUser(ctx *middleware.Context, params martini.Params, form auth.AdminEditUserForm) {
+func EditUser(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)
+		ctx.Handle(404, "admin.user.EditUser", err)
 		return
 	}
 
 	u, err := models.GetUserById(int64(uid))
 	if err != nil {
-		ctx.Handle(200, "admin.user.EditUser", err)
+		ctx.Handle(500, "admin.user.EditUser", err)
 		return
 	}
 
-	if ctx.Req.Method == "GET" {
-		ctx.Data["User"] = u
-		ctx.HTML(200, "admin/users/edit")
+	ctx.Data["User"] = u
+	ctx.HTML(200, "admin/users/edit")
+}
+
+func EditUserPost(ctx *middleware.Context, params martini.Params, form auth.AdminEditUserForm) {
+	ctx.Data["Title"] = "Edit Account"
+	ctx.Data["PageIsUsers"] = true
+
+	uid, err := base.StrTo(params["userid"]).Int()
+	if err != nil {
+		ctx.Handle(404, "admin.user.EditUser", err)
+		return
+	}
+
+	u, err := models.GetUserById(int64(uid))
+	if err != nil {
+		ctx.Handle(500, "admin.user.EditUser", err)
 		return
 	}
 
@@ -96,47 +111,44 @@ func EditUser(ctx *middleware.Context, params martini.Params, form auth.AdminEdi
 	u.IsActive = form.Active == "on"
 	u.IsAdmin = form.Admin == "on"
 	if err := models.UpdateUser(u); err != nil {
-		ctx.Handle(200, "admin.user.EditUser", err)
+		ctx.Handle(500, "admin.user.EditUser", err)
 		return
 	}
-
-	ctx.Data["IsSuccess"] = true
-	ctx.Data["User"] = u
-	ctx.HTML(200, "admin/users/edit")
-
 	log.Trace("%s User profile updated by admin(%s): %s", ctx.Req.RequestURI,
 		ctx.User.LowerName, ctx.User.LowerName)
+
+	ctx.Data["User"] = u
+	ctx.Flash.Success("Account profile has been successfully updated.")
+	ctx.Redirect("/admin/users/" + params["userid"])
 }
 
 func DeleteUser(ctx *middleware.Context, params martini.Params) {
-	ctx.Data["Title"] = "Edit Account"
+	ctx.Data["Title"] = "Delete Account"
 	ctx.Data["PageIsUsers"] = true
 
+	log.Info("delete")
 	uid, err := base.StrTo(params["userid"]).Int()
 	if err != nil {
-		ctx.Handle(200, "admin.user.EditUser", err)
+		ctx.Handle(404, "admin.user.EditUser", err)
 		return
 	}
 
 	u, err := models.GetUserById(int64(uid))
 	if err != nil {
-		ctx.Handle(200, "admin.user.EditUser", err)
+		ctx.Handle(500, "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")
+			ctx.Flash.Error("This account still has ownership of repository, owner has to delete or transfer them first.")
+			ctx.Redirect("/admin/users/" + params["userid"])
 		default:
-			ctx.Handle(200, "admin.user.DeleteUser", err)
+			ctx.Handle(500, "admin.user.DeleteUser", err)
 		}
 		return
 	}
-
 	log.Trace("%s User deleted by admin(%s): %s", ctx.Req.RequestURI,
 		ctx.User.LowerName, ctx.User.LowerName)
 

+ 1 - 1
routers/api/v1/miscellaneous.go

@@ -13,6 +13,6 @@ func Markdown(ctx *middleware.Context) {
 	content := ctx.Query("content")
 	ctx.Render.JSON(200, map[string]interface{}{
 		"ok":      true,
-		"content": string(base.RenderMarkdown([]byte(content), "")),
+		"content": string(base.RenderMarkdown([]byte(content), ctx.Query("repoLink"))),
 	})
 }

+ 6 - 0
routers/dashboard.go

@@ -5,6 +5,7 @@
 package routers
 
 import (
+	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/middleware"
 	"github.com/gogits/gogs/routers/user"
@@ -23,6 +24,11 @@ func Home(ctx *middleware.Context) {
 		return
 	}
 
+	repos, _ := models.GetRecentUpdatedRepositories()
+	for _, repo := range repos {
+		repo.Owner, _ = models.GetUserById(repo.OwnerId)
+	}
+	ctx.Data["Repos"] = repos
 	ctx.Data["PageIsHome"] = true
 	ctx.HTML(200, "home")
 }

+ 64 - 38
routers/install.go

@@ -6,20 +6,23 @@ package routers
 
 import (
 	"errors"
-	"fmt"
 	"os"
+	"os/exec"
 	"strings"
 
 	"github.com/Unknwon/goconfig"
 	"github.com/go-martini/martini"
-	"github.com/lunny/xorm"
+	"github.com/go-xorm/xorm"
+	qlog "github.com/qiniu/log"
 
 	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/auth"
 	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/cron"
 	"github.com/gogits/gogs/modules/log"
 	"github.com/gogits/gogs/modules/mailer"
 	"github.com/gogits/gogs/modules/middleware"
+	"github.com/gogits/gogs/modules/social"
 )
 
 // Check run mode(Default of martini is Dev).
@@ -27,12 +30,18 @@ func checkRunMode() {
 	switch base.Cfg.MustValue("", "RUN_MODE") {
 	case "prod":
 		martini.Env = martini.Prod
+		base.IsProdMode = true
 	case "test":
 		martini.Env = martini.Test
 	}
 	log.Info("Run Mode: %s", strings.Title(martini.Env))
 }
 
+func NewServices() {
+	base.NewBaseServices()
+	social.NewOauthService()
+}
+
 // GlobalInit is for global configuration reload-able.
 func GlobalInit() {
 	base.NewConfigContext()
@@ -40,16 +49,19 @@ func GlobalInit() {
 	models.LoadModelsConfig()
 	models.LoadRepoConfig()
 	models.NewRepoContext()
+	NewServices()
 
 	if base.InstallLock {
 		if err := models.NewEngine(); err != nil {
-			fmt.Println(err)
-			os.Exit(2)
+			qlog.Fatal(err)
 		}
 
 		models.HasEngine = true
+		if models.EnableSQLite3 {
+			log.Info("SQLite3 Enabled")
+		}
+		cron.NewCronContext()
 	}
-	base.NewServices()
 	checkRunMode()
 }
 
@@ -62,47 +74,59 @@ func Install(ctx *middleware.Context, form auth.InstallForm) {
 	ctx.Data["Title"] = "Install"
 	ctx.Data["PageIsInstall"] = true
 
-	if ctx.Req.Method == "GET" {
-		// Get and assign value to install form.
-		if len(form.Host) == 0 {
-			form.Host = models.DbCfg.Host
-		}
-		if len(form.User) == 0 {
-			form.User = models.DbCfg.User
-		}
-		if len(form.Passwd) == 0 {
-			form.Passwd = models.DbCfg.Pwd
-		}
-		if len(form.DatabaseName) == 0 {
-			form.DatabaseName = models.DbCfg.Name
-		}
-		if len(form.DatabasePath) == 0 {
-			form.DatabasePath = models.DbCfg.Path
-		}
+	// Get and assign value to install form.
+	if len(form.Host) == 0 {
+		form.Host = models.DbCfg.Host
+	}
+	if len(form.User) == 0 {
+		form.User = models.DbCfg.User
+	}
+	if len(form.Passwd) == 0 {
+		form.Passwd = models.DbCfg.Pwd
+	}
+	if len(form.DatabaseName) == 0 {
+		form.DatabaseName = models.DbCfg.Name
+	}
+	if len(form.DatabasePath) == 0 {
+		form.DatabasePath = models.DbCfg.Path
+	}
 
-		if len(form.RepoRootPath) == 0 {
-			form.RepoRootPath = base.RepoRootPath
-		}
-		if len(form.RunUser) == 0 {
-			form.RunUser = base.RunUser
-		}
-		if len(form.Domain) == 0 {
-			form.Domain = base.Domain
-		}
-		if len(form.AppUrl) == 0 {
-			form.AppUrl = base.AppUrl
-		}
+	if len(form.RepoRootPath) == 0 {
+		form.RepoRootPath = base.RepoRootPath
+	}
+	if len(form.RunUser) == 0 {
+		form.RunUser = base.RunUser
+	}
+	if len(form.Domain) == 0 {
+		form.Domain = base.Domain
+	}
+	if len(form.AppUrl) == 0 {
+		form.AppUrl = base.AppUrl
+	}
 
-		auth.AssignForm(form, ctx.Data)
-		ctx.HTML(200, "install")
+	auth.AssignForm(form, ctx.Data)
+	ctx.HTML(200, "install")
+}
+
+func InstallPost(ctx *middleware.Context, form auth.InstallForm) {
+	if base.InstallLock {
+		ctx.Handle(404, "install.Install", errors.New("Installation is prohibited"))
 		return
 	}
 
+	ctx.Data["Title"] = "Install"
+	ctx.Data["PageIsInstall"] = true
+
 	if ctx.HasError() {
 		ctx.HTML(200, "install")
 		return
 	}
 
+	if _, err := exec.LookPath("git"); err != nil {
+		ctx.RenderWithErr("Fail to test 'git' command: "+err.Error(), "install", &form)
+		return
+	}
+
 	// Pass basic check, now test configuration.
 	// Test database setting.
 	dbTypes := map[string]string{"mysql": "mysql", "pgsql": "postgres", "sqlite": "sqlite3"}
@@ -133,9 +157,9 @@ func Install(ctx *middleware.Context, form auth.InstallForm) {
 	}
 
 	// Check run user.
-	curUser := os.Getenv("USERNAME")
+	curUser := os.Getenv("USER")
 	if len(curUser) == 0 {
-		curUser = os.Getenv("USER")
+		curUser = os.Getenv("USERNAME")
 	}
 	// Does not check run user when the install lock is off.
 	if form.RunUser != curUser {
@@ -183,6 +207,7 @@ func Install(ctx *middleware.Context, form auth.InstallForm) {
 	if _, err := models.RegisterUser(&models.User{Name: form.AdminName, Email: form.AdminEmail, Passwd: form.AdminPasswd,
 		IsAdmin: true, IsActive: true}); err != nil {
 		if err != models.ErrUserAlreadyExist {
+			base.InstallLock = false
 			ctx.RenderWithErr("Admin account setting is invalid: "+err.Error(), "install", &form)
 			return
 		}
@@ -190,5 +215,6 @@ func Install(ctx *middleware.Context, form auth.InstallForm) {
 	}
 
 	log.Info("First-time run install finished!")
+	ctx.Flash.Success("Welcome! We're glad that you choose Gogs, have fun and take care.")
 	ctx.Redirect("/user/login")
 }

+ 1 - 2
routers/repo/branch.go

@@ -7,12 +7,11 @@ package repo
 import (
 	"github.com/go-martini/martini"
 
-	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/middleware"
 )
 
 func Branches(ctx *middleware.Context, params martini.Params) {
-	brs, err := models.GetBranches(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+	brs, err := ctx.Repo.GitRepo.GetBranches()
 	if err != nil {
 		ctx.Handle(404, "repo.Branches", err)
 		return

+ 66 - 20
routers/repo/commit.go

@@ -5,7 +5,6 @@
 package repo
 
 import (
-	"container/list"
 	"path"
 
 	"github.com/go-martini/martini"
@@ -16,35 +15,51 @@ import (
 )
 
 func Commits(ctx *middleware.Context, params martini.Params) {
-	userName := params["username"]
-	repoName := params["reponame"]
-	branchName := params["branchname"]
+	userName := ctx.Repo.Owner.Name
+	repoName := ctx.Repo.Repository.Name
 
-	brs, err := models.GetBranches(userName, repoName)
+	brs, err := ctx.Repo.GitRepo.GetBranches()
 	if err != nil {
-		ctx.Handle(200, "repo.Commits", err)
+		ctx.Handle(500, "repo.Commits", err)
 		return
 	} else if len(brs) == 0 {
 		ctx.Handle(404, "repo.Commits", nil)
 		return
 	}
 
-	var commits *list.List
-	if models.IsBranchExist(userName, repoName, branchName) {
-		commits, err = models.GetCommitsByBranch(userName, repoName, branchName)
-	} else {
-		commits, err = models.GetCommitsByCommitId(userName, repoName, branchName)
+	commitsCount, err := ctx.Repo.Commit.CommitsCount()
+	if err != nil {
+		ctx.Handle(500, "repo.Commits(GetCommitsCount)", err)
+		return
+	}
+
+	// Calculate and validate page number.
+	page, _ := base.StrTo(ctx.Query("p")).Int()
+	if page < 1 {
+		page = 1
+	}
+	lastPage := page - 1
+	if lastPage < 0 {
+		lastPage = 0
+	}
+	nextPage := page + 1
+	if nextPage*50 > commitsCount {
+		nextPage = 0
 	}
 
+	//both `git log branchName` and `git log  commitId` work
+	commits, err := ctx.Repo.Commit.CommitsByRange(page)
 	if err != nil {
-		ctx.Handle(404, "repo.Commits", err)
+		ctx.Handle(500, "repo.Commits(get commits)", err)
 		return
 	}
 
 	ctx.Data["Username"] = userName
 	ctx.Data["Reponame"] = repoName
-	ctx.Data["CommitCount"] = commits.Len()
+	ctx.Data["CommitCount"] = commitsCount
 	ctx.Data["Commits"] = commits
+	ctx.Data["LastPageNum"] = lastPage
+	ctx.Data["NextPageNum"] = nextPage
 	ctx.Data["IsRepoToolbarCommits"] = true
 	ctx.HTML(200, "repo/commits")
 }
@@ -52,7 +67,6 @@ func Commits(ctx *middleware.Context, params martini.Params) {
 func Diff(ctx *middleware.Context, params martini.Params) {
 	userName := ctx.Repo.Owner.Name
 	repoName := ctx.Repo.Repository.Name
-	branchName := ctx.Repo.BranchName
 	commitId := ctx.Repo.CommitId
 
 	commit := ctx.Repo.Commit
@@ -64,19 +78,15 @@ func Diff(ctx *middleware.Context, params martini.Params) {
 	}
 
 	isImageFile := func(name string) bool {
-		repoFile, err := models.GetTargetFile(userName, repoName,
-			branchName, commitId, name)
-
+		blob, err := ctx.Repo.Commit.GetBlobByPath(name)
 		if err != nil {
 			return false
 		}
 
-		blob, err := repoFile.LookupBlob()
+		data, err := blob.Data()
 		if err != nil {
 			return false
 		}
-
-		data := blob.Contents()
 		_, isImage := base.IsImageFile(data)
 		return isImage
 	}
@@ -85,8 +95,44 @@ func Diff(ctx *middleware.Context, params martini.Params) {
 	ctx.Data["Title"] = commit.Message() + " · " + base.ShortSha(commitId)
 	ctx.Data["Commit"] = commit
 	ctx.Data["Diff"] = diff
+	ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0
 	ctx.Data["IsRepoToolbarCommits"] = true
 	ctx.Data["SourcePath"] = "/" + path.Join(userName, repoName, "src", commitId)
 	ctx.Data["RawPath"] = "/" + path.Join(userName, repoName, "raw", commitId)
 	ctx.HTML(200, "repo/diff")
 }
+
+func SearchCommits(ctx *middleware.Context, params martini.Params) {
+	keyword := ctx.Query("q")
+	if len(keyword) == 0 {
+		ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchName)
+		return
+	}
+
+	userName := params["username"]
+	repoName := params["reponame"]
+
+	brs, err := ctx.Repo.GitRepo.GetBranches()
+	if err != nil {
+		ctx.Handle(500, "repo.SearchCommits(GetBranches)", err)
+		return
+	} else if len(brs) == 0 {
+		ctx.Handle(404, "repo.SearchCommits(GetBranches)", nil)
+		return
+	}
+
+	commits, err := ctx.Repo.Commit.SearchCommits(keyword)
+	if err != nil {
+		ctx.Handle(500, "repo.SearchCommits(SearchCommits)", err)
+		return
+	}
+
+	ctx.Data["Keyword"] = keyword
+	ctx.Data["Username"] = userName
+	ctx.Data["Reponame"] = repoName
+	ctx.Data["CommitCount"] = commits.Len()
+	ctx.Data["Commits"] = commits
+	ctx.Data["IsSearchPage"] = true
+	ctx.Data["IsRepoToolbarCommits"] = true
+	ctx.HTML(200, "repo/commits")
+}

+ 68 - 0
routers/repo/download.go

@@ -0,0 +1,68 @@
+// 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 (
+	"os"
+	"path/filepath"
+
+	"github.com/Unknwon/com"
+	"github.com/go-martini/martini"
+
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/middleware"
+)
+
+func SingleDownload(ctx *middleware.Context, params martini.Params) {
+	// Get tree path
+	treename := params["_1"]
+
+	blob, err := ctx.Repo.Commit.GetBlobByPath(treename)
+	if err != nil {
+		ctx.Handle(404, "repo.SingleDownload(GetBlobByPath)", err)
+		return
+	}
+
+	data, err := blob.Data()
+	if err != nil {
+		ctx.Handle(404, "repo.SingleDownload(Data)", err)
+		return
+	}
+
+	contentType, isTextFile := base.IsTextFile(data)
+	_, isImageFile := base.IsImageFile(data)
+	ctx.Res.Header().Set("Content-Type", contentType)
+	if !isTextFile && !isImageFile {
+		ctx.Res.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(treename))
+		ctx.Res.Header().Set("Content-Transfer-Encoding", "binary")
+	}
+	ctx.Res.Write(data)
+}
+
+func ZipDownload(ctx *middleware.Context, params martini.Params) {
+	commitId := ctx.Repo.CommitId
+	archivesPath := filepath.Join(ctx.Repo.GitRepo.Path, "archives")
+	if !com.IsDir(archivesPath) {
+		if err := os.Mkdir(archivesPath, 0755); err != nil {
+			ctx.Handle(404, "ZipDownload -> os.Mkdir(archivesPath)", err)
+			return
+		}
+	}
+
+	zipPath := filepath.Join(archivesPath, commitId+".zip")
+
+	if com.IsFile(zipPath) {
+		ctx.ServeFile(zipPath, ctx.Repo.Repository.Name+".zip")
+		return
+	}
+
+	err := ctx.Repo.Commit.CreateArchive(zipPath)
+	if err != nil {
+		ctx.Handle(404, "ZipDownload -> CreateArchive "+zipPath, err)
+		return
+	}
+
+	ctx.ServeFile(zipPath, ctx.Repo.Repository.Name+".zip")
+}

+ 55 - 0
routers/repo/git.go

@@ -0,0 +1,55 @@
+package repo
+
+import (
+	"fmt"
+	"strings"
+)
+
+const advertise_refs = "--advertise-refs"
+
+func command(cmd string, opts ...string) string {
+	return fmt.Sprintf("git %s %s", cmd, strings.Join(opts, " "))
+}
+
+/*func upload_pack(repository_path string, opts ...string) string {
+	cmd = "upload-pack"
+	opts = append(opts, "--stateless-rpc", repository_path)
+	return command(cmd, opts...)
+}
+
+func receive_pack(repository_path string, opts ...string) string {
+	cmd = "receive-pack"
+	opts = append(opts, "--stateless-rpc", repository_path)
+	return command(cmd, opts...)
+}*/
+
+/*func update_server_info(repository_path, opts = {}, &block)
+      cmd = "update-server-info"
+      args = []
+      opts.each {|k,v| args << command_options[k] if command_options.has_key?(k) }
+      opts[:args] = args
+      Dir.chdir(repository_path) do # "git update-server-info" does not take a parameter to specify the repository, so set the working directory to the repository
+        self.command(cmd, opts, &block)
+      end
+    end
+
+    def get_config_setting(repository_path, key)
+      path = get_config_location(repository_path)
+      raise "Config file could not be found for repository in #{repository_path}." unless path
+      self.command("config", {:args => ["-f #{path}", key]}).chomp
+    end
+
+    def get_config_location(repository_path)
+      non_bare = File.join(repository_path,'.git') # This is where the config file will be if the repository is non-bare
+      if File.exists?(non_bare) then # The repository is non-bare
+        non_bare_config = File.join(non_bare, 'config')
+        return non_bare_config if File.exists?(non_bare_config)
+      else # We are dealing with a bare repository
+        bare_config = File.join(repository_path, "config")
+        return bare_config if File.exists?(bare_config)
+      end
+      return nil
+    end
+
+  end
+*/

+ 496 - 0
routers/repo/http.go

@@ -0,0 +1,496 @@
+// 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 (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"path"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-martini/martini"
+	"github.com/gogits/gogs/models"
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/middleware"
+)
+
+func Http(ctx *middleware.Context, params martini.Params) {
+	username := params["username"]
+	reponame := params["reponame"]
+	if strings.HasSuffix(reponame, ".git") {
+		reponame = reponame[:len(reponame)-4]
+	}
+
+	var isPull bool
+	service := ctx.Query("service")
+	if service == "git-receive-pack" ||
+		strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
+		isPull = false
+	} else if service == "git-upload-pack" ||
+		strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
+		isPull = true
+	} else {
+		isPull = (ctx.Req.Method == "GET")
+	}
+
+	repoUser, err := models.GetUserByName(username)
+	if err != nil {
+		ctx.Handle(500, "repo.GetUserByName", nil)
+		return
+	}
+
+	repo, err := models.GetRepositoryByName(repoUser.Id, reponame)
+	if err != nil {
+		ctx.Handle(500, "repo.GetRepositoryByName", nil)
+		return
+	}
+
+	// only public pull don't need auth
+	isPublicPull := !repo.IsPrivate && isPull
+	var askAuth = !isPublicPull || base.Service.RequireSignInView
+
+	var authUser *models.User
+
+	// check access
+	if askAuth {
+		baHead := ctx.Req.Header.Get("Authorization")
+		if baHead == "" {
+			// ask auth
+			authRequired(ctx)
+			return
+		}
+
+		auths := strings.Fields(baHead)
+		// currently check basic auth
+		// TODO: support digit auth
+		if len(auths) != 2 || auths[0] != "Basic" {
+			ctx.Handle(401, "no basic auth and digit auth", nil)
+			return
+		}
+		authUsername, passwd, err := basicDecode(auths[1])
+		if err != nil {
+			ctx.Handle(401, "no basic auth and digit auth", nil)
+			return
+		}
+
+		authUser, err = models.GetUserByName(authUsername)
+		if err != nil {
+			ctx.Handle(401, "no basic auth and digit auth", nil)
+			return
+		}
+
+		newUser := &models.User{Passwd: passwd, Salt: authUser.Salt}
+		newUser.EncodePasswd()
+		if authUser.Passwd != newUser.Passwd {
+			ctx.Handle(401, "no basic auth and digit auth", nil)
+			return
+		}
+
+		if !isPublicPull {
+			var tp = models.AU_WRITABLE
+			if isPull {
+				tp = models.AU_READABLE
+			}
+
+			has, err := models.HasAccess(authUsername, username+"/"+reponame, tp)
+			if err != nil {
+				ctx.Handle(401, "no basic auth and digit auth", nil)
+				return
+			} else if !has {
+				if tp == models.AU_READABLE {
+					has, err = models.HasAccess(authUsername, username+"/"+reponame, models.AU_WRITABLE)
+					if err != nil || !has {
+						ctx.Handle(401, "no basic auth and digit auth", nil)
+						return
+					}
+				} else {
+					ctx.Handle(401, "no basic auth and digit auth", nil)
+					return
+				}
+			}
+		}
+	}
+
+	config := Config{base.RepoRootPath, "git", true, true, func(rpc string, input []byte) {
+		if rpc == "receive-pack" {
+			firstLine := bytes.IndexRune(input, '\000')
+			if firstLine > -1 {
+				fields := strings.Fields(string(input[:firstLine]))
+				if len(fields) == 3 {
+					oldCommitId := fields[0][4:]
+					newCommitId := fields[1]
+					refName := fields[2]
+
+					models.Update(refName, oldCommitId, newCommitId, username, reponame, authUser.Id)
+				}
+			}
+		}
+	}}
+
+	handler := HttpBackend(&config)
+	handler(ctx.ResponseWriter, ctx.Req)
+
+	/* Webdav
+	dir := models.RepoPath(username, reponame)
+
+	prefix := path.Join("/", username, params["reponame"])
+	server := webdav.NewServer(
+		dir, prefix, true)
+
+	server.ServeHTTP(ctx.ResponseWriter, ctx.Req)
+	*/
+}
+
+type route struct {
+	cr      *regexp.Regexp
+	method  string
+	handler func(handler)
+}
+
+type Config struct {
+	ReposRoot   string
+	GitBinPath  string
+	UploadPack  bool
+	ReceivePack bool
+	OnSucceed   func(rpc string, input []byte)
+}
+
+type handler struct {
+	*Config
+	w    http.ResponseWriter
+	r    *http.Request
+	Dir  string
+	File string
+}
+
+var routes = []route{
+	{regexp.MustCompile("(.*?)/git-upload-pack$"), "POST", serviceUploadPack},
+	{regexp.MustCompile("(.*?)/git-receive-pack$"), "POST", serviceReceivePack},
+	{regexp.MustCompile("(.*?)/info/refs$"), "GET", getInfoRefs},
+	{regexp.MustCompile("(.*?)/HEAD$"), "GET", getTextFile},
+	{regexp.MustCompile("(.*?)/objects/info/alternates$"), "GET", getTextFile},
+	{regexp.MustCompile("(.*?)/objects/info/http-alternates$"), "GET", getTextFile},
+	{regexp.MustCompile("(.*?)/objects/info/packs$"), "GET", getInfoPacks},
+	{regexp.MustCompile("(.*?)/objects/info/[^/]*$"), "GET", getTextFile},
+	{regexp.MustCompile("(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"), "GET", getLooseObject},
+	{regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"), "GET", getPackFile},
+	{regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"), "GET", getIdxFile},
+}
+
+// Request handling function
+func HttpBackend(config *Config) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		//log.Printf("%s %s %s %s", r.RemoteAddr, r.Method, r.URL.Path, r.Proto)
+		for _, route := range routes {
+			if m := route.cr.FindStringSubmatch(r.URL.Path); m != nil {
+				if route.method != r.Method {
+					renderMethodNotAllowed(w, r)
+					return
+				}
+
+				file := strings.Replace(r.URL.Path, m[1]+"/", "", 1)
+				dir, err := getGitDir(config, m[1])
+
+				if err != nil {
+					log.Print(err)
+					renderNotFound(w)
+					return
+				}
+
+				hr := handler{config, w, r, dir, file}
+				route.handler(hr)
+				return
+			}
+		}
+		renderNotFound(w)
+		return
+	}
+}
+
+// Actual command handling functions
+
+func serviceUploadPack(hr handler) {
+	serviceRpc("upload-pack", hr)
+}
+
+func serviceReceivePack(hr handler) {
+	serviceRpc("receive-pack", hr)
+}
+
+func serviceRpc(rpc string, hr handler) {
+	w, r, dir := hr.w, hr.r, hr.Dir
+	access := hasAccess(r, hr.Config, dir, rpc, true)
+
+	if access == false {
+		renderNoAccess(w)
+		return
+	}
+
+	input, _ := ioutil.ReadAll(r.Body)
+
+	w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", rpc))
+	w.WriteHeader(http.StatusOK)
+
+	args := []string{rpc, "--stateless-rpc", dir}
+	cmd := exec.Command(hr.Config.GitBinPath, args...)
+	cmd.Dir = dir
+	in, err := cmd.StdinPipe()
+	if err != nil {
+		log.Print(err)
+		return
+	}
+
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		log.Print(err)
+		return
+	}
+
+	err = cmd.Start()
+	if err != nil {
+		log.Print(err)
+		return
+	}
+
+	in.Write(input)
+	io.Copy(w, stdout)
+	cmd.Wait()
+
+	if hr.Config.OnSucceed != nil {
+		hr.Config.OnSucceed(rpc, input)
+	}
+}
+
+func getInfoRefs(hr handler) {
+	w, r, dir := hr.w, hr.r, hr.Dir
+	serviceName := getServiceType(r)
+	access := hasAccess(r, hr.Config, dir, serviceName, false)
+
+	if access {
+		args := []string{serviceName, "--stateless-rpc", "--advertise-refs", "."}
+		refs := gitCommand(hr.Config.GitBinPath, dir, args...)
+
+		hdrNocache(w)
+		w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", serviceName))
+		w.WriteHeader(http.StatusOK)
+		w.Write(packetWrite("# service=git-" + serviceName + "\n"))
+		w.Write(packetFlush())
+		w.Write(refs)
+	} else {
+		updateServerInfo(hr.Config.GitBinPath, dir)
+		hdrNocache(w)
+		sendFile("text/plain; charset=utf-8", hr)
+	}
+}
+
+func getInfoPacks(hr handler) {
+	hdrCacheForever(hr.w)
+	sendFile("text/plain; charset=utf-8", hr)
+}
+
+func getLooseObject(hr handler) {
+	hdrCacheForever(hr.w)
+	sendFile("application/x-git-loose-object", hr)
+}
+
+func getPackFile(hr handler) {
+	hdrCacheForever(hr.w)
+	sendFile("application/x-git-packed-objects", hr)
+}
+
+func getIdxFile(hr handler) {
+	hdrCacheForever(hr.w)
+	sendFile("application/x-git-packed-objects-toc", hr)
+}
+
+func getTextFile(hr handler) {
+	hdrNocache(hr.w)
+	sendFile("text/plain", hr)
+}
+
+// Logic helping functions
+
+func sendFile(contentType string, hr handler) {
+	w, r := hr.w, hr.r
+	reqFile := path.Join(hr.Dir, hr.File)
+
+	//fmt.Println("sendFile:", reqFile)
+
+	f, err := os.Stat(reqFile)
+	if os.IsNotExist(err) {
+		renderNotFound(w)
+		return
+	}
+
+	w.Header().Set("Content-Type", contentType)
+	w.Header().Set("Content-Length", fmt.Sprintf("%d", f.Size()))
+	w.Header().Set("Last-Modified", f.ModTime().Format(http.TimeFormat))
+	http.ServeFile(w, r, reqFile)
+}
+
+func getGitDir(config *Config, fPath string) (string, error) {
+	root := config.ReposRoot
+
+	if root == "" {
+		cwd, err := os.Getwd()
+
+		if err != nil {
+			log.Print(err)
+			return "", err
+		}
+
+		root = cwd
+	}
+
+	if !strings.HasSuffix(fPath, ".git") {
+		fPath = fPath + ".git"
+	}
+
+	f := filepath.Join(root, fPath)
+	if _, err := os.Stat(f); os.IsNotExist(err) {
+		return "", err
+	}
+
+	return f, nil
+}
+
+func getServiceType(r *http.Request) string {
+	serviceType := r.FormValue("service")
+
+	if s := strings.HasPrefix(serviceType, "git-"); !s {
+		return ""
+	}
+
+	return strings.Replace(serviceType, "git-", "", 1)
+}
+
+func hasAccess(r *http.Request, config *Config, dir string, rpc string, checkContentType bool) bool {
+	if checkContentType {
+		if r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", rpc) {
+			return false
+		}
+	}
+
+	if !(rpc == "upload-pack" || rpc == "receive-pack") {
+		return false
+	}
+	if rpc == "receive-pack" {
+		return config.ReceivePack
+	}
+	if rpc == "upload-pack" {
+		return config.UploadPack
+	}
+
+	return getConfigSetting(config.GitBinPath, rpc, dir)
+}
+
+func getConfigSetting(gitBinPath, serviceName string, dir string) bool {
+	serviceName = strings.Replace(serviceName, "-", "", -1)
+	setting := getGitConfig(gitBinPath, "http."+serviceName, dir)
+
+	if serviceName == "uploadpack" {
+		return setting != "false"
+	}
+
+	return setting == "true"
+}
+
+func getGitConfig(gitBinPath, configName string, dir string) string {
+	args := []string{"config", configName}
+	out := string(gitCommand(gitBinPath, dir, args...))
+	return out[0 : len(out)-1]
+}
+
+func updateServerInfo(gitBinPath, dir string) []byte {
+	args := []string{"update-server-info"}
+	return gitCommand(gitBinPath, dir, args...)
+}
+
+func gitCommand(gitBinPath, dir string, args ...string) []byte {
+	command := exec.Command(gitBinPath, args...)
+	command.Dir = dir
+	out, err := command.Output()
+
+	if err != nil {
+		log.Print(err)
+	}
+
+	return out
+}
+
+// HTTP error response handling functions
+
+func renderMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
+	if r.Proto == "HTTP/1.1" {
+		w.WriteHeader(http.StatusMethodNotAllowed)
+		w.Write([]byte("Method Not Allowed"))
+	} else {
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write([]byte("Bad Request"))
+	}
+}
+
+func renderNotFound(w http.ResponseWriter) {
+	w.WriteHeader(http.StatusNotFound)
+	w.Write([]byte("Not Found"))
+}
+
+func renderNoAccess(w http.ResponseWriter) {
+	w.WriteHeader(http.StatusForbidden)
+	w.Write([]byte("Forbidden"))
+}
+
+// Packet-line handling function
+
+func packetFlush() []byte {
+	return []byte("0000")
+}
+
+func packetWrite(str string) []byte {
+	s := strconv.FormatInt(int64(len(str)+4), 16)
+
+	if len(s)%4 != 0 {
+		s = strings.Repeat("0", 4-len(s)%4) + s
+	}
+
+	return []byte(s + str)
+}
+
+// Header writing functions
+
+func hdrNocache(w http.ResponseWriter) {
+	w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
+	w.Header().Set("Pragma", "no-cache")
+	w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
+}
+
+func hdrCacheForever(w http.ResponseWriter) {
+	now := time.Now().Unix()
+	expires := now + 31536000
+	w.Header().Set("Date", fmt.Sprintf("%d", now))
+	w.Header().Set("Expires", fmt.Sprintf("%d", expires))
+	w.Header().Set("Cache-Control", "public, max-age=31536000")
+}
+
+// Main
+/*
+func main() {
+	http.HandleFunc("/", requestHandler())
+
+	err := http.ListenAndServe(":8080", nil)
+	if err != nil {
+		log.Fatal("ListenAndServe: ", err)
+	}
+}*/

+ 35 - 15
routers/repo/issue.go

@@ -9,6 +9,7 @@ import (
 	"net/url"
 	"strings"
 
+	"github.com/Unknwon/com"
 	"github.com/go-martini/martini"
 
 	"github.com/gogits/gogs/models"
@@ -81,15 +82,17 @@ func Issues(ctx *middleware.Context) {
 	ctx.HTML(200, "issue/list")
 }
 
-func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) {
+func CreateIssue(ctx *middleware.Context, params martini.Params) {
 	ctx.Data["Title"] = "Create issue"
 	ctx.Data["IsRepoToolbarIssues"] = true
 	ctx.Data["IsRepoToolbarIssuesList"] = false
+	ctx.HTML(200, "issue/create")
+}
 
-	if ctx.Req.Method == "GET" {
-		ctx.HTML(200, "issue/create")
-		return
-	}
+func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) {
+	ctx.Data["Title"] = "Create issue"
+	ctx.Data["IsRepoToolbarIssues"] = true
+	ctx.Data["IsRepoToolbarIssuesList"] = false
 
 	if ctx.HasError() {
 		ctx.HTML(200, "issue/create")
@@ -99,7 +102,7 @@ func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat
 	issue, err := models.CreateIssue(ctx.User.Id, ctx.Repo.Repository.Id, form.MilestoneId, form.AssigneeId,
 		ctx.Repo.Repository.NumIssues, form.IssueName, form.Labels, form.Content, false)
 	if err != nil {
-		ctx.Handle(200, "issue.CreateIssue", err)
+		ctx.Handle(500, "issue.CreateIssue(CreateIssue)", err)
 		return
 	}
 
@@ -107,19 +110,36 @@ func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat
 	if err = models.NotifyWatchers(&models.Action{ActUserId: ctx.User.Id, ActUserName: ctx.User.Name, ActEmail: ctx.User.Email,
 		OpType: models.OP_CREATE_ISSUE, Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name),
 		RepoId: ctx.Repo.Repository.Id, RepoName: ctx.Repo.Repository.Name, RefName: ""}); err != nil {
-		ctx.Handle(200, "issue.CreateIssue", err)
+		ctx.Handle(500, "issue.CreateIssue(NotifyWatchers)", err)
 		return
 	}
 
-	// Mail watchers.
+	// Mail watchers and mentions.
 	if base.Service.NotifyMail {
-		if err = mailer.SendNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue); err != nil {
-			ctx.Handle(200, "issue.CreateIssue", err)
+		tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
+		if err != nil {
+			ctx.Handle(500, "issue.CreateIssue(SendIssueNotifyMail)", err)
 			return
 		}
-	}
 
+		tos = append(tos, ctx.User.LowerName)
+		ms := base.MentionPattern.FindAllString(issue.Content, -1)
+		newTos := make([]string, 0, len(ms))
+		for _, m := range ms {
+			if com.IsSliceContainsStr(tos, m[1:]) {
+				continue
+			}
+
+			newTos = append(newTos, m[1:])
+		}
+		if err = mailer.SendIssueMentionMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository,
+			issue, models.GetUserEmailsByNames(newTos)); err != nil {
+			ctx.Handle(500, "issue.CreateIssue(SendIssueMentionMail)", err)
+			return
+		}
+	}
 	log.Trace("%d Issue created: %d", ctx.Repo.Repository.Id, issue.Id)
+
 	ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index))
 }
 
@@ -147,7 +167,7 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) {
 		return
 	}
 	issue.Poster = u
-	issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ""))
+	issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink))
 
 	// Get comments.
 	comments, err := models.GetIssueComments(issue.Id)
@@ -164,7 +184,7 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) {
 			return
 		}
 		comments[i].Poster = u
-		comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ""))
+		comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink))
 	}
 
 	ctx.Data["Title"] = issue.Name
@@ -193,7 +213,7 @@ func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat
 		return
 	}
 
-	if ctx.User.Id != issue.PosterId {
+	if ctx.User.Id != issue.PosterId && !ctx.Repo.IsOwner {
 		ctx.Handle(404, "issue.UpdateIssue", nil)
 		return
 	}
@@ -211,7 +231,7 @@ func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat
 	ctx.JSON(200, map[string]interface{}{
 		"ok":      true,
 		"title":   issue.Name,
-		"content": string(base.RenderMarkdown([]byte(issue.Content), "")),
+		"content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)),
 	})
 }
 

+ 137 - 3
routers/repo/release.go

@@ -5,18 +5,152 @@
 package repo
 
 import (
+	"sort"
+
 	"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"
 )
 
+type ReleaseSorter struct {
+	rels []*models.Release
+}
+
+func (rs *ReleaseSorter) Len() int {
+	return len(rs.rels)
+}
+
+func (rs *ReleaseSorter) Less(i, j int) bool {
+	return rs.rels[i].NumCommits > rs.rels[j].NumCommits
+}
+
+func (rs *ReleaseSorter) Swap(i, j int) {
+	rs.rels[i], rs.rels[j] = rs.rels[j], rs.rels[i]
+}
+
 func Releases(ctx *middleware.Context) {
 	ctx.Data["Title"] = "Releases"
 	ctx.Data["IsRepoToolbarReleases"] = true
-	tags, err := models.GetTags(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+	ctx.Data["IsRepoReleaseNew"] = false
+	rawTags, err := ctx.Repo.GitRepo.GetTags()
+	if err != nil {
+		ctx.Handle(500, "release.Releases(GetTags)", err)
+		return
+	}
+
+	rels, err := models.GetReleasesByRepoId(ctx.Repo.Repository.Id)
+	if err != nil {
+		ctx.Handle(500, "release.Releases(GetReleasesByRepoId)", err)
+		return
+	}
+
+	commitsCount, err := ctx.Repo.Commit.CommitsCount()
 	if err != nil {
-		ctx.Handle(404, "repo.Releases(GetTags)", err)
+		ctx.Handle(500, "release.Releases(CommitsCount)", err)
 		return
 	}
-	ctx.Data["Releases"] = tags
+
+	var tags ReleaseSorter
+	tags.rels = make([]*models.Release, len(rawTags))
+	for i, rawTag := range rawTags {
+		for _, rel := range rels {
+			if rel.TagName == rawTag {
+				rel.Publisher, err = models.GetUserById(rel.PublisherId)
+				if err != nil {
+					ctx.Handle(500, "release.Releases(GetUserById)", err)
+					return
+				}
+				rel.NumCommitsBehind = commitsCount - rel.NumCommits
+				rel.Note = base.RenderMarkdownString(rel.Note, ctx.Repo.RepoLink)
+				tags.rels[i] = rel
+				break
+			}
+		}
+
+		if tags.rels[i] == nil {
+			commit, err := ctx.Repo.GitRepo.GetCommitOfTag(rawTag)
+			if err != nil {
+				ctx.Handle(500, "release.Releases(GetCommitOfTag)", err)
+				return
+			}
+
+			tags.rels[i] = &models.Release{
+				Title:   rawTag,
+				TagName: rawTag,
+				SHA1:    commit.Id.String(),
+			}
+			tags.rels[i].NumCommits, err = ctx.Repo.GitRepo.CommitsCount(commit.Id.String())
+			if err != nil {
+				ctx.Handle(500, "release.Releases(CommitsCount)", err)
+				return
+			}
+			tags.rels[i].NumCommitsBehind = commitsCount - tags.rels[i].NumCommits
+			tags.rels[i].Created = commit.Author.When
+		}
+	}
+
+	sort.Sort(&tags)
+
+	ctx.Data["Releases"] = tags.rels
 	ctx.HTML(200, "release/list")
 }
+
+func ReleasesNew(ctx *middleware.Context) {
+	if !ctx.Repo.IsOwner {
+		ctx.Handle(404, "release.ReleasesNew", nil)
+		return
+	}
+
+	ctx.Data["Title"] = "New Release"
+	ctx.Data["IsRepoToolbarReleases"] = true
+	ctx.Data["IsRepoReleaseNew"] = true
+	ctx.HTML(200, "release/new")
+}
+
+func ReleasesNewPost(ctx *middleware.Context, form auth.NewReleaseForm) {
+	if !ctx.Repo.IsOwner {
+		ctx.Handle(404, "release.ReleasesNew", nil)
+		return
+	}
+
+	ctx.Data["Title"] = "New Release"
+	ctx.Data["IsRepoToolbarReleases"] = true
+	ctx.Data["IsRepoReleaseNew"] = true
+
+	if ctx.HasError() {
+		ctx.HTML(200, "release/new")
+		return
+	}
+
+	commitsCount, err := ctx.Repo.Commit.CommitsCount()
+	if err != nil {
+		ctx.Handle(500, "release.ReleasesNewPost(CommitsCount)", err)
+		return
+	}
+
+	rel := &models.Release{
+		RepoId:       ctx.Repo.Repository.Id,
+		PublisherId:  ctx.User.Id,
+		Title:        form.Title,
+		TagName:      form.TagName,
+		SHA1:         ctx.Repo.Commit.Id.String(),
+		NumCommits:   commitsCount,
+		Note:         form.Content,
+		IsPrerelease: form.Prerelease,
+	}
+
+	if err = models.CreateRelease(models.RepoPath(ctx.User.Name, ctx.Repo.Repository.Name),
+		rel, ctx.Repo.GitRepo); err != nil {
+		if err == models.ErrReleaseAlreadyExist {
+			ctx.RenderWithErr("Release with this tag name has already existed", "release/new", &form)
+		} else {
+			ctx.Handle(500, "release.ReleasesNewPost(IsReleaseExist)", err)
+		}
+		return
+	}
+	log.Trace("%s Release created: %s/%s:%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.Name, form.TagName)
+
+	ctx.Redirect(ctx.Repo.RepoLink + "/releases")
+}

+ 151 - 104
routers/repo/repo.go

@@ -5,15 +5,16 @@
 package repo
 
 import (
+	"encoding/base64"
+	"errors"
 	"fmt"
+	"github.com/gogits/git"
 	"path"
 	"path/filepath"
 	"strings"
 
 	"github.com/go-martini/martini"
 
-	"github.com/gogits/webdav"
-
 	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/auth"
 	"github.com/gogits/gogs/modules/base"
@@ -21,24 +22,27 @@ import (
 	"github.com/gogits/gogs/modules/middleware"
 )
 
-func Create(ctx *middleware.Context, form auth.CreateRepoForm) {
+func Create(ctx *middleware.Context) {
 	ctx.Data["Title"] = "Create repository"
-	ctx.Data["PageIsNewRepo"] = true // For navbar arrow.
+	ctx.Data["PageIsNewRepo"] = true
 	ctx.Data["LanguageIgns"] = models.LanguageIgns
 	ctx.Data["Licenses"] = models.Licenses
+	ctx.HTML(200, "repo/create")
+}
 
-	if ctx.Req.Method == "GET" {
-		ctx.HTML(200, "repo/create")
-		return
-	}
+func CreatePost(ctx *middleware.Context, form auth.CreateRepoForm) {
+	ctx.Data["Title"] = "Create repository"
+	ctx.Data["PageIsNewRepo"] = true
+	ctx.Data["LanguageIgns"] = models.LanguageIgns
+	ctx.Data["Licenses"] = models.Licenses
 
 	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")
+	repo, err := models.CreateRepository(ctx.User, form.RepoName, form.Description,
+		form.Language, form.License, form.Private, false, form.InitReadme)
 	if err == nil {
 		log.Trace("%s Repository created: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, form.RepoName)
 		ctx.Redirect("/" + ctx.User.Name + "/" + form.RepoName)
@@ -50,12 +54,60 @@ func Create(ctx *middleware.Context, form auth.CreateRepoForm) {
 		ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "repo/create", &form)
 		return
 	}
-	ctx.Handle(200, "repo.Create", err)
+
+	if repo != nil {
+		if errDelete := models.DeleteRepository(ctx.User.Id, repo.Id, ctx.User.Name); errDelete != nil {
+			log.Error("repo.MigratePost(CreatePost): %v", errDelete)
+		}
+	}
+	ctx.Handle(500, "repo.Create", err)
+}
+
+func Migrate(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Migrate repository"
+	ctx.Data["PageIsNewRepo"] = true
+	ctx.HTML(200, "repo/migrate")
+}
+
+func MigratePost(ctx *middleware.Context, form auth.MigrateRepoForm) {
+	ctx.Data["Title"] = "Migrate repository"
+	ctx.Data["PageIsNewRepo"] = true
+
+	if ctx.HasError() {
+		ctx.HTML(200, "repo/migrate")
+		return
+	}
+
+	url := strings.Replace(form.Url, "://", fmt.Sprintf("://%s:%s@", form.AuthUserName, form.AuthPasswd), 1)
+	repo, err := models.MigrateRepository(ctx.User, form.RepoName, form.Description, form.Private,
+		form.Mirror, url)
+	if err == nil {
+		log.Trace("%s Repository migrated: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, form.RepoName)
+		ctx.Redirect("/" + ctx.User.Name + "/" + form.RepoName)
+		return
+	} else if err == models.ErrRepoAlreadyExist {
+		ctx.RenderWithErr("Repository name has already been used", "repo/migrate", &form)
+		return
+	} else if err == models.ErrRepoNameIllegal {
+		ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "repo/migrate", &form)
+		return
+	}
+
+	if repo != nil {
+		if errDelete := models.DeleteRepository(ctx.User.Id, repo.Id, ctx.User.Name); errDelete != nil {
+			log.Error("repo.MigratePost(DeleteRepository): %v", errDelete)
+		}
+	}
+
+	if strings.Contains(err.Error(), "Authentication failed") {
+		ctx.RenderWithErr(err.Error(), "repo/migrate", &form)
+		return
+	}
+	ctx.Handle(500, "repo.Migrate", err)
 }
 
 func Single(ctx *middleware.Context, params martini.Params) {
 	branchName := ctx.Repo.BranchName
-	commitId := ctx.Repo.CommitId
 	userName := ctx.Repo.Owner.Name
 	repoName := ctx.Repo.Repository.Name
 
@@ -73,46 +125,42 @@ func Single(ctx *middleware.Context, params martini.Params) {
 
 	ctx.Data["IsRepoToolbarSource"] = true
 
-	// Branches.
-	brs, err := models.GetBranches(userName, repoName)
-	if err != nil {
-		ctx.Handle(404, "repo.Single(GetBranches)", err)
-		return
-	}
-
-	ctx.Data["Branches"] = brs
-
 	isViewBranch := ctx.Repo.IsBranch
 	ctx.Data["IsViewBranch"] = isViewBranch
 
-	repoFile, err := models.GetTargetFile(userName, repoName,
-		branchName, commitId, treename)
+	treePath := treename
+	if len(treePath) != 0 {
+		treePath = treePath + "/"
+	}
+
+	entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treename)
 
-	if err != nil && err != models.ErrRepoFileNotExist {
-		ctx.Handle(404, "repo.Single(GetTargetFile)", err)
+	if err != nil && err != git.ErrNotExist {
+		ctx.Handle(404, "repo.Single(GetTreeEntryByPath)", err)
 		return
 	}
 
-	if len(treename) != 0 && repoFile == nil {
+	if len(treename) != 0 && entry == nil {
 		ctx.Handle(404, "repo.Single", nil)
 		return
 	}
 
-	if repoFile != nil && repoFile.IsFile() {
-		if blob, err := repoFile.LookupBlob(); err != nil {
-			ctx.Handle(404, "repo.Single(repoFile.LookupBlob)", err)
+	if entry != nil && !entry.IsDir() {
+		blob := entry.Blob()
+
+		if data, err := blob.Data(); err != nil {
+			ctx.Handle(404, "repo.Single(blob.Data)", err)
 		} else {
-			ctx.Data["FileSize"] = repoFile.Size
+			ctx.Data["FileSize"] = blob.Size()
 			ctx.Data["IsFile"] = true
-			ctx.Data["FileName"] = repoFile.Name
-			ext := path.Ext(repoFile.Name)
+			ctx.Data["FileName"] = blob.Name()
+			ext := path.Ext(blob.Name())
 			if len(ext) > 0 {
 				ext = ext[1:]
 			}
 			ctx.Data["FileExt"] = ext
 			ctx.Data["FileLink"] = rawLink + "/" + treename
 
-			data := blob.Contents()
 			_, isTextFile := base.IsTextFile(data)
 			_, isImageFile := base.IsImageFile(data)
 			ctx.Data["FileIsText"] = isTextFile
@@ -120,7 +168,7 @@ func Single(ctx *middleware.Context, params martini.Params) {
 			if isImageFile {
 				ctx.Data["IsImageFile"] = true
 			} else {
-				readmeExist := base.IsMarkdownFile(repoFile.Name) || base.IsReadmeFile(repoFile.Name)
+				readmeExist := base.IsMarkdownFile(blob.Name()) || base.IsReadmeFile(blob.Name())
 				ctx.Data["ReadmeExist"] = readmeExist
 				if readmeExist {
 					ctx.Data["FileContent"] = string(base.RenderMarkdown(data, ""))
@@ -134,21 +182,35 @@ func Single(ctx *middleware.Context, params martini.Params) {
 
 	} else {
 		// Directory and file list.
-		files, err := models.GetReposFiles(userName, repoName, ctx.Repo.CommitId, treename)
+		tree, err := ctx.Repo.Commit.SubTree(treename)
 		if err != nil {
-			ctx.Handle(404, "repo.Single(GetReposFiles)", err)
+			ctx.Handle(404, "repo.Single(SubTree)", err)
 			return
 		}
+		entries := tree.ListEntries()
+		entries.Sort()
+
+		files := make([][]interface{}, 0, len(entries))
+
+		for _, te := range entries {
+			c, err := ctx.Repo.Commit.GetCommitOfRelPath(filepath.Join(treePath, te.Name()))
+			if err != nil {
+				ctx.Handle(404, "repo.Single(SubTree)", err)
+				return
+			}
+
+			files = append(files, []interface{}{te, c})
+		}
 
 		ctx.Data["Files"] = files
 
-		var readmeFile *models.RepoFile
+		var readmeFile *git.Blob
 
-		for _, f := range files {
-			if !f.IsFile() || !base.IsReadmeFile(f.Name) {
+		for _, f := range entries {
+			if f.IsDir() || !base.IsReadmeFile(f.Name()) {
 				continue
 			} else {
-				readmeFile = f
+				readmeFile = f.Blob()
 				break
 			}
 		}
@@ -156,16 +218,15 @@ func Single(ctx *middleware.Context, params martini.Params) {
 		if readmeFile != nil {
 			ctx.Data["ReadmeInSingle"] = true
 			ctx.Data["ReadmeExist"] = true
-			if blob, err := readmeFile.LookupBlob(); err != nil {
+			if data, err := readmeFile.Data(); err != nil {
 				ctx.Handle(404, "repo.Single(readmeFile.LookupBlob)", err)
 				return
 			} else {
 				ctx.Data["FileSize"] = readmeFile.Size
 				ctx.Data["FileLink"] = rawLink + "/" + treename
-				data := blob.Contents()
 				_, isTextFile := base.IsTextFile(data)
 				ctx.Data["FileIsText"] = isTextFile
-				ctx.Data["FileName"] = readmeFile.Name
+				ctx.Data["FileName"] = readmeFile.Name()
 				if isTextFile {
 					ctx.Data["FileContent"] = string(base.RenderMarkdown(data, branchLink))
 				}
@@ -194,64 +255,36 @@ func Single(ctx *middleware.Context, params martini.Params) {
 	ctx.Data["LastCommit"] = ctx.Repo.Commit
 	ctx.Data["Paths"] = Paths
 	ctx.Data["Treenames"] = treenames
+	ctx.Data["TreePath"] = treePath
 	ctx.Data["BranchLink"] = branchLink
 	ctx.HTML(200, "repo/single")
 }
 
-func SingleDownload(ctx *middleware.Context, params martini.Params) {
-	// Get tree path
-	treename := params["_1"]
-
-	branchName := params["branchname"]
-	userName := params["username"]
-	repoName := params["reponame"]
-
-	var commitId string
-	if !models.IsBranchExist(userName, repoName, branchName) {
-		commitId = branchName
-		branchName = ""
-	}
-
-	repoFile, err := models.GetTargetFile(userName, repoName,
-		branchName, commitId, treename)
-
-	if err != nil {
-		ctx.Handle(404, "repo.SingleDownload(GetTargetFile)", err)
-		return
-	}
+func basicEncode(username, password string) string {
+	auth := username + ":" + password
+	return base64.StdEncoding.EncodeToString([]byte(auth))
+}
 
-	blob, err := repoFile.LookupBlob()
+func basicDecode(encoded string) (user string, name string, err error) {
+	var s []byte
+	s, err = base64.StdEncoding.DecodeString(encoded)
 	if err != nil {
-		ctx.Handle(404, "repo.SingleDownload(LookupBlob)", err)
 		return
 	}
 
-	data := blob.Contents()
-	contentType, isTextFile := base.IsTextFile(data)
-	_, isImageFile := base.IsImageFile(data)
-	ctx.Res.Header().Set("Content-Type", contentType)
-	if !isTextFile && !isImageFile {
-		ctx.Res.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(treename))
-		ctx.Res.Header().Set("Content-Transfer-Encoding", "binary")
+	a := strings.Split(string(s), ":")
+	if len(a) == 2 {
+		user, name = a[0], a[1]
+	} else {
+		err = errors.New("decode failed")
 	}
-	ctx.Res.Write(data)
+	return
 }
 
-func Http(ctx *middleware.Context, params martini.Params) {
-	// TODO: access check
-
-	username := params["username"]
-	reponame := params["reponame"]
-	if strings.HasSuffix(reponame, ".git") {
-		reponame = reponame[:len(reponame)-4]
-	}
-
-	dir := models.RepoPath(username, reponame)
-	prefix := path.Join("/", username, params["reponame"])
-	server := webdav.NewServer(
-		dir, prefix, true)
-
-	server.ServeHTTP(ctx.ResponseWriter, ctx.Req)
+func authRequired(ctx *middleware.Context) {
+	ctx.ResponseWriter.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
+	ctx.Data["ErrorMsg"] = "no basic auth and digit auth"
+	ctx.HTML(401, fmt.Sprintf("status/401"))
 }
 
 func Setting(ctx *middleware.Context, params martini.Params) {
@@ -277,43 +310,58 @@ func SettingPost(ctx *middleware.Context) {
 		return
 	}
 
+	ctx.Data["IsRepoToolbarSetting"] = true
+
 	switch ctx.Query("action") {
 	case "update":
-		isNameChanged := false
 		newRepoName := ctx.Query("name")
 		// Check if repository name has been changed.
 		if ctx.Repo.Repository.Name != newRepoName {
 			isExist, err := models.IsRepositoryExist(ctx.Repo.Owner, newRepoName)
 			if err != nil {
-				ctx.Handle(404, "repo.SettingPost(update: check existence)", err)
+				ctx.Handle(500, "repo.SettingPost(update: check existence)", err)
 				return
 			} else if isExist {
 				ctx.RenderWithErr("Repository name has been taken in your repositories.", "repo/setting", nil)
 				return
 			} else if err = models.ChangeRepositoryName(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName); err != nil {
-				ctx.Handle(404, "repo.SettingPost(change repository name)", err)
+				ctx.Handle(500, "repo.SettingPost(change repository name)", err)
 				return
 			}
 			log.Trace("%s Repository name changed: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newRepoName)
 
-			isNameChanged = true
 			ctx.Repo.Repository.Name = newRepoName
 		}
 
+		br := ctx.Query("branch")
+
+		if git.IsBranchExist(models.RepoPath(ctx.User.Name, ctx.Repo.Repository.Name), br) {
+			ctx.Repo.Repository.DefaultBranch = br
+		}
 		ctx.Repo.Repository.Description = ctx.Query("desc")
 		ctx.Repo.Repository.Website = ctx.Query("site")
+		ctx.Repo.Repository.IsPrivate = ctx.Query("private") == "on"
+		ctx.Repo.Repository.IsGoget = ctx.Query("goget") == "on"
 		if err := models.UpdateRepository(ctx.Repo.Repository); err != nil {
 			ctx.Handle(404, "repo.SettingPost(update)", err)
 			return
 		}
+		log.Trace("%s Repository updated: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
 
-		ctx.Data["IsSuccess"] = true
-		if isNameChanged {
-			ctx.Redirect(fmt.Sprintf("/%s/%s/settings", ctx.Repo.Owner.Name, ctx.Repo.Repository.Name))
-		} else {
-			ctx.HTML(200, "repo/setting")
+		if ctx.Repo.Repository.IsMirror {
+			if len(ctx.Query("interval")) > 0 {
+				var err error
+				ctx.Repo.Mirror.Interval, err = base.StrTo(ctx.Query("interval")).Int()
+				if err != nil {
+					log.Error("repo.SettingPost(get mirror interval): %v", err)
+				} else if err = models.UpdateMirror(ctx.Repo.Mirror); err != nil {
+					log.Error("repo.SettingPost(UpdateMirror): %v", err)
+				}
+			}
 		}
-		log.Trace("%s Repository updated: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+
+		ctx.Flash.Success("Repository options has been successfully updated.")
+		ctx.Redirect(fmt.Sprintf("/%s/%s/settings", ctx.Repo.Owner.Name, ctx.Repo.Repository.Name))
 	case "transfer":
 		if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") {
 			ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil)
@@ -324,19 +372,18 @@ func SettingPost(ctx *middleware.Context) {
 		// Check if new owner exists.
 		isExist, err := models.IsUserExist(newOwner)
 		if err != nil {
-			ctx.Handle(404, "repo.SettingPost(transfer: check existence)", err)
+			ctx.Handle(500, "repo.SettingPost(transfer: check existence)", err)
 			return
 		} else if !isExist {
 			ctx.RenderWithErr("Please make sure you entered owner name is correct.", "repo/setting", nil)
 			return
 		} else if err = models.TransferOwnership(ctx.User, newOwner, ctx.Repo.Repository); err != nil {
-			ctx.Handle(404, "repo.SettingPost(transfer repository)", err)
+			ctx.Handle(500, "repo.SettingPost(transfer repository)", err)
 			return
 		}
 		log.Trace("%s Repository transfered: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newOwner)
 
 		ctx.Redirect("/")
-		return
 	case "delete":
 		if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") {
 			ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil)
@@ -344,11 +391,11 @@ func SettingPost(ctx *middleware.Context) {
 		}
 
 		if err := models.DeleteRepository(ctx.User.Id, ctx.Repo.Repository.Id, ctx.User.LowerName); err != nil {
-			ctx.Handle(200, "repo.Delete", err)
+			ctx.Handle(500, "repo.Delete", err)
 			return
 		}
-
 		log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName)
+
 		ctx.Redirect("/")
 	}
 }

+ 196 - 0
routers/user/home.go

@@ -0,0 +1,196 @@
+// 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 user
+
+import (
+	"fmt"
+
+	"github.com/go-martini/martini"
+
+	"github.com/gogits/gogs/models"
+	"github.com/gogits/gogs/modules/auth"
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/middleware"
+)
+
+func Dashboard(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Dashboard"
+	ctx.Data["PageIsUserDashboard"] = true
+	repos, err := models.GetRepositories(&models.User{Id: ctx.User.Id}, true)
+	if err != nil {
+		ctx.Handle(500, "user.Dashboard", err)
+		return
+	}
+	ctx.Data["MyRepos"] = repos
+
+	feeds, err := models.GetFeeds(ctx.User.Id, 0, false)
+	if err != nil {
+		ctx.Handle(500, "user.Dashboard", err)
+		return
+	}
+	ctx.Data["Feeds"] = feeds
+	ctx.HTML(200, "user/dashboard")
+}
+
+func Profile(ctx *middleware.Context, params martini.Params) {
+	ctx.Data["Title"] = "Profile"
+
+	// TODO: Need to check view self or others.
+	user, err := models.GetUserByName(params["username"])
+	if err != nil {
+		ctx.Handle(500, "user.Profile", err)
+		return
+	}
+
+	ctx.Data["Owner"] = user
+
+	tab := ctx.Query("tab")
+	ctx.Data["TabName"] = tab
+
+	switch tab {
+	case "activity":
+		feeds, err := models.GetFeeds(user.Id, 0, true)
+		if err != nil {
+			ctx.Handle(500, "user.Profile", err)
+			return
+		}
+		ctx.Data["Feeds"] = feeds
+	default:
+		repos, err := models.GetRepositories(user, ctx.IsSigned && ctx.User.Id == user.Id)
+		if err != nil {
+			ctx.Handle(500, "user.Profile", err)
+			return
+		}
+		ctx.Data["Repos"] = repos
+	}
+
+	ctx.Data["PageIsUserProfile"] = true
+	ctx.HTML(200, "user/profile")
+}
+
+func Email2User(ctx *middleware.Context) {
+	u, err := models.GetUserByEmail(ctx.Query("email"))
+	if err != nil {
+		if err == models.ErrUserNotExist {
+			ctx.Handle(404, "user.Email2User", err)
+		} else {
+			ctx.Handle(500, "user.Email2User(GetUserByEmail)", err)
+		}
+		return
+	}
+
+	ctx.Redirect("/user/" + u.Name)
+}
+
+const (
+	TPL_FEED = `<i class="icon fa fa-%s"></i>
+                        <div class="info"><span class="meta">%s</span><br>%s</div>`
+)
+
+func Feeds(ctx *middleware.Context, form auth.FeedsForm) {
+	actions, err := models.GetFeeds(form.UserId, form.Page*20, false)
+	if err != nil {
+		ctx.JSON(500, err)
+	}
+
+	feeds := make([]string, len(actions))
+	for i := range actions {
+		feeds[i] = fmt.Sprintf(TPL_FEED, base.ActionIcon(actions[i].OpType),
+			base.TimeSince(actions[i].Created), base.ActionDesc(actions[i]))
+	}
+	ctx.JSON(200, &feeds)
+}
+
+func Issues(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Your Issues"
+	ctx.Data["ViewType"] = "all"
+
+	page, _ := base.StrTo(ctx.Query("page")).Int()
+	repoId, _ := base.StrTo(ctx.Query("repoid")).Int64()
+
+	ctx.Data["RepoId"] = repoId
+
+	var posterId int64 = 0
+	if ctx.Query("type") == "created_by" {
+		posterId = ctx.User.Id
+		ctx.Data["ViewType"] = "created_by"
+	}
+
+	// Get all repositories.
+	repos, err := models.GetRepositories(ctx.User, true)
+	if err != nil {
+		ctx.Handle(200, "user.Issues(get repositories)", err)
+		return
+	}
+
+	showRepos := make([]models.Repository, 0, len(repos))
+
+	isShowClosed := ctx.Query("state") == "closed"
+	var closedIssueCount, createdByCount, allIssueCount int
+
+	// Get all issues.
+	allIssues := make([]models.Issue, 0, 5*len(repos))
+	for i, repo := range repos {
+		issues, err := models.GetIssues(0, repo.Id, posterId, 0, page, isShowClosed, false, "", "")
+		if err != nil {
+			ctx.Handle(200, "user.Issues(get issues)", err)
+			return
+		}
+
+		allIssueCount += repo.NumIssues
+		closedIssueCount += repo.NumClosedIssues
+
+		// Set repository information to issues.
+		for j := range issues {
+			issues[j].Repo = &repos[i]
+		}
+		allIssues = append(allIssues, issues...)
+
+		repos[i].NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
+		if repos[i].NumOpenIssues > 0 {
+			showRepos = append(showRepos, repos[i])
+		}
+	}
+
+	showIssues := make([]models.Issue, 0, len(allIssues))
+	ctx.Data["IsShowClosed"] = isShowClosed
+
+	// Get posters and filter issues.
+	for i := range allIssues {
+		u, err := models.GetUserById(allIssues[i].PosterId)
+		if err != nil {
+			ctx.Handle(200, "user.Issues(get poster): %v", err)
+			return
+		}
+		allIssues[i].Poster = u
+		if u.Id == ctx.User.Id {
+			createdByCount++
+		}
+
+		if repoId > 0 && repoId != allIssues[i].Repo.Id {
+			continue
+		}
+
+		if isShowClosed == allIssues[i].IsClosed {
+			showIssues = append(showIssues, allIssues[i])
+		}
+	}
+
+	ctx.Data["Repos"] = showRepos
+	ctx.Data["Issues"] = showIssues
+	ctx.Data["AllIssueCount"] = allIssueCount
+	ctx.Data["ClosedIssueCount"] = closedIssueCount
+	ctx.Data["OpenIssueCount"] = allIssueCount - closedIssueCount
+	ctx.Data["CreatedByCount"] = createdByCount
+	ctx.HTML(200, "issue/user")
+}
+
+func Pulls(ctx *middleware.Context) {
+	ctx.HTML(200, "user/pulls")
+}
+
+func Stars(ctx *middleware.Context) {
+	ctx.HTML(200, "user/stars")
+}

+ 58 - 29
routers/user/setting.go

@@ -14,8 +14,16 @@ import (
 	"github.com/gogits/gogs/modules/middleware"
 )
 
+func Setting(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Setting"
+	ctx.Data["PageIsUserSetting"] = true
+	ctx.Data["IsUserPageSetting"] = true
+	ctx.Data["Owner"] = ctx.User
+	ctx.HTML(200, "user/setting")
+}
+
 // Render user setting page (email, website modify)
-func Setting(ctx *middleware.Context, form auth.UpdateProfileForm) {
+func SettingPost(ctx *middleware.Context, form auth.UpdateProfileForm) {
 	ctx.Data["Title"] = "Setting"
 	ctx.Data["PageIsUserSetting"] = true // For navbar arrow.
 	ctx.Data["IsUserPageSetting"] = true // For setting nav highlight.
@@ -23,7 +31,7 @@ func Setting(ctx *middleware.Context, form auth.UpdateProfileForm) {
 	user := ctx.User
 	ctx.Data["Owner"] = user
 
-	if ctx.Req.Method == "GET" || ctx.HasError() {
+	if ctx.HasError() {
 		ctx.HTML(200, "user/setting")
 		return
 	}
@@ -32,13 +40,13 @@ func Setting(ctx *middleware.Context, form auth.UpdateProfileForm) {
 	if user.Name != form.UserName {
 		isExist, err := models.IsUserExist(form.UserName)
 		if err != nil {
-			ctx.Handle(404, "user.Setting(update: check existence)", err)
+			ctx.Handle(500, "user.Setting(update: check existence)", err)
 			return
 		} else if isExist {
 			ctx.RenderWithErr("User name has been taken.", "user/setting", &form)
 			return
 		} else if err = models.ChangeUserName(user, form.UserName); err != nil {
-			ctx.Handle(404, "user.Setting(change user name)", err)
+			ctx.Handle(500, "user.Setting(change user name)", err)
 			return
 		}
 		log.Trace("%s User name changed: %s -> %s", ctx.Req.RequestURI, user.Name, form.UserName)
@@ -52,50 +60,69 @@ func Setting(ctx *middleware.Context, form auth.UpdateProfileForm) {
 	user.Avatar = base.EncodeMd5(form.Avatar)
 	user.AvatarEmail = form.Avatar
 	if err := models.UpdateUser(user); err != nil {
-		ctx.Handle(200, "setting.Setting", err)
+		ctx.Handle(500, "setting.Setting", err)
 		return
 	}
-
-	ctx.Data["IsSuccess"] = true
-	ctx.HTML(200, "user/setting")
 	log.Trace("%s User setting updated: %s", ctx.Req.RequestURI, ctx.User.LowerName)
+
+	ctx.Flash.Success("Your profile has been successfully updated.")
+	ctx.Redirect("/user/setting")
+}
+
+func SettingSocial(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Social Account"
+	ctx.Data["PageIsUserSetting"] = true
+	ctx.Data["IsUserPageSettingSocial"] = true
+	socials, err := models.GetOauthByUserId(ctx.User.Id)
+	if err != nil {
+		ctx.Handle(500, "user.SettingSocial", err)
+		return
+	}
+
+	ctx.Data["Socials"] = socials
+	ctx.HTML(200, "user/social")
+}
+
+func SettingPassword(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Password"
+	ctx.Data["PageIsUserSetting"] = true
+	ctx.Data["IsUserPageSettingPasswd"] = true
+	ctx.HTML(200, "user/password")
 }
 
-func SettingPassword(ctx *middleware.Context, form auth.UpdatePasswdForm) {
+func SettingPasswordPost(ctx *middleware.Context, form auth.UpdatePasswdForm) {
 	ctx.Data["Title"] = "Password"
 	ctx.Data["PageIsUserSetting"] = true
 	ctx.Data["IsUserPageSettingPasswd"] = true
 
-	if ctx.Req.Method == "GET" {
+	if ctx.HasError() {
 		ctx.HTML(200, "user/password")
 		return
 	}
 
 	user := ctx.User
-	newUser := &models.User{Passwd: form.NewPasswd}
-	if err := newUser.EncodePasswd(); err != nil {
-		ctx.Handle(200, "setting.SettingPassword", err)
-		return
+	tmpUser := &models.User{
+		Passwd: form.OldPasswd,
+		Salt:   user.Salt,
 	}
-
-	if user.Passwd != newUser.Passwd {
-		ctx.Data["HasError"] = true
-		ctx.Data["ErrorMsg"] = "Old password is not correct"
+	tmpUser.EncodePasswd()
+	if user.Passwd != tmpUser.Passwd {
+		ctx.Flash.Error("Old password is not correct")
 	} else if form.NewPasswd != form.RetypePasswd {
-		ctx.Data["HasError"] = true
-		ctx.Data["ErrorMsg"] = "New password and re-type password are not same"
+		ctx.Flash.Error("New password and re-type password are not same")
 	} else {
-		user.Passwd = newUser.Passwd
+		user.Passwd = form.NewPasswd
+		user.Salt = models.GetUserSalt()
+		user.EncodePasswd()
 		if err := models.UpdateUser(user); err != nil {
 			ctx.Handle(200, "setting.SettingPassword", err)
 			return
 		}
-		ctx.Data["IsSuccess"] = true
+		log.Trace("%s User password updated: %s", ctx.Req.RequestURI, ctx.User.LowerName)
+		ctx.Flash.Success("Password is changed successfully. You can now sign in via new password.")
 	}
 
-	ctx.Data["Owner"] = user
-	ctx.HTML(200, "user/password")
-	log.Trace("%s User password updated: %s", ctx.Req.RequestURI, ctx.User.LowerName)
+	ctx.Redirect("/user/setting/password")
 }
 
 func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
@@ -134,7 +161,7 @@ func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
 
 	// Add new SSH key.
 	if ctx.Req.Method == "POST" {
-		if hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) {
+		if ctx.HasError() {
 			ctx.HTML(200, "user/publickey")
 			return
 		}
@@ -149,11 +176,13 @@ func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
 				ctx.RenderWithErr("Public key name has been used", "user/publickey", &form)
 				return
 			}
-			ctx.Handle(200, "ssh.AddPublicKey", err)
-			log.Trace("%s User SSH key added: %s", ctx.Req.RequestURI, ctx.User.LowerName)
+			ctx.Handle(500, "ssh.AddPublicKey", err)
 			return
 		} else {
-			ctx.Data["AddSSHKeySuccess"] = true
+			log.Trace("%s User SSH key added: %s", ctx.Req.RequestURI, ctx.User.LowerName)
+			ctx.Flash.Success("New SSH Key has been added!")
+			ctx.Redirect("/user/setting/ssh")
+			return
 		}
 	}
 

+ 77 - 27
routers/user/social.go

@@ -1,49 +1,99 @@
 // 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 user
 
 import (
 	"encoding/json"
+	"errors"
+	"fmt"
+	"net/url"
+	"strings"
+
+	"github.com/go-martini/martini"
 
-	"code.google.com/p/goauth2/oauth"
+	"github.com/gogits/gogs/models"
+	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
-	"github.com/gogits/gogs/modules/oauth2"
+	"github.com/gogits/gogs/modules/middleware"
+	"github.com/gogits/gogs/modules/social"
 )
 
-// github && google && ...
-func SocialSignIn(tokens oauth2.Tokens) {
-	transport := &oauth.Transport{}
-	transport.Token = &oauth.Token{
-		AccessToken:  tokens.Access(),
-		RefreshToken: tokens.Refresh(),
-		Expiry:       tokens.ExpiryTime(),
-		Extra:        tokens.ExtraData(),
+func extractPath(next string) string {
+	n, err := url.Parse(next)
+	if err != nil {
+		return "/"
+	}
+	return n.Path
+}
+
+func SocialSignIn(ctx *middleware.Context, params martini.Params) {
+	if base.OauthService == nil {
+		ctx.Handle(404, "social.SocialSignIn(oauth service not enabled)", nil)
+		return
+	}
+
+	next := extractPath(ctx.Query("next"))
+	name := params["name"]
+	connect, ok := social.SocialMap[name]
+	if !ok {
+		ctx.Handle(404, "social.SocialSignIn(social login not enabled)", errors.New(name))
+		return
 	}
 
-	// Github API refer: https://developer.github.com/v3/users/
-	// FIXME: need to judge url
-	type GithubUser struct {
-		Id    int    `json:"id"`
-		Name  string `json:"login"`
-		Email string `json:"email"`
+	code := ctx.Query("code")
+	if code == "" {
+		// redirect to social login page
+		connect.SetRedirectUrl(strings.TrimSuffix(base.AppUrl, "/") + ctx.Req.URL.Path)
+		ctx.Redirect(connect.AuthCodeURL(next))
+		return
 	}
 
-	// Make the request.
-	scope := "https://api.github.com/user"
-	r, err := transport.Client().Get(scope)
+	// handle call back
+	tk, err := connect.Exchange(code)
 	if err != nil {
-		log.Error("connect with github error: %s", err)
-		// FIXME: handle error page
+		ctx.Handle(500, "social.SocialSignIn(Exchange)", err)
 		return
 	}
-	defer r.Body.Close()
+	next = extractPath(ctx.Query("state"))
+	log.Trace("social.SocialSignIn(Got token)")
 
-	user := &GithubUser{}
-	err = json.NewDecoder(r.Body).Decode(user)
+	ui, err := connect.UserInfo(tk, ctx.Req.URL)
 	if err != nil {
-		log.Error("Get: %s", err)
+		ctx.Handle(500, fmt.Sprintf("social.SocialSignIn(get info from %s)", name), err)
+		return
 	}
-	log.Info("login: %s", user.Name)
-	// FIXME: login here, user email to check auth, if not registe, then generate a uniq username
+	log.Info("social.SocialSignIn(social login): %s", ui)
+
+	oa, err := models.GetOauth2(ui.Identity)
+	switch err {
+	case nil:
+		ctx.Session.Set("userId", oa.User.Id)
+		ctx.Session.Set("userName", oa.User.Name)
+	case models.ErrOauth2RecordNotExist:
+		raw, _ := json.Marshal(tk)
+		oa = &models.Oauth2{
+			Uid:      -1,
+			Type:     connect.Type(),
+			Identity: ui.Identity,
+			Token:    string(raw),
+		}
+		log.Trace("social.SocialSignIn(oa): %v", oa)
+		if err = models.AddOauth2(oa); err != nil {
+			log.Error("social.SocialSignIn(add oauth2): %v", err) // 501
+			return
+		}
+	case models.ErrOauth2NotAssociated:
+		next = "/user/sign_up"
+	default:
+		ctx.Handle(500, "social.SocialSignIn(GetOauth2)", err)
+		return
+	}
+
+	ctx.Session.Set("socialId", oa.Id)
+	ctx.Session.Set("socialName", ui.Name)
+	ctx.Session.Set("socialEmail", ui.Email)
+	log.Trace("social.SocialSignIn(social ID): %v", oa.Id)
+	ctx.Redirect(next)
 }

+ 276 - 226
routers/user/user.go

@@ -5,12 +5,9 @@
 package user
 
 import (
-	"fmt"
 	"net/url"
 	"strings"
 
-	"github.com/go-martini/martini"
-
 	"github.com/gogits/gogs/models"
 	"github.com/gogits/gogs/modules/auth"
 	"github.com/gogits/gogs/modules/base"
@@ -19,105 +16,72 @@ import (
 	"github.com/gogits/gogs/modules/middleware"
 )
 
-func Dashboard(ctx *middleware.Context) {
-	ctx.Data["Title"] = "Dashboard"
-	ctx.Data["PageIsUserDashboard"] = true
-	repos, err := models.GetRepositories(&models.User{Id: ctx.User.Id})
-	if err != nil {
-		ctx.Handle(200, "user.Dashboard", err)
+func SignIn(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Log In"
+
+	if _, ok := ctx.Session.Get("socialId").(int64); ok {
+		ctx.Data["IsSocialLogin"] = true
+		ctx.HTML(200, "user/signin")
 		return
 	}
-	ctx.Data["MyRepos"] = repos
 
-	feeds, err := models.GetFeeds(ctx.User.Id, 0, false)
-	if err != nil {
-		ctx.Handle(200, "user.Dashboard", err)
+	if base.OauthService != nil {
+		ctx.Data["OauthEnabled"] = true
+		ctx.Data["OauthService"] = base.OauthService
+	}
+
+	// Check auto-login.
+	userName := ctx.GetCookie(base.CookieUserName)
+	if len(userName) == 0 {
+		ctx.HTML(200, "user/signin")
 		return
 	}
-	ctx.Data["Feeds"] = feeds
-	ctx.HTML(200, "user/dashboard")
-}
 
-func Profile(ctx *middleware.Context, params martini.Params) {
-	ctx.Data["Title"] = "Profile"
+	isSucceed := false
+	defer func() {
+		if !isSucceed {
+			log.Trace("user.SignIn(auto-login cookie cleared): %s", userName)
+			ctx.SetCookie(base.CookieUserName, "", -1)
+			ctx.SetCookie(base.CookieRememberName, "", -1)
+			return
+		}
+	}()
 
-	// TODO: Need to check view self or others.
-	user, err := models.GetUserByName(params["username"])
+	user, err := models.GetUserByName(userName)
 	if err != nil {
-		ctx.Handle(200, "user.Profile", err)
+		ctx.HTML(500, "user/signin")
 		return
 	}
 
-	ctx.Data["Owner"] = user
+	secret := base.EncodeMd5(user.Rands + user.Passwd)
+	value, _ := ctx.GetSecureCookie(secret, base.CookieRememberName)
+	if value != user.Name {
+		ctx.HTML(500, "user/signin")
+		return
+	}
 
-	tab := ctx.Query("tab")
-	ctx.Data["TabName"] = tab
+	isSucceed = true
 
-	switch tab {
-	case "activity":
-		feeds, err := models.GetFeeds(user.Id, 0, true)
-		if err != nil {
-			ctx.Handle(200, "user.Profile", err)
-			return
-		}
-		ctx.Data["Feeds"] = feeds
-	default:
-		repos, err := models.GetRepositories(user)
-		if err != nil {
-			ctx.Handle(200, "user.Profile", err)
-			return
-		}
-		ctx.Data["Repos"] = repos
+	ctx.Session.Set("userId", user.Id)
+	ctx.Session.Set("userName", user.Name)
+	if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
+		ctx.SetCookie("redirect_to", "", -1)
+		ctx.Redirect(redirectTo)
+		return
 	}
 
-	ctx.Data["PageIsUserProfile"] = true
-	ctx.HTML(200, "user/profile")
+	ctx.Redirect("/")
 }
 
-func SignIn(ctx *middleware.Context, form auth.LogInForm) {
+func SignInPost(ctx *middleware.Context, form auth.LogInForm) {
 	ctx.Data["Title"] = "Log In"
 
-	if ctx.Req.Method == "GET" {
-		// 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
+	sid, isOauth := ctx.Session.Get("socialId").(int64)
+	if isOauth {
+		ctx.Data["IsSocialLogin"] = true
+	} else if base.OauthService != nil {
+		ctx.Data["OauthEnabled"] = true
+		ctx.Data["OauthService"] = base.OauthService
 	}
 
 	if ctx.HasError() {
@@ -133,7 +97,7 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) {
 			return
 		}
 
-		ctx.Handle(200, "user.SignIn", err)
+		ctx.Handle(500, "user.SignIn", err)
 		return
 	}
 
@@ -144,40 +108,116 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) {
 		ctx.SetSecureCookie(secret, base.CookieRememberName, user.Name, days)
 	}
 
+	// Bind with social account.
+	if isOauth {
+		if err = models.BindUserOauth2(user.Id, sid); err != nil {
+			if err == models.ErrOauth2RecordNotExist {
+				ctx.Handle(404, "user.SignInPost(GetOauth2ById)", err)
+			} else {
+				ctx.Handle(500, "user.SignInPost(GetOauth2ById)", err)
+			}
+			return
+		}
+		ctx.Session.Delete("socialId")
+		log.Trace("%s OAuth binded: %s -> %d", ctx.Req.RequestURI, form.UserName, sid)
+	}
+
 	ctx.Session.Set("userId", user.Id)
 	ctx.Session.Set("userName", user.Name)
-	redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to"))
-	if len(redirectTo) > 0 {
+	if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
 		ctx.SetCookie("redirect_to", "", -1)
 		ctx.Redirect(redirectTo)
-	} else {
-		ctx.Redirect("/")
+		return
+	}
+
+	ctx.Redirect("/")
+}
+
+func oauthSignInPost(ctx *middleware.Context, sid int64) {
+	ctx.Data["Title"] = "OAuth Sign Up"
+	ctx.Data["PageIsSignUp"] = true
+
+	if _, err := models.GetOauth2ById(sid); err != nil {
+		if err == models.ErrOauth2RecordNotExist {
+			ctx.Handle(404, "user.oauthSignUp(GetOauth2ById)", err)
+		} else {
+			ctx.Handle(500, "user.oauthSignUp(GetOauth2ById)", err)
+		}
+		return
 	}
+
+	ctx.Data["IsSocialLogin"] = true
+	ctx.Data["username"] = ctx.Session.Get("socialName")
+	ctx.Data["email"] = ctx.Session.Get("socialEmail")
+	log.Trace("user.oauthSignUp(social ID): %v", ctx.Session.Get("socialId"))
+
+	ctx.HTML(200, "user/signup")
 }
 
 func SignOut(ctx *middleware.Context) {
 	ctx.Session.Delete("userId")
 	ctx.Session.Delete("userName")
+	ctx.Session.Delete("socialId")
+	ctx.Session.Delete("socialName")
+	ctx.Session.Delete("socialEmail")
 	ctx.SetCookie(base.CookieUserName, "", -1)
 	ctx.SetCookie(base.CookieRememberName, "", -1)
 	ctx.Redirect("/")
 }
 
-func SignUp(ctx *middleware.Context, form auth.RegisterForm) {
+func SignUp(ctx *middleware.Context) {
 	ctx.Data["Title"] = "Sign Up"
 	ctx.Data["PageIsSignUp"] = true
 
-	if base.Service.DisenableRegisteration {
-		ctx.Data["DisenableRegisteration"] = true
+	if base.Service.DisableRegistration {
+		ctx.Data["DisableRegistration"] = true
 		ctx.HTML(200, "user/signup")
 		return
 	}
 
-	if ctx.Req.Method == "GET" {
-		ctx.HTML(200, "user/signup")
+	if sid, ok := ctx.Session.Get("socialId").(int64); ok {
+		oauthSignUp(ctx, sid)
+		return
+	}
+
+	ctx.HTML(200, "user/signup")
+}
+
+func oauthSignUp(ctx *middleware.Context, sid int64) {
+	ctx.Data["Title"] = "OAuth Sign Up"
+	ctx.Data["PageIsSignUp"] = true
+
+	if _, err := models.GetOauth2ById(sid); err != nil {
+		if err == models.ErrOauth2RecordNotExist {
+			ctx.Handle(404, "user.oauthSignUp(GetOauth2ById)", err)
+		} else {
+			ctx.Handle(500, "user.oauthSignUp(GetOauth2ById)", err)
+		}
 		return
 	}
 
+	ctx.Data["IsSocialLogin"] = true
+	ctx.Data["username"] = strings.Replace(ctx.Session.Get("socialName").(string), " ", "", -1)
+	ctx.Data["email"] = ctx.Session.Get("socialEmail")
+	log.Trace("user.oauthSignUp(social ID): %v", ctx.Session.Get("socialId"))
+
+	ctx.HTML(200, "user/signup")
+}
+
+func SignUpPost(ctx *middleware.Context, form auth.RegisterForm) {
+	ctx.Data["Title"] = "Sign Up"
+	ctx.Data["PageIsSignUp"] = true
+
+	if base.Service.DisableRegistration {
+		ctx.Handle(403, "user.SignUpPost", nil)
+		return
+	}
+
+	sid, isOauth := ctx.Session.Get("socialId").(int64)
+	if isOauth {
+		ctx.Data["IsSocialLogin"] = true
+	}
+
 	if form.Password != form.RetypePasswd {
 		ctx.Data["HasError"] = true
 		ctx.Data["Err_Password"] = true
@@ -195,7 +235,7 @@ func SignUp(ctx *middleware.Context, form auth.RegisterForm) {
 		Name:     form.UserName,
 		Email:    form.Email,
 		Passwd:   form.Password,
-		IsActive: !base.Service.RegisterEmailConfirm,
+		IsActive: !base.Service.RegisterEmailConfirm || isOauth,
 	}
 
 	var err error
@@ -208,20 +248,30 @@ func SignUp(ctx *middleware.Context, form auth.RegisterForm) {
 		case models.ErrUserNameIllegal:
 			ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "user/signup", &form)
 		default:
-			ctx.Handle(200, "user.SignUp", err)
+			ctx.Handle(500, "user.SignUp(RegisterUser)", err)
 		}
 		return
 	}
 
-	log.Trace("%s User created: %s", ctx.Req.RequestURI, strings.ToLower(form.UserName))
+	log.Trace("%s User created: %s", ctx.Req.RequestURI, form.UserName)
+
+	// Bind social account.
+	if isOauth {
+		if err = models.BindUserOauth2(u.Id, sid); err != nil {
+			ctx.Handle(500, "user.SignUp(BindUserOauth2)", err)
+			return
+		}
+		ctx.Session.Delete("socialId")
+		log.Trace("%s OAuth binded: %s -> %d", ctx.Req.RequestURI, form.UserName, sid)
+	}
 
-	// Send confirmation e-mail.
-	if base.Service.RegisterEmailConfirm && u.Id > 1 {
+	// Send confirmation e-mail, no need for social account.
+	if !isOauth && base.Service.RegisterEmailConfirm && u.Id > 1 {
 		mailer.SendRegisterMail(ctx.Render, u)
 		ctx.Data["IsSendRegisterMail"] = true
 		ctx.Data["Email"] = u.Email
 		ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60
-		ctx.HTML(200, "user/active")
+		ctx.HTML(200, "user/activate")
 
 		if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
 			log.Error("Set cache(MailResendLimit) fail: %v", err)
@@ -235,25 +285,28 @@ func Delete(ctx *middleware.Context) {
 	ctx.Data["Title"] = "Delete Account"
 	ctx.Data["PageIsUserSetting"] = true
 	ctx.Data["IsUserPageSettingDelete"] = true
+	ctx.HTML(200, "user/delete")
+}
 
-	if ctx.Req.Method == "GET" {
-		ctx.HTML(200, "user/delete")
-		return
-	}
+func DeletePost(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Delete Account"
+	ctx.Data["PageIsUserSetting"] = true
+	ctx.Data["IsUserPageSettingDelete"] = true
 
-	tmpUser := models.User{Passwd: ctx.Query("password")}
+	tmpUser := models.User{
+		Passwd: ctx.Query("password"),
+		Salt:   ctx.User.Salt,
+	}
 	tmpUser.EncodePasswd()
-	if len(tmpUser.Passwd) == 0 || tmpUser.Passwd != ctx.User.Passwd {
-		ctx.Data["HasError"] = true
-		ctx.Data["ErrorMsg"] = "Password is not correct. Make sure you are owner of this account."
+	if tmpUser.Passwd != ctx.User.Passwd {
+		ctx.Flash.Error("Password is not correct. Make sure you are owner of this account.")
 	} else {
 		if err := models.DeleteUser(ctx.User); err != nil {
-			ctx.Data["HasError"] = true
 			switch err {
 			case models.ErrUserOwnRepos:
-				ctx.Data["ErrorMsg"] = "Your account still have ownership of repository, you have to delete or transfer them first."
+				ctx.Flash.Error("Your account still have ownership of repository, you have to delete or transfer them first.")
 			default:
-				ctx.Handle(200, "user.Delete", err)
+				ctx.Handle(500, "user.Delete", err)
 				return
 			}
 		} else {
@@ -262,118 +315,7 @@ func Delete(ctx *middleware.Context) {
 		}
 	}
 
-	ctx.HTML(200, "user/delete")
-}
-
-const (
-	TPL_FEED = `<i class="icon fa fa-%s"></i>
-                        <div class="info"><span class="meta">%s</span><br>%s</div>`
-)
-
-func Feeds(ctx *middleware.Context, form auth.FeedsForm) {
-	actions, err := models.GetFeeds(form.UserId, form.Page*20, false)
-	if err != nil {
-		ctx.JSON(500, err)
-	}
-
-	feeds := make([]string, len(actions))
-	for i := range actions {
-		feeds[i] = fmt.Sprintf(TPL_FEED, base.ActionIcon(actions[i].OpType),
-			base.TimeSince(actions[i].Created), base.ActionDesc(actions[i]))
-	}
-	ctx.JSON(200, &feeds)
-}
-
-func Issues(ctx *middleware.Context) {
-	ctx.Data["Title"] = "Your Issues"
-	ctx.Data["ViewType"] = "all"
-
-	page, _ := base.StrTo(ctx.Query("page")).Int()
-	repoId, _ := base.StrTo(ctx.Query("repoid")).Int64()
-
-	ctx.Data["RepoId"] = repoId
-
-	var posterId int64 = 0
-	if ctx.Query("type") == "created_by" {
-		posterId = ctx.User.Id
-		ctx.Data["ViewType"] = "created_by"
-	}
-
-	// Get all repositories.
-	repos, err := models.GetRepositories(ctx.User)
-	if err != nil {
-		ctx.Handle(200, "user.Issues(get repositories)", err)
-		return
-	}
-
-	showRepos := make([]models.Repository, 0, len(repos))
-
-	isShowClosed := ctx.Query("state") == "closed"
-	var closedIssueCount, createdByCount, allIssueCount int
-
-	// Get all issues.
-	allIssues := make([]models.Issue, 0, 5*len(repos))
-	for i, repo := range repos {
-		issues, err := models.GetIssues(0, repo.Id, posterId, 0, page, isShowClosed, false, "", "")
-		if err != nil {
-			ctx.Handle(200, "user.Issues(get issues)", err)
-			return
-		}
-
-		allIssueCount += repo.NumIssues
-		closedIssueCount += repo.NumClosedIssues
-
-		// Set repository information to issues.
-		for j := range issues {
-			issues[j].Repo = &repos[i]
-		}
-		allIssues = append(allIssues, issues...)
-
-		repos[i].NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
-		if repos[i].NumOpenIssues > 0 {
-			showRepos = append(showRepos, repos[i])
-		}
-	}
-
-	showIssues := make([]models.Issue, 0, len(allIssues))
-	ctx.Data["IsShowClosed"] = isShowClosed
-
-	// Get posters and filter issues.
-	for i := range allIssues {
-		u, err := models.GetUserById(allIssues[i].PosterId)
-		if err != nil {
-			ctx.Handle(200, "user.Issues(get poster): %v", err)
-			return
-		}
-		allIssues[i].Poster = u
-		if u.Id == ctx.User.Id {
-			createdByCount++
-		}
-
-		if repoId > 0 && repoId != allIssues[i].Repo.Id {
-			continue
-		}
-
-		if isShowClosed == allIssues[i].IsClosed {
-			showIssues = append(showIssues, allIssues[i])
-		}
-	}
-
-	ctx.Data["Repos"] = showRepos
-	ctx.Data["Issues"] = showIssues
-	ctx.Data["AllIssueCount"] = allIssueCount
-	ctx.Data["ClosedIssueCount"] = closedIssueCount
-	ctx.Data["OpenIssueCount"] = allIssueCount - closedIssueCount
-	ctx.Data["CreatedByCount"] = createdByCount
-	ctx.HTML(200, "issue/user")
-}
-
-func Pulls(ctx *middleware.Context) {
-	ctx.HTML(200, "user/pulls")
-}
-
-func Stars(ctx *middleware.Context) {
-	ctx.HTML(200, "user/stars")
+	ctx.Redirect("/user/delete")
 }
 
 func Activate(ctx *middleware.Context) {
@@ -391,11 +333,15 @@ func Activate(ctx *middleware.Context) {
 			} else {
 				ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60
 				mailer.SendActiveMail(ctx.Render, ctx.User)
+
+				if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
+					log.Error("Set cache(MailResendLimit) fail: %v", err)
+				}
 			}
 		} else {
 			ctx.Data["ServiceNotEnabled"] = true
 		}
-		ctx.HTML(200, "user/active")
+		ctx.HTML(200, "user/activate")
 		return
 	}
 
@@ -403,9 +349,12 @@ func Activate(ctx *middleware.Context) {
 	if user := models.VerifyUserActiveCode(code); user != nil {
 		user.IsActive = true
 		user.Rands = models.GetUserSalt()
-		models.UpdateUser(user)
+		if err := models.UpdateUser(user); err != nil {
+			ctx.Handle(404, "user.Activate", err)
+			return
+		}
 
-		log.Trace("%s User activated: %s", ctx.Req.RequestURI, user.LowerName)
+		log.Trace("%s User activated: %s", ctx.Req.RequestURI, user.Name)
 
 		ctx.Session.Set("userId", user.Id)
 		ctx.Session.Set("userName", user.Name)
@@ -414,5 +363,106 @@ func Activate(ctx *middleware.Context) {
 	}
 
 	ctx.Data["IsActivateFailed"] = true
-	ctx.HTML(200, "user/active")
+	ctx.HTML(200, "user/activate")
+}
+
+func ForgotPasswd(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Forgot Password"
+
+	if base.MailService == nil {
+		ctx.Data["IsResetDisable"] = true
+		ctx.HTML(200, "user/forgot_passwd")
+		return
+	}
+
+	ctx.Data["IsResetRequest"] = true
+	ctx.HTML(200, "user/forgot_passwd")
+}
+
+func ForgotPasswdPost(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Forgot Password"
+
+	if base.MailService == nil {
+		ctx.Handle(403, "user.ForgotPasswdPost", nil)
+		return
+	}
+	ctx.Data["IsResetRequest"] = true
+
+	email := ctx.Query("email")
+	u, err := models.GetUserByEmail(email)
+	if err != nil {
+		if err == models.ErrUserNotExist {
+			ctx.RenderWithErr("This e-mail address does not associate to any account.", "user/forgot_passwd", nil)
+		} else {
+			ctx.Handle(500, "user.ResetPasswd(check existence)", err)
+		}
+		return
+	}
+
+	if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
+		ctx.Data["ResendLimited"] = true
+		ctx.HTML(200, "user/forgot_passwd")
+		return
+	}
+
+	mailer.SendResetPasswdMail(ctx.Render, u)
+	if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+		log.Error("Set cache(MailResendLimit) fail: %v", err)
+	}
+
+	ctx.Data["Email"] = email
+	ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60
+	ctx.Data["IsResetSent"] = true
+	ctx.HTML(200, "user/forgot_passwd")
+}
+
+func ResetPasswd(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Reset Password"
+
+	code := ctx.Query("code")
+	if len(code) == 0 {
+		ctx.Error(404)
+		return
+	}
+	ctx.Data["Code"] = code
+
+	ctx.Data["IsResetForm"] = true
+	ctx.HTML(200, "user/reset_passwd")
+}
+
+func ResetPasswdPost(ctx *middleware.Context) {
+	ctx.Data["Title"] = "Reset Password"
+
+	code := ctx.Query("code")
+	if len(code) == 0 {
+		ctx.Error(404)
+		return
+	}
+	ctx.Data["Code"] = code
+
+	if u := models.VerifyUserActiveCode(code); u != nil {
+		// Validate password length.
+		passwd := ctx.Query("passwd")
+		if len(passwd) < 6 || len(passwd) > 30 {
+			ctx.Data["IsResetForm"] = true
+			ctx.RenderWithErr("Password length should be in 6 and 30.", "user/reset_passwd", nil)
+			return
+		}
+
+		u.Passwd = passwd
+		u.Rands = models.GetUserSalt()
+		u.Salt = models.GetUserSalt()
+		u.EncodePasswd()
+		if err := models.UpdateUser(u); err != nil {
+			ctx.Handle(500, "user.ResetPasswd(UpdateUser)", err)
+			return
+		}
+
+		log.Trace("%s User password reset: %s", ctx.Req.RequestURI, u.Name)
+		ctx.Redirect("/user/login")
+		return
+	}
+
+	ctx.Data["IsResetFailed"] = true
+	ctx.HTML(200, "user/reset_passwd")
 }

+ 39 - 44
serve.go

@@ -14,7 +14,7 @@ import (
 	"strings"
 
 	"github.com/codegangsta/cli"
-	"github.com/gogits/gogs/modules/log"
+	qlog "github.com/qiniu/log"
 
 	//"github.com/gogits/git"
 	"github.com/gogits/gogs/models"
@@ -44,11 +44,16 @@ gogs serv provide access auth for repositories`,
 }
 
 func newLogger(execDir string) {
-	level := "0"
 	logPath := execDir + "/log/serv.log"
 	os.MkdirAll(path.Dir(logPath), os.ModePerm)
-	log.NewLogger(0, "file", fmt.Sprintf(`{"level":%s,"filename":"%s"}`, level, logPath))
-	log.Trace("start logging...")
+
+	f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.ModePerm)
+	if err != nil {
+		qlog.Fatal(err)
+	}
+
+	qlog.SetOutput(f)
+	qlog.Info("Start logging serv...")
 }
 
 func parseCmd(cmd string) (string, string) {
@@ -87,21 +92,18 @@ func runServ(k *cli.Context) {
 	keys := strings.Split(os.Args[2], "-")
 	if len(keys) != 2 {
 		println("auth file format error")
-		log.Error("auth file format error")
-		return
+		qlog.Fatal("auth file format error")
 	}
 
 	keyId, err := strconv.ParseInt(keys[1], 10, 64)
 	if err != nil {
 		println("auth file format error")
-		log.Error("auth file format error", err)
-		return
+		qlog.Fatal("auth file format error", err)
 	}
 	user, err := models.GetUserByKeyId(keyId)
 	if err != nil {
 		println("You have no right to access")
-		log.Error("SSH visit error: %v", err)
-		return
+		qlog.Fatalf("SSH visit error: %v", err)
 	}
 
 	cmd := os.Getenv("SSH_ORIGINAL_COMMAND")
@@ -114,24 +116,19 @@ func runServ(k *cli.Context) {
 	repoPath := strings.Trim(args, "'")
 	rr := strings.SplitN(repoPath, "/", 2)
 	if len(rr) != 2 {
-		println("Unavilable repository", args)
-		log.Error("Unavilable repository %v", args)
-		return
+		println("Unavailable repository", args)
+		qlog.Fatalf("Unavailable repository %v", args)
 	}
 	repoUserName := rr[0]
-	repoName := rr[1]
-	if strings.HasSuffix(repoName, ".git") {
-		repoName = repoName[:len(repoName)-4]
-	}
+	repoName := strings.TrimSuffix(rr[1], ".git")
 
 	isWrite := In(verb, COMMANDS_WRITE)
 	isRead := In(verb, COMMANDS_READONLY)
 
 	repoUser, err := models.GetUserByName(repoUserName)
 	if err != nil {
-		fmt.Println("You have no right to access")
-		log.Error("Get user failed", err)
-		return
+		println("You have no right to access")
+		qlog.Fatal("Get user failed", err)
 	}
 
 	// access check
@@ -139,55 +136,45 @@ func runServ(k *cli.Context) {
 	case isWrite:
 		has, err := models.HasAccess(user.LowerName, path.Join(repoUserName, repoName), models.AU_WRITABLE)
 		if err != nil {
-			println("Inernel error:", err)
-			log.Error(err.Error())
-			return
+			println("Internal error:", err)
+			qlog.Fatal(err)
 		} else if !has {
 			println("You have no right to write this repository")
-			log.Error("User %s has no right to write repository %s", user.Name, repoPath)
-			return
+			qlog.Fatalf("User %s has no right to write repository %s", user.Name, repoPath)
 		}
 	case isRead:
 		repo, err := models.GetRepositoryByName(repoUser.Id, repoName)
 		if err != nil {
 			println("Get repository error:", err)
-			log.Error("Get repository error: " + err.Error())
-			return
+			qlog.Fatal("Get repository error: " + err.Error())
 		}
 
 		if !repo.IsPrivate {
 			break
 		}
 
-		has, err := models.HasAccess(user.Name, repoPath, models.AU_READABLE)
+		has, err := models.HasAccess(user.Name, path.Join(repoUserName, repoName), models.AU_READABLE)
 		if err != nil {
-			println("Inernel error")
-			log.Error(err.Error())
-			return
+			println("Internal error")
+			qlog.Fatal(err)
 		}
 		if !has {
 			has, err = models.HasAccess(user.Name, repoPath, models.AU_WRITABLE)
 			if err != nil {
-				println("Inernel error")
-				log.Error(err.Error())
-				return
+				println("Internal error")
+				qlog.Fatal(err)
 			}
 		}
 		if !has {
 			println("You have no right to access this repository")
-			log.Error("You have no right to access this repository")
-			return
+			qlog.Fatal("You have no right to access this repository")
 		}
 	default:
 		println("Unknown command")
-		log.Error("Unknown command")
-		return
+		qlog.Fatal("Unknown command")
 	}
 
-	// for update use
-	os.Setenv("userName", user.Name)
-	os.Setenv("userId", strconv.Itoa(int(user.Id)))
-	os.Setenv("repoName", repoName)
+	models.SetRepoEnvs(user.Id, user.Name, repoName)
 
 	gitcmd := exec.Command(verb, repoPath)
 	gitcmd.Dir = base.RepoRootPath
@@ -197,7 +184,15 @@ func runServ(k *cli.Context) {
 
 	if err = gitcmd.Run(); err != nil {
 		println("execute command error:", err.Error())
-		log.Error("execute command error: " + err.Error())
-		return
+		qlog.Fatal("execute command error: " + err.Error())
 	}
+
+	//refName := os.Getenv("refName")
+	//oldCommitId := os.Getenv("oldCommitId")
+	//newCommitId := os.Getenv("newCommitId")
+
+	//qlog.Error("get envs:", refName, oldCommitId, newCommitId)
+
+	// update
+	//models.Update(refName, oldCommitId, newCommitId, repoUserName, repoName, user.Id)
 }

+ 12 - 3
start.sh

@@ -1,6 +1,15 @@
-#!/bin/bash -
+#!/bin/sh -
+# 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.
 #
 # start gogs web
 #
-cd "$(dirname $0)"
-./gogs web
+IFS=' 
+	'
+PATH=/bin:/usr/bin:/usr/local/bin
+HOME=${HOME:?"need \$HOME variable"}
+USER=$(whoami)
+export USER HOME PATH
+
+cd "$(dirname $0)" && exec ./gogs web

+ 26 - 4
templates/admin/config.tmpl

@@ -62,8 +62,8 @@
                 <dl class="dl-horizontal admin-dl-horizontal">
                     <dt>Register Email Confirmation</dt>
                     <dd><i class="fa fa{{if .Service.RegisterEmailConfirm}}-check{{end}}-square-o"></i></dd>
-                    <dt>Disenable Registeration</dt>
-                    <dd><i class="fa fa{{if .Service.DisenableRegisteration}}-check{{end}}-square-o"></i></dd>
+                    <dt>Disable Registration</dt>
+                    <dd><i class="fa fa{{if .Service.DisableRegistration}}-check{{end}}-square-o"></i></dd>
                     <dt>Require Sign In View</dt>
                     <dd><i class="fa fa{{if .Service.RequireSignInView}}-check{{end}}-square-o"></i></dd>
                     <dt>Mail Notification</dt>
@@ -88,12 +88,34 @@
                 <dl class="dl-horizontal admin-dl-horizontal">
                     <dt>Enabled</dt>
                     <dd><i class="fa fa{{if .MailerEnabled}}-check{{end}}-square-o"></i></dd>
-                    <dt>Name</dt>
+                    {{if .MailerEnabled}}<dt>Name</dt>
                     <dd>{{.Mailer.Name}}</dd>
                     <dt>Host</dt>
                     <dd>{{.Mailer.Host}}</dd>
                     <dt>User</dt>
-                    <dd>{{.Mailer.User}}</dd>
+                    <dd>{{.Mailer.User}}</dd>{{end}}
+                </dl>
+            </div>
+        </div>
+
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                OAuth Configuration
+            </div>
+
+            <div class="panel-body">
+                <dl class="dl-horizontal admin-dl-horizontal">
+                    <dt>Enabled</dt>
+                    <dd><i class="fa fa{{if .OauthEnabled}}-check{{end}}-square-o"></i></dd>
+                    {{if .OauthEnabled}}<dt>GitHub</dt>
+                    <dd><i class="fa fa{{if .Oauther.GitHub}}-check{{end}}-square-o"></i></dd>
+                    <dt>Google</dt>
+                    <dd><i class="fa fa{{if .Oauther.Google}}-check{{end}}-square-o"></i></dd>
+                    <dt>Tencent QQ</dt>
+                    <dd><i class="fa fa{{if .Oauther.Tencent}}-check{{end}}-square-o"></i></dd>
+                    <dt>Weibo</dt>
+                    <dd><i class="fa fa{{if .Oauther.Weibo}}-check{{end}}-square-o"></i></dd>
+                    {{end}}
                 </dl>
             </div>
         </div>

+ 1 - 1
templates/admin/dashboard.tmpl

@@ -9,7 +9,7 @@
             </div>
 
             <div class="panel-body">
-                Gogs database has <b>{{.Stats.Counter.User}}</b> users, <b>{{.Stats.Counter.PublicKey}}</b> SSH keys, <b>{{.Stats.Counter.Repo}}</b> repositories, <b>{{.Stats.Counter.Watch}}</b> watches, <b>{{.Stats.Counter.Action}}</b> actions, and <b>{{.Stats.Counter.Access}}</b> accesses.
+                Gogs database has <b>{{.Stats.Counter.User}}</b> users, <b>{{.Stats.Counter.PublicKey}}</b> SSH keys, <b>{{.Stats.Counter.Repo}}</b> repositories, <b>{{.Stats.Counter.Watch}}</b> watches, <b>{{.Stats.Counter.Action}}</b> actions, <b>{{.Stats.Counter.Access}}</b> accesses, <b>{{.Stats.Counter.Issue}}</b> issues, <b>{{.Stats.Counter.Comment}}</b> comments, <b>{{.Stats.Counter.Mirror}}</b> mirrors, <b>{{.Stats.Counter.Oauth}}</b> oauthes, <b>{{.Stats.Counter.Release}}</b> releases.
             </div>
         </div>
 

+ 1 - 1
templates/admin/users/edit.tmpl

@@ -11,8 +11,8 @@
             <div class="panel-body">
             	<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}}
+				    {{template "base/alert" .}}
                 	<input type="hidden" value="{{.User.Id}}" name="userId"/>
 					<div class="form-group">
 						<label class="col-md-3 control-label">Username: </label>

+ 1 - 1
templates/admin/users/new.tmpl

@@ -12,7 +12,7 @@
             	<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>
+				    {{template "base/alert" .}}
 					<div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}">
 						<label class="col-md-3 control-label">Username: </label>
 						<div class="col-md-7">

+ 2 - 0
templates/base/alert.tmpl

@@ -0,0 +1,2 @@
+{{if .Flash.ErrorMsg}}<div class="alert alert-danger form-error">{{.Flash.ErrorMsg}}</div>{{end}}
+{{if .Flash.SuccessMsg}}<div class="alert alert-success">{{.Flash.SuccessMsg}}</div>{{end}}

+ 14 - 3
templates/base/head.tmpl

@@ -9,16 +9,27 @@
 		<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}}" />
+		{{if .Repository.IsGoget}}<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}">{{end}}
 
 		 <!-- Stylesheets -->
+		{{if IsProdMode}}
+		<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
+		<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
+
+		<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
+		<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
+		{{else}}
 		<link href="/css/bootstrap.min.css" rel="stylesheet" />
-		<link href="/css/todc-bootstrap.min.css" rel="stylesheet" />
 		<link href="/css/font-awesome.min.css" rel="stylesheet" />
-		<link href="/css/markdown.css" rel="stylesheet" />
-		<link href="/css/gogs.css" rel="stylesheet" />
 
 		<script src="/js/jquery-1.10.1.min.js"></script>
 		<script src="/js/bootstrap.min.js"></script>
+		{{end}}
+
+		<link href="/css/todc-bootstrap.min.css" rel="stylesheet" />
+		<link href="/css/markdown.css" rel="stylesheet" />
+		<link href="/css/gogs.css" rel="stylesheet" />
+
         <script src="/js/lib.js"></script>
         <script src="/js/app.js"></script>
 		<title>{{if .Title}}{{.Title}} - {{end}}{{AppName}}</title>

+ 26 - 4
templates/base/navbar.tmpl

@@ -1,16 +1,38 @@
 <div class="masthead navbar" id="masthead">
     <div class="container">
         <nav class="nav">
-            <a id="nav-logo" class="nav-item{{if .PageIsHome}} active{{end}}" href="/"><img src="/img/favicon.png" alt="Gogs Logo" id="logo"></a>
-            <a class="nav-item{{if .PageIsUserDashboard}} active{{end}}" href="/">Dashboard</a>
-            <a class="nav-item{{if .PageIsHelp}} active{{end}}" href="https://github.com/gogits/gogs/wiki">Help</a>{{if .IsSigned}}
+            <a id="nav-logo" class="nav-item pull-left{{if .PageIsHome}} active{{end}}" href="/"><img src="/img/favicon.png" alt="Gogs Logo" id="logo"></a>
+            <a class="nav-item pull-left{{if .PageIsUserDashboard}} active{{end}}" href="/">Dashboard</a>
+            <a class="nav-item pull-left{{if .PageIsHelp}} active{{end}}" href="https://github.com/gogits/gogs/wiki">Help</a>{{if .IsSigned}}
+            {{if .HasAccess}}<!-- <form class="nav-item pull-left{{if .PageIsNewRepo}} active{{end}}" id="nav-search-form">
+                <div class="input-group">
+                    <div class="input-group-btn">
+                        <button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown">{{if .Repository}}This Repository{{else}}All Repositories{{end}} <span class="caret"></span></button>
+                        <ul class="dropdown-menu">
+                            {{if .Repository}}<li><a href="#">This Repository</a></li>
+                            <li class="divider"></li>{{end}}
+                            <li><a href="#">All Repositories</a></li>
+                        </ul>
+                    </div>
+                    <input type="search" class="form-control input-sm" name="q" placeholder="search code, commits and issues"/>
+                </div>
+            </form> -->{{end}}
             <a id="nav-out" class="nav-item navbar-right navbar-btn btn btn-danger" href="/user/logout/"><i class="fa fa-power-off fa-lg"></i></a>
             <a id="nav-avatar" class="nav-item navbar-right{{if .PageIsUserProfile}} active{{end}}" href="{{.SignedUser.HomeLink}}" data-toggle="tooltip" data-placement="bottom" title="{{.SignedUserName}}">
                 <img src="{{.SignedUser.AvatarLink}}?s=28" alt="user-avatar" title="username"/>
             </a>
-            <a class="navbar-right nav-item{{if .PageIsNewRepo}} active{{end}}" href="/repo/create" data-toggle="tooltip" data-placement="bottom" title="New Repository"><i class="fa fa-plus fa-lg"></i></a>
             <a class="navbar-right nav-item{{if .PageIsUserSetting}} active{{end}}" href="/user/setting"  data-toggle="tooltip" data-placement="bottom" title="Setting"><i class="fa fa-cogs fa-lg"></i></a>
             {{if .IsAdmin}}<a class="navbar-right nav-item{{if .PageIsAdmin}} active{{end}}" href="/admin"  data-toggle="tooltip" data-placement="bottom" title="Admin"><i class="fa fa-gear fa-lg"></i></a>{{end}}
+            <div class="navbar-right nav-item pull-right{{if .PageIsNewRepo}} active{{end}}" id="nav-repo-new" data-toggle="tooltip" data-placement="bottom" title="New Repo">
+                <button type="button" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-plus-square fa-lg"></i></button>
+                <div class="dropdown-menu">
+                    <ul class="list-unstyled">
+                        <li><a href="/repo/create"><i class="fa fa-book"></i>Repository</a></li>
+                        <li><a href="/repo/migrate"><i class="fa fa-clipboard"></i>Migration</a></li>
+                        <!-- <li><a href="#"><i class="fa fa-users"></i>Organization</a></li> -->
+                    </ul>
+                </div>
+            </div>
             {{else}}<a id="nav-signin" class="nav-item navbar-right navbar-btn btn btn-danger" href="/user/login/">Sign In</a>
             <a id="nav-signup" class="nav-item navbar-right" href="/user/sign_up/">Sign Up</a>{{end}}
         </nav>

+ 20 - 1
templates/home.tmpl

@@ -1,8 +1,27 @@
 {{template "base/head" .}}
 {{template "base/navbar" .}}
 <div id="body" class="container">
+	{{if not .Repos}}
 	<h4>Hey there, welcome to the land of Gogs!</h4>
-	<p>If you just get your Gogs server running, go <a href="/install">install</a> guide page will help you setup things for your first-time run.</p>
+	<p>If you just got your Gogs server running, go to the <a href="/install">install</a> guide page, which will guide you through your initial setup.</p>
 	<img src="http://gowalker.org/public/gogs_demo.gif">
+	{{else}}
+	<h4>Hey there, welcome to the land of Gogs!</h4>
+	<h5>Here are some recent updated repositories:</h5>
+    <div class="tab-pane active">
+        <ul class="list-unstyled repo-list">
+        {{range .Repos}}
+            <li>
+                <div class="meta pull-right"><!-- <i class="fa fa-star"></i> {{.NumStars}} --> <i class="fa fa-code-fork"></i> {{.NumForks}}</div>
+                <h4>
+                    <a href="/{{.Owner.Name}}/{{.Name}}">{{.Name}}</a>
+                </h4>
+                <p class="desc">{{.Description}}</p>
+                <div class="info">Last updated {{.Updated|TimeSince}}</div>
+            </li>
+        {{end}}
+        </ul>
+    </div>
+	{{end}}
 </div>
 {{template "base/footer" .}}

+ 5 - 9
templates/install.tmpl

@@ -3,8 +3,8 @@
     <form action="/install" method="post" class="form-horizontal card" id="install-card">
         {{.CsrfTokenHtml}}
         <h3>Install Steps For First-time Run</h3>
-        <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div>
-        <p class="help-block text-center">Gogs requires MySQL or PostgreSQL, SQLite3 only available for official binary version</p>
+        {{template "base/alert" .}}
+        <p class="help-block text-center">Gogs requires MySQL, SQLite3. or PostgreSQL. SQLite3 is only available in the official binary version.</p>
         <div class="form-group">
             <label class="col-md-3 control-label">Database Type: </label>
             <div class="col-md-8">
@@ -156,11 +156,11 @@
                             <label class="col-md-3 control-label">SMTP Host: </label>
 
                             <div class="col-md-8">
-                                <input name="smtp_host" type="text" class="form-control" placeholder="Type SMTP host address" value="{{.smtp_host}}">
+                                <input name="smtp_host" type="text" class="form-control" placeholder="Type SMTP host address and port" value="{{.smtp_host}}">
                             </div>
                         </div>
                         <div class="form-group">
-                            <label class="col-md-3 control-label">Email: </label>
+                            <label class="col-md-3 control-label">Username: </label>
 
                             <div class="col-md-8">
                                 <input name="mailer_user" type="text" class="form-control" placeholder="Type SMTP user e-mail address" value="{{.mailer_user}}">
@@ -184,11 +184,7 @@
                                         <strong>Enable Register Confirmation</strong>
                                     </label>
                                 </div>
-                            </div>
-                        </div>
 
-                        <div class="form-group">
-                            <div class="col-md-offset-3 col-md-7">
                                 <div class="checkbox">
                                     <label>
                                         <input name="mail_notify" type="checkbox" {{if .mail_notify}}checked{{end}}>
@@ -208,4 +204,4 @@
 
     </form>
 </div>
-{{template "base/footer" .}}
+{{template "base/footer" .}}

+ 2 - 1
templates/issue/create.tmpl

@@ -6,6 +6,7 @@
     <div id="issue">
         <form class="form" action="{{.RepoLink}}/issues/new" method="post" id="issue-create-form">
             {{.CsrfTokenHtml}}
+            {{template "base/alert" .}}
             <div class="col-md-1">
                 <img class="avatar" src="{{.SignedUser.AvatarLink}}" alt=""/>
             </div>
@@ -19,7 +20,7 @@
                     </div>
                     <ul class="nav nav-tabs" data-init="tabs">
                         <li class="active issue-write"><a href="#issue-textarea" data-toggle="tab">Write</a></li>
-                        <li class="issue-preview"><a href="#issue-preview" data-toggle="tab" data-ajax="/api/v1/markdown?repo=repo_id&issue=new" data-ajax-name="issue-preview" data-ajax-method="post" data-preview="#issue-preview">Preview</a></li>
+                        <li class="issue-preview"><a href="#issue-preview" data-toggle="tab" data-ajax="/api/v1/markdown?repoLink={{.RepoLink}}" data-ajax-name="issue-preview" data-ajax-method="post" data-preview="#issue-preview">Preview</a></li>
                     </ul>
                     <div class="tab-content">
                         <div class="tab-pane" id="issue-textarea">

+ 1 - 1
templates/issue/view.tmpl

@@ -72,7 +72,7 @@
                                 </div>
                                 <ul class="nav nav-tabs" data-init="tabs">
                                     <li class="active issue-write"><a href="#issue-textarea" data-toggle="tab">Write</a></li>
-                                    <li class="issue-preview"><a href="#issue-preview" data-toggle="tab" data-ajax="/api/v1/markdown?repo=repo_id&issue=issue_id&comment=new" data-ajax-name="issue-preview" data-ajax-method="post" data-preview="#issue-preview">Preview</a></li>
+                                    <li class="issue-preview"><a href="#issue-preview" data-toggle="tab" data-ajax="/api/v1/markdown?repoLink={{.RepoLink}}" data-ajax-name="issue-preview" data-ajax-method="post" data-preview="#issue-preview">Preview</a></li>
                                 </ul>
                                 <div class="tab-content">
                                     <div class="tab-pane" id="issue-textarea">

+ 3 - 3
templates/mail/auth/active_email.tmpl

@@ -15,11 +15,11 @@
                         Hi <span style="color: #00BFFF;">{{.User.Name}}</span>,
                     </div>
                     <div style="font-size:14px; padding:0 15px;">
-						<p style="margin:0;padding:0 0 9px 0;">Please click following link to verify your e-mail address within <b>{{.ActiveCodeLives}} hours</b>.</p>
+						<p style="margin:0;padding:0 0 9px 0;">Please click the following link to verify your e-mail address within <b>{{.ActiveCodeLives}} hours</b>.</p>
 						<p style="margin:0;padding:0 0 9px 0;">
 							<a href="{{.AppUrl}}user/activate?code={{.Code}}">{{.AppUrl}}user/activate?code={{.Code}}</a>
 						</p>
-						<p style="margin:0;padding:0 0 9px 0;">Copy and paste it to your browser if the link is not working.</p>
+						<p style="margin:0;padding:0 0 9px 0;">Not working? Try copying and pasting it to your browser.</p>
                     </div>
                 </div>
             </div>
@@ -30,4 +30,4 @@
     </div>
 </div>
 </body>
-</html>
+</html>

+ 4 - 4
templates/mail/auth/register_success.tmpl

@@ -12,14 +12,14 @@
                 <h1 style="font-size:20px; padding:10px 0 20px; margin:0; border-bottom:1px solid #ddd;"><img src="{{.AppUrl}}/{{.AppLogo}}" style="height: 32px; margin-bottom: -10px;"> <a style="color:#333;text-decoration:none;" target="_blank" href="{{.AppUrl}}">{{.AppName}}</a></h1>
                 <div style="padding:40px 15px;">
                     <div style="font-size:16px; padding-bottom:30px; font-weight:bold;">
-                        Hi <span style="color: #00BFFF;">{{.User.Name}}</span>, welcome to register {{.AppName}}!
+                        Hi <span style="color: #00BFFF;">{{.User.Name}}</span>, this is your registration email for {{.AppName}}!
                     </div>
                     <div style="font-size:14px; padding:0 15px;">
-						<p style="margin:0;padding:0 0 9px 0;">Please click following link to verify your e-mail address within <b>{{.ActiveCodeLives}} hours</b>.</p>
+						<p style="margin:0;padding:0 0 9px 0;">Please click the following link to verify your e-mail address within <b>{{.ActiveCodeLives}} hours</b>.</p>
 						<p style="margin:0;padding:0 0 9px 0;">
 							<a href="{{.AppUrl}}user/activate?code={{.Code}}">{{.AppUrl}}user/activate?code={{.Code}}</a>
 						</p>
-						<p style="margin:0;padding:0 0 9px 0;">Copy and paste it to your browser if the link is not working.</p>
+						<p style="margin:0;padding:0 0 9px 0;">Not working? Try copying and pasting it to your browser.</p>
                     </div>
                 </div>
             </div>
@@ -30,4 +30,4 @@
     </div>
 </div>
 </body>
-</html>
+</html>

+ 33 - 0
templates/mail/auth/reset_passwd.tmpl

@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>{{.User.Name}}, please reset your password</title>
+</head>
+<body style="background:#eee;">
+<div style="color:#333; font:12px/1.5 Tahoma,Arial,sans-serif;; text-shadow:1px 1px #fff; padding:0; margin:0;">
+    <div style="width:600px;margin:0 auto; padding:40px 0 20px;">
+        <div style="border:1px solid #d9d9d9;border-radius:3px; background:#fff; box-shadow: 0px 2px 5px rgba(0, 0, 0,.05); -webkit-box-shadow: 0px 2px 5px rgba(0, 0, 0,.05);">
+            <div style="padding: 20px 15px;">
+                <h1 style="font-size:20px; padding:10px 0 20px; margin:0; border-bottom:1px solid #ddd;"><img src="{{.AppUrl}}/{{.AppLogo}}" style="height: 32px; margin-bottom: -10px;"> <a style="color:#333;text-decoration:none;" target="_blank" href="{{.AppUrl}}">{{.AppName}}</a></h1>
+                <div style="padding:40px 15px;">
+                    <div style="font-size:16px; padding-bottom:30px; font-weight:bold;">
+                        Hi <span style="color: #00BFFF;">{{.User.Name}}</span>,
+                    </div>
+                    <div style="font-size:14px; padding:0 15px;">
+						<p style="margin:0;padding:0 0 9px 0;">Please click the following link to reset your password within <b>{{.ActiveCodeLives}} hours</b>.</p>
+						<p style="margin:0;padding:0 0 9px 0;">
+							<a href="{{.AppUrl}}user/reset_password?code={{.Code}}">{{.AppUrl}}user/reset_password?code={{.Code}}</a>
+						</p>
+						<p style="margin:0;padding:0 0 9px 0;">Not working? Try copying and pasting it to your browser.</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div style="color:#aaa;padding:10px;text-align:center;">
+            © 2014 <a style="color:#888;text-decoration:none;" target="_blank" href="http://gogits.org">Gogs: Go Git Service</a>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 27 - 33
templates/release/list.tmpl

@@ -5,55 +5,52 @@
 <div id="body" class="container">
     <div id="release">
         <h4 id="release-head">
-            <span class="release"><strong>Release</strong></span> /
-            <a class="tag" href="/{tag_link}">Tags</a>
+            <span class="release"><strong>Releases</strong></span><!--  /
+            <a class="tag" href="/{tag_link}">Tags</a> -->
             <!-- comment : if in tag page, show a.release and span.tag please -->
         </h4>
         <ul id="release-list" class="list-unstyled">
-            <li class="release-item release-tag clearfix" id="release-tag-{release_tag_id}">
+            {{range .Releases}}
+            <li class="release-item clearfix" id="release-{{.SHA1}}">
+                {{if .PublisherId}}
                 <div class="col-md-2 text-right">
-                    <a class="commit" href="{commit_link}"><i class="fa fa-code"></i>commit-sha</a>
+                    {{if .IsPrerelease}}<span class="btn btn-warning status pre-release">Pre-Release</span>{{else}}<span class="btn btn-success status stable">Stable</span>{{end}}
+                    <a class="tag" href="{{$.RepoLink}}/src/{{.TagName}}"><i class="fa fa-tag"></i>{{.TagName}}</a>
+                    <a class="commit" href="{{$.RepoLink}}/src/{{.SHA1}}"><i class="fa fa-code"></i>{{ShortSha .SHA1}}</a>
                 </div>
                 <div class="col-md-10">
-                    <h5 class="title"><a href="{release_single_link}">Release Tag</a><i class="fa fa-tag"></i></h5>
+                    <h4 class="title"><a href="{{$.RepoLink}}/src/{{.TagName}}">{{.Title}}</a></h4>
                     <p class="info">
-                        <span class="author"><img class="avatar" src="http://1.gravatar.com/avatar/f72f7454ce9d710baa506394f68f4132" alt="" width="20">&nbsp;&nbsp;
-                        <a href="/user/fuxiaohei">fuxiaohei</a></span>
-                        <span class="time">1 week ago</span>
-                        <span class="ahead"><strong>0</strong> commits since this tag</span>
+                        <span class="author"><img class="avatar" src="{{.Publisher.AvatarLink}}" alt="" width="20">&nbsp;&nbsp;
+                        <a href="/user/{{.Publisher.Name}}">{{.Publisher.Name}}</a></span>
+                        {{if .Created}}<span class="time">{{TimeSince .Created}}</span>{{end}}
+                        <span class="ahead"><strong>{{.NumCommitsBehind}}</strong> commits since this release</span>
                     </p>
+                    <div class="markdown desc">
+                        {{str2html .Note}}
+                    </div>
                     <p class="download">
-                        <a class="download-link" href="{release_download_link}"><i class="fa fa-download"></i>zip</a>
-                        <a class="download-link" href="{release_download_link}"><i class="fa fa-download"></i>tar.gz</a>
+                        <a class="btn btn-default" href="{{$.RepoLink}}/archive/{{.TagName}}/{{$.Repository.Name}}.zip"><i class="fa fa-download"></i>Source Code (ZIP)</a>
+                        <!-- <a class="btn btn-default" href="{release_download_link}"><i class="fa fa-download"></i>Source Code (TAR.GZ)</a> -->
                     </p>
                     <span class="dot">&nbsp;</span>
                 </div>
-            </li>
-            <li class="release-item clearfix" id="release-{release_id}">
+                {{else}}
                 <div class="col-md-2 text-right">
-                    <span class="btn btn-success status stable">Stable</span>
-                    <a class="tag" href="{commit_link}"><i class="fa fa-tag"></i>release tag</a>
-                    <a class="commit" href="{commit_link}"><i class="fa fa-code"></i>commit-sha</a>
+                    <a class="commit" href="{{$.RepoLink}}/src/{{.SHA1}}"><i class="fa fa-code"></i>{{ShortSha .SHA1}}</a>
                 </div>
                 <div class="col-md-10">
-                    <h4 class="title"><a href="{release_single_link}">Release Title</a></h4>
-                    <p class="info">
-                        <span class="author"><img class="avatar" src="http://1.gravatar.com/avatar/f72f7454ce9d710baa506394f68f4132" alt="" width="20">&nbsp;&nbsp;
-                        <a href="/user/fuxiaohei">fuxiaohei</a></span>
-                        <span class="time">1 week ago</span>
-                        <span class="ahead"><strong>0</strong> commits since this tag</span>
-                    </p>
-                    <div class="markdown desc">
-                        release descriptions, support markdown content
-                    </div>
+                    <h5 class="title"><a href="{{$.RepoLink}}/src/{{.TagName}}">{{.TagName}}</a><i class="fa fa-tag"></i></h5>
                     <p class="download">
-                        <a class="btn btn-default" href="{release_download_link}"><i class="fa fa-download"></i>Source Code (ZIP)</a>
-                        <a class="btn btn-default" href="{release_download_link}"><i class="fa fa-download"></i>Source Code (TAR.GZ)</a>
+                        <a class="download-link" href="{{$.RepoLink}}/archive/{{.TagName}}/{{$.Repository.Name}}.zip"><i class="fa fa-download"></i>zip</a>
+                        <!-- <a class="download-link" href="{release_download_link}"><i class="fa fa-download"></i>tar.gz</a> -->
                     </p>
                     <span class="dot">&nbsp;</span>
                 </div>
+                {{end}}
             </li>
-            <li class="release-item clearfix" id="release-{release_id}">
+            {{end}}
+            <!-- <li class="release-item clearfix" id="release-{release_id}">
                 <div class="col-md-2 text-right">
                     <span class="btn btn-warning status pre-release">Pre-Release</span>
                     <a class="tag" href="{commit_link}"><i class="fa fa-tag"></i>release tag</a>
@@ -76,11 +73,8 @@
                     </p>
                     <span class="dot">&nbsp;</span>
                 </div>
-            </li>
+            </li> -->
         </ul>
     </div>
-    {{range .Releases}}
-        {{.}}
-    {{end}}
 </div>
 {{template "base/footer" .}}

+ 70 - 0
templates/release/new.tmpl

@@ -0,0 +1,70 @@
+{{template "base/head" .}}
+{{template "base/navbar" .}}
+{{template "repo/nav" .}}
+{{template "repo/toolbar" .}}
+<div id="body" class="container">
+    <div id="release">
+        <h4 id="release-head">New Release</h4>
+        {{template "base/alert" .}}
+        <form id="release-new-form" action="{{.RepoLink}}/releases/new" method="post" class="form form-inline">
+            {{.CsrfTokenHtml}}
+            <div class="form-group">
+                <input id="tag-name" name="tag_name" type="text" class="form-control" placeholder="tag name" value="{{.tag_name}}" />
+                <span class="target-at">@</span>
+                <div class="btn-group" id="release-new-target-select">
+                    <button type="button" class="btn btn-default"><i class="fa fa-code-fork fa-lg fa-m"></i>
+                        <span class="target-text">Target : </span>
+                        <strong id="release-new-target-name"> {{.Repository.DefaultBranch}}</strong>
+                    </button>
+                    <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
+                        <span class="caret"></span>
+                    </button>
+                    <div class="dropdown-menu clone-group-btn" id="release-new-target-branch-list">
+                        <ul class="list-group">
+                            {{range .Branches}}
+                            <li class="list-group-item">
+                                <a href="#" rel="{{.}}"><i class="fa fa-code-fork"></i>{{.}}</a>
+                            </li>
+                            {{end}}
+                        </ul>
+                    </div>
+                    <input id="tag-target" type="hidden" name="tag_target" value="{{.Repository.DefaultBranch}}"/>
+                </div>
+                <p class="help-block">Choose an existing tag, or create a new tag on publish</p>
+            </div>
+            <div class="form-group" style="display: block">
+                <input class="form-control input-lg" id="release-new-title" name="title" type="text" placeholder="release title" value="{{.title}}" />
+            </div>
+            <div class="form-group col-md-8" style="display: block" id="release-new-content-div">
+                <div class="md-help pull-right">
+                    Content with <a href="https://help.github.com/articles/markdown-basics">Markdown</a>
+                </div>
+                <ul class="nav nav-tabs" data-init="tabs">
+                    <li class="release-write active"><a href="#release-textarea" data-toggle="tab">Write</a></li>
+                    <li class="release-preview"><a href="#release-preview" data-toggle="tab" data-ajax="/api/v1/markdown?repo=repo_id&amp;release=new" data-ajax-name="release-preview" data-ajax-method="post" data-preview="#release-preview">Preview</a></li>
+                </ul>
+                <div class="tab-content">
+                    <div class="tab-pane active" id="release-textarea">
+                        <div class="form-group">
+                            <textarea class="form-control" name="content" id="release-new-content" rows="10" placeholder="Write some content" data-ajax-rel="release-preview" data-ajax-val="val" data-ajax-field="content">{{.content}}</textarea>
+                        </div>
+                    </div>
+                    <div class="tab-pane release-preview-content" id="release-preview">loading...</div>
+                </div>
+            </div>
+            <div class="text-right form-group col-md-8" style="display: block">
+                <hr/>
+                <label for="release-new-pre-release">
+                    <input id="release-new-pre-release" type="checkbox" name="prerelease" {{if .prerelease}}checked{{end}}/>
+                    <strong>This is a pre-release</strong>
+                </label>
+                <p class="help-block">We’ll point out that this release is identified as non-production ready.</p>
+            </div>
+            <div class="text-right form-group col-md-8" style="display: block">
+                <button class="btn-success btn">Publish release</button>
+                <!-- <input class="btn btn-default" type="submit" name="draft" value="Save Draft"/> -->
+            </div>
+        </form>
+    </div>
+</div>
+{{template "base/footer" .}}

+ 16 - 7
templates/repo/commits.tmpl

@@ -6,16 +6,21 @@
     <div id="commits">
         <div class="panel panel-default commit-box info-box">
             <div class="panel-heading info-head">
-                <div class="search pull-right form">
-                    <input class="form-control search" type="search" placeholder="search commit"/>
-                </div>
+                <form class="search pull-right col-md-3" action="{{.RepoLink}}/commits/{{.BranchName}}/search" method="get" id="commits-search-form">
+                    <div class="input-group">
+                        <input class="form-control search" type="search" placeholder="search commit" name="q" value="{{.Keyword}}" />
+                        <div class="input-group-btn">
+                            <button type="submit" class="btn btn-default">Find</button>
+                        </div>
+                    </div>
+                </form>
                 <h4>{{.CommitCount}} Commits</h4>
             </div>
             <table class="panel-footer table commit-list table table-striped">
                 <thead>
                     <tr>
                         <th class="author">Author</th>
-                        <th class="sha">Commit</th>
+                        <th class="sha">SHA1</th>
                         <th class="message">Message</th>
                         <th class="date">Date</th>
                     </tr>
@@ -26,15 +31,19 @@
                 {{$r := List .Commits}}
                 {{range $r}}
                 <tr>
-                    <td class="author"><img class="avatar" src="{{AvatarLink .Committer.Email}}" alt=""/><a href="/user/{{.Committer.Name}}">{{.Committer.Name}}</a></td>
-                    <td class="sha"><a class="label label-success" href="/{{$username}}/{{$reponame}}/commit/{{.Id}} ">{{SubStr .Id.String 0 10}} </a></td>
+                    <td class="author"><img class="avatar" src="{{AvatarLink .Author.Email}}" alt=""/><a href="/user/email2user?email={{.Author.Email}}">{{.Author.Name}}</a></td>
+                    <td class="sha"><a rel="nofollow" class="label label-success" href="/{{$username}}/{{$reponame}}/commit/{{.Id}} ">{{SubStr .Id.String 0 10}} </a></td>
                     <td class="message">{{.Message}} </td>
-                    <td class="date">{{TimeSince .Committer.When}}</td>
+                    <td class="date">{{TimeSince .Author.When}}</td>
                 </tr>
                 {{end}}
                 </tbody>
             </table>
         </div>
+        {{if not .IsSearchPage}}<ul class="pagination" id="commits-pager">
+            {{if .LastPageNum}}<li><a href="{{.RepoLink}}/commits/{{.BranchName}}?p={{.LastPageNum}}">&laquo; Newer</a></li>{{end}}
+            {{if .NextPageNum}}<li><a href="{{.RepoLink}}/commits/{{.BranchName}}?p={{.NextPageNum}}">&raquo; Older</a></li>{{end}}
+        </ul>{{end}}
     </div>
 </div>
 {{template "base/footer" .}}

+ 10 - 4
templates/repo/create.tmpl

@@ -4,7 +4,7 @@
     <form action="/repo/create" method="post" class="form-horizontal card" id="repo-create">
         {{.CsrfTokenHtml}}
         <h3>Create New Repository</h3>
-        <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div>
+        {{template "base/alert" .}}
         <div class="form-group">
             <label class="col-md-2 control-label">Owner<strong class="text-danger">*</strong></label>
             <div class="col-md-8">
@@ -22,10 +22,14 @@
         </div>
 
         <div class="form-group">
-            <label class="col-md-2 control-label">Visibility<strong class="text-danger">*</strong></label>
+            <label class="col-md-2 control-label">Visibility</label>
             <div class="col-md-8">
-                <p class="form-control-static">Public</p>
-                <input type="hidden" value="public" name="visibility"/>
+                <div class="checkbox">
+                    <label>
+                        <input type="checkbox" name="private" {{if .private}}checked{{end}}>
+                        <strong>This repository is private</strong>
+                    </label>
+                </div>
             </div>
         </div>
 
@@ -43,6 +47,8 @@
                     <option value="">Select a language</option>
                     {{range .LanguageIgns}}<option value="{{.}}">{{.}}</option>{{end}}
                 </select>
+                <br>
+                <div>Need more .gitignore? Go <a href="http://www.gitignore.io/">gitignore.io</a>.</div>
             </div>
         </div>
 

+ 15 - 329
templates/repo/diff.tmpl

@@ -5,7 +5,7 @@
     <div id="source">
         <div class="panel panel-info diff-box diff-head-box">
             <div class="panel-heading">
-                <a class="pull-right btn btn-primary btn-sm" href="{{.SourcePath}}">Browse Source</a>
+                <a class="pull-right btn btn-primary btn-sm" rel="nofollow" href="{{.SourcePath}}">Browse Source</a>
                 <h4>{{.Commit.Message}}</h4>
             </div>
             <div class="panel-body">
@@ -14,12 +14,15 @@
                 </span>
                 <p class="author">
                     <img class="avatar" src="{{AvatarLink .Commit.Author.Email}}" alt=""/>
-                    <a class="name" href="#"><strong>{{.Commit.Author.Name}}</strong></a>
+                    <a class="name" href="/user/email2user?email={{.Commit.Author.Email}}"><strong>{{.Commit.Author.Name}}</strong></a>
                     <span class="time">{{TimeSince .Commit.Author.When}}</span>
                 </p>
             </div>
         </div>
 
+        {{if .DiffNotAvailable}}
+        <h4>Diff Data Not Available.</h4>
+        {{else}}
         <div class="diff-detail-box diff-box">
             <a class="pull-right btn btn-default" data-toggle="collapse" data-target="#diff-files">Show Diff Stats</a>
             <p class="showing">
@@ -30,12 +33,16 @@
                 {{range .Diff.Files}}
                 <li>
                     <div class="diff-counter count pull-right">
+                        {{if not .IsBin}}
                         <span class="add" data-line="{{.Addition}}">{{.Addition}}</span>
                         <span class="bar">
                             <span class="pull-left add"></span>
                             <span class="pull-left del"></span>
                         </span>
                         <span class="del" data-line="{{.Deletion}}">{{.Deletion}}</span>
+                        {{else}}
+                        <span>BIN</span>
+                        {{end}}
                     </div>
                     <!-- todo finish all file status, now modify, add, delete and rename -->
                     <span class="status {{DiffTypeToStr .Type}}" data-toggle="tooltip" data-placement="right" title="{{DiffTypeToStr .Type}}">&nbsp;</span>
@@ -49,14 +56,18 @@
         <div class="panel panel-default diff-file-box diff-box file-content" id="diff-2">
             <div class="panel-heading">
                 <div class="diff-counter count pull-left">
+                    {{if not .IsBin}}
                     <span class="add" data-line="{{.Addition}}">+ {{.Addition}}</span>
                     <span class="bar">
                         <span class="pull-left add"></span>
                         <span class="pull-left del"></span>
                     </span>
                     <span class="del" data-line="{{.Deletion}}">- {{.Deletion}}</span>
+                    {{else}}
+                    BIN
+                    {{end}}
                 </div>
-                <a class="btn btn-default btn-sm pull-right" href="{{$.SourcePath}}/{{.Name}}">View File</a>
+                <a class="btn btn-default btn-sm pull-right" rel="nofollow" href="{{$.SourcePath}}/{{.Name}}">View File</a>
                 <span class="file">{{.Name}}</span>
             </div>
             {{$isImage := (call $.IsImageFile .Name)}}
@@ -83,338 +94,13 @@
                         </tr>
                         {{end}}
                         {{end}}
-                       <!--  <tr class="same-code nl-2 ol-2">
-                            <td class="lines-num lines-num-old">
-                                <span rel="L1">2</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="L1">2</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="same-code nl-3 ol-3">
-                            <td class="lines-num lines-num-old">
-                                <span rel="L3">3</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="L3">3</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="add-code nl-4 ol-0">
-                            <td class="lines-num lines-num-old">
-                                <span rel="add">+</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="L4">4</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="add-code nl-5 ol-0">
-                            <td class="lines-num lines-num-old">
-                                <span rel="add">+</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="L5">5</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="del-code nl-0 ol-4">
-                            <td class="lines-num lines-num-old">
-                                <span rel="L4">4</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="del">-</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="del-code nl-0 ol-5">
-                            <td class="lines-num lines-num-old">
-                                <span rel="L5">5</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="del">-</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="del-code nl-0 ol-6">
-                            <td class="lines-num lines-num-old">
-                                <span rel="L6">6</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="del">-</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="del-code nl-0 ol-7">
-                            <td class="lines-num lines-num-old">
-                                <span rel="L7">7</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="del">-</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="same-code nl-6 ol-8">
-                            <td class="lines-num lines-num-old">
-                                <span rel="L8">8</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="L6">6</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="same-code nl-7 ol-9">
-                            <td class="lines-num lines-num-old">
-                                <span rel="L1">9</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="L1">7</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr>
-                        <tr class="same-code nl-8 ol-10">
-                            <td class="lines-num lines-num-old">
-                                <span rel="L1">10</span>
-                            </td>
-                            <td class="lines-num lines-num-new">
-                                <span rel="L1">8</span>
-                            </td>
-                            <td class="lines-code">
-                                <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                            </td>
-                        </tr> -->
                     </tbody>
                 </table>
                 {{end}}
             </div>
         </div>
         {{end}}
-
-        <!-- <div class="panel panel-default diff-file-box diff-box file-content">
-            <div class="panel-heading">
-                <div class="diff-counter count pull-left">
-                    <span class="add" data-line="2">+ 2</span>
-                    <span class="bar">
-                        <span class="pull-left add"></span>
-                        <span class="pull-left del"></span>
-                    </span>
-                    <span class="del" data-line="4">- 4</span>
-                </div>
-                <a class="btn btn-default btn-sm pull-right" href="#">View File</a>
-                <span class="file">data/test/bson_test/simple_type.go</span>
-            </div>
-            <div class="panel-body file-body file-code code-view code-diff">
-                <table>
-                    <tbody>
-                    <tr class="same-code nl-1 ol-1">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L1">1</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L1">1</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="same-code nl-2 ol-2">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L1">2</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L1">2</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="same-code nl-3 ol-3">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L3">3</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L3">3</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="add-code nl-4 ol-0">
-                        <td class="lines-num lines-num-old">
-                            <span rel="add">+</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L4">4</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="add-code nl-5 ol-0">
-                        <td class="lines-num lines-num-old">
-                            <span rel="add">+</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L5">5</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="del-code nl-0 ol-4">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L4">4</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="del">-</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="del-code nl-0 ol-5">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L5">5</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="del">-</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="del-code nl-0 ol-6">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L6">6</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="del">-</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="del-code nl-0 ol-7">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L7">7</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="del">-</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="same-code nl-6 ol-8">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L8">8</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L6">6</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="same-code nl-7 ol-9">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L1">9</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L1">7</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="same-code nl-8 ol-10">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L1">10</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L1">8</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="ellipsis-code">
-                        <td class="text-center lines-ellipsis" colspan="2">
-                            <i class="fa fa-ellipsis-h"></i>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="same-code nl-8 ol-10">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L1">10</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L1">8</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    <tr class="same-code nl-8 ol-10">
-                        <td class="lines-num lines-num-old">
-                            <span rel="L1">10</span>
-                        </td>
-                        <td class="lines-num lines-num-new">
-                            <span rel="L1">8</span>
-                        </td>
-                        <td class="lines-code">
-                            <pre>	"github.com/youtube/vitess/go/bson"</pre>
-                        </td>
-                    </tr>
-                    </tbody>
-                </table>
-            </div>
-        </div>
-
-        <div class="panel panel-default diff-file-box diff-box file-content">
-            <div class="panel-heading">
-                <div class="diff-counter count pull-left">
-                    <span class="add" data-line="0">BIN</span>
-                    <span class="bar">
-                        <span class="pull-left add"></span>
-                        <span class="pull-left del"></span>
-                    </span>
-                    <span class="del" data-line="1"></span>
-                </div>
-                <a class="btn btn-default btn-sm pull-right" href="#">View File</a>
-                <span class="file">data/test/bson_test/simple_type.png</span>
-            </div>
-            <div class="panel-body file-body file-code code-view code-bin">
-                <table>
-                    <tbody>
-                    <tr class="text-center"><td><img src="http://1.gravatar.com/avatar/f72f7454ce9d710baa506394f68f4132?s=200" alt=""/></td></tr>
-                    </tbody>
-                </table>
-            </div>
-        </div> -->
+        {{end}}
     </div>
 </div>
 {{template "base/footer" .}}

+ 99 - 0
templates/repo/migrate.tmpl

@@ -0,0 +1,99 @@
+{{template "base/head" .}}
+{{template "base/navbar" .}}
+<div class="container" id="body">
+    <form action="/repo/migrate" method="post" class="form-horizontal card" id="repo-create">
+        {{.CsrfTokenHtml}}
+        <h3>Repository Migration</h3>
+        {{template "base/alert" .}}
+        <!-- <div class="form-group">
+            <label class="col-md-2 control-label">From<strong class="text-danger">*</strong></label>
+            <div class="col-md-8">
+                <select class="form-control" name="from">
+                    <option value="github">GitHub</option>
+                </select>
+            </div>
+        </div> -->
+
+        <div class="form-group">
+            <label class="col-md-2 control-label">HTTPS URL<strong class="text-danger">*</strong></label>
+            <div class="col-md-8">
+                <input name="url" type="text" class="form-control" placeholder="Type your migration repository HTTPS URL" value="{{.url}}" required="required" >
+            </div>
+        </div>
+
+        <div class="form-group">
+            <div class="col-md-offset-2 col-md-8">
+                <a class="btn btn-default" data-toggle="collapse" data-target="#repo-import-auth">Need Authorization</a>
+            </div>
+            <div id="repo-import-auth" class="collapse">
+                <div class="form-group">
+                    <label class="col-md-2 control-label">Username</label>
+                    <div class="col-md-8">
+                        <input name="auth_username" type="text" class="form-control" placeholder="Type your user name" value="{{.auth_username}}" >
+                    </div>
+                </div>
+                <div class="form-group">
+                    <label class="col-md-2 control-label">Password</label>
+                    <div class="col-md-8">
+                        <input name="auth_password" type="password" class="form-control" placeholder="Type your password" value="{{.auth_password}}" >
+                    </div>
+                </div>
+            </div>
+        </div>
+        <hr/>
+        <div class="form-group">
+            <label class="col-md-2 control-label">Owner<strong class="text-danger">*</strong></label>
+            <div class="col-md-8">
+                <p class="form-control-static">{{.SignedUserName}}</p>
+                <input type="hidden" value="{{.SignedUserId}}" name="userId"/>
+            </div>
+        </div>
+
+        <div class="form-group {{if .Err_RepoName}}has-error has-feedback{{end}}">
+            <label class="col-md-2 control-label">Repository<strong class="text-danger">*</strong></label>
+            <div class="col-md-8">
+                <input name="repo" type="text" class="form-control" placeholder="Type your repository name" value="{{.repo}}" required="required">
+                <span class="help-block">Great repository names are short and memorable. </span>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <label class="col-md-2 control-label">Migration Type</label>
+            <div class="col-md-8">
+                <div class="checkbox">
+                    <label>
+                        <input type="checkbox" name="mirror" {{if .mirror}}checked{{end}}>
+                        <strong>This repository is a mirror</strong>
+                    </label>
+                </div>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <label class="col-md-2 control-label">Visibility</label>
+            <div class="col-md-8">
+                <div class="checkbox">
+                    <label>
+                        <input type="checkbox" name="private" {{if .private}}checked{{end}}>
+                        <strong>This repository is private</strong>
+                    </label>
+                </div>
+            </div>
+        </div>
+
+        <div class="form-group {{if .Err_Description}}has-error has-feedback{{end}}">
+            <label class="col-md-2 control-label">Description</label>
+            <div class="col-md-8">
+                <textarea name="desc" class="form-control" placeholder="Type your repository description">{{.desc}}</textarea>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <div class="col-md-offset-2 col-md-8">
+                <button type="submit" class="btn btn-lg btn-primary">Migrate repository</button>
+                <a href="/" class="text-danger">Cancel</a>
+            </div>
+        </div>
+    </form>
+</div>
+{{template "base/footer" .}}

+ 6 - 6
templates/repo/nav.tmpl

@@ -2,13 +2,13 @@
     <div class="container">
         <div class="row">
             <div class="col-md-7">
-                <h3 class="name"><i class="fa fa-book fa-lg"></i><a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="/{{.Owner.Name}}/{{.Repository.Name}}">{{.Repository.Name}}</a></h3>
+                <h3 class="name"><i class="fa fa-book fa-lg"></i><a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="/{{.Owner.Name}}/{{.Repository.Name}}">{{.Repository.Name}}</a> {{if .Repository.IsPrivate}}<span class="label label-default">Private</span>{{else if .Repository.IsMirror}}<span class="label label-default">Mirror</span>{{end}}</h3>
                 <p class="desc">{{.Repository.Description}}{{if .Repository.Website}} <a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}</p>
             </div>
             <div class="col-md-5 actions text-right clone-group-btn">
                 {{if not .IsBareRepo}}
                 <div class="btn-group" id="repo-clone">
-                    <button type="button" class="btn btn-default"><i class="fa fa-download fa-lg fa-m"></i></button>
+                    <a class="btn btn-default" href="{{.RepoLink}}/archive/{{.BranchName}}/{{.Repository.Name}}.zip"><i class="fa fa-download fa-lg fa-m"></i></a>
                     <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
                         <span class="caret"></span>
                     </button>
@@ -24,10 +24,10 @@
                             </span>
                         </div>
                         <p class="help-block text-center">Need help cloning? Visit <a href="#">Help</a>!</p>
-                        <!-- <hr/>
+                        <hr/>
                         <div class="clone-zip text-center">
-                            <a class="btn btn-success btn-lg" href="#"><i class="fa fa-suitcase"></i>Download ZIP</a>
-                        </div> -->
+                            <a class="btn btn-success btn-lg" href="{{.RepoLink}}/archive/{{.BranchName}}/{{.Repository.Name}}.zip"><i class="fa fa-suitcase"></i>Download ZIP</a>
+                        </div>
                     </div>
                 </div>
                 <div class="btn-group {{if .IsRepositoryWatching}}watching{{else}}no-watching{{end}}" id="repo-watching" data-watch="/{{.Owner.Name}}/{{.Repository.Name}}/action/watch" data-unwatch="/{{.Owner.Name}}/{{.Repository.Name}}/action/unwatch">
@@ -61,4 +61,4 @@
             </div>
         </div>
     </div>
-</div>
+</div>

+ 38 - 7
templates/repo/setting.tmpl

@@ -12,7 +12,7 @@
     </div>
 
     <div id="repo-setting-container" class="col-md-9">
-        {{if .IsSuccess}}<p class="alert alert-success">Repository options has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}}
+        {{template "base/alert" .}}
         <div class="panel panel-default">
             <div class="panel-heading">
                 Repository Options
@@ -23,9 +23,10 @@
                     {{.CsrfTokenHtml}}
                     <input type="hidden" name="action" value="update">
                     <div class="form-group">
-                        <label class="col-md-3 text-right">Name</label>
+                        <label class="col-md-3 text-right" for="repo-setting-name">Name</label>
                         <div class="col-md-9">
-                            <input class="form-control" name="name" value="{{.Repository.Name}}" title="{{.Repository.Name}}" />
+                            <input class="form-control" name="name" value="{{.Repository.Name}}" title="{{.Repository.Name}}" id="repo-setting-name"/>
+                            <p class="help-block hidden"><span class="text-danger">Cautious : </span>your repository name is changing !</p>
                         </div>
                     </div>
 
@@ -42,14 +43,44 @@
                             <input type="url" class="form-control" name="site" value="{{.Repository.Website}}" />
                         </div>
                     </div>
-                    <!-- <div class="form-group">
+                    <hr>
+                    <div class="form-group">
                         <label class="col-md-3 text-right">Default Branch</label>
-                        <div class="col-md-9">
+                        <div class="col-md-3">
                             <select name="branch" id="repo-default-branch" class="form-control">
-                                <option value="">Branch</option>
+                                <option value="{{.Repository.DefaultBranch}}">{{.Repository.DefaultBranch}}</option>
+                                {{range .Branches}}
+                                {{if eq . $.Repository.DefaultBranch}}{{else}}<option value="{{.}}">{{.}}</option>{{end}}
+                                {{end}}
                             </select>
                         </div>
-                    </div> -->
+                    </div>
+
+                    {{if .Repository.IsMirror}}<div class="form-group">
+                        <label class="col-md-3 text-right">Mirror Interval(hours)</label>
+                        <div class="col-md-3">
+                            <input class="form-control" name="interval" value="{{.MirrorInterval}}"/>
+                        </div>
+                    </div>{{end}}
+
+                    <div class="form-group">
+                        <div class="col-md-offset-3 col-md-9">
+                            <div class="checkbox">
+                                <label style="line-height: 15px;">
+                                    <input type="checkbox" name="private" {{if .Repository.IsPrivate}}checked{{end}}>
+                                    <strong>Make this repository private</strong>
+                                </label>
+                            </div>
+
+                            <div class="checkbox">
+                                <label style="line-height: 15px;">
+                                    <input type="checkbox" name="goget" {{if .Repository.IsGoget}}checked{{end}}>
+                                    <strong>Enable 'go get' meta</strong>
+                                </label>
+                            </div>
+                        </div>
+                    </div>
+
                     <div class="form-group">
                         <div class="col-md-9 col-md-offset-3">
                             <button class="btn btn-primary" type="submit">Save Options</button>

+ 30 - 33
templates/repo/single_list.tmpl

@@ -1,6 +1,6 @@
 <div class="panel panel-default info-box">
     <div class="panel-heading info-head">
-        <a href="/{{.Username}}/{{.Reponame}}/commit/{{.LastCommit.Oid.String}}">{{.LastCommit.Message}}</a>
+        <a href="/{{.Username}}/{{.Reponame}}/commit/{{.LastCommit.Id}}">{{.LastCommit.Message}}</a>
     </div>
     <div class="panel-body info-content">
         <a href="/user/{{.LastCommit.Author.Name}}">{{.LastCommit.Author.Name}}</a> <span class="text-muted">{{TimeSince .LastCommit.Author.When}}</span>
@@ -15,40 +15,37 @@
         </tr>
         </thead>
         <tbody>
-        {{if .HasParentPath}}
-            <tr class="has-parent">
-                <td class="icon"><a href="{{.BranchLink}}{{.ParentPath}}"><i class="fa fa-reply"></i></a></td>
-                <td class="name"><a href="{{.BranchLink}}{{.ParentPath}}">..</a></td>
-                <td class="text"></td>
-                <td class="date"></td>
-            </tr>
-        {{end}}
-        {{range .Files}}
-        <tr
-        {{if .IsDir}}class="is-dir"{{end}}>
-        <td class="icon">
-            <i class="fa {{if .IsDir}}fa-folder{{else}}fa-file-text-o{{end}}"></i>
-        </td>
-        <td class="name">
-            <span class="wrap">
-                {{if .IsDir}}
-                <a href="{{$.BranchLink}}/{{.Path}}">{{.Name}}</a>
-                {{else}}
-                <a href="{{$.BranchLink}}/{{.Path}}">{{.Name}}</a>
-                {{end}}
-            </span>
-        </td>
-        <td class="text">
-            <span class="wrap"><a href="/{{$.Username}}/{{$.Reponame}}/commit/{{.Commit.Oid}}">{{.Commit.Message}}</a></span>
-        </td>
-        <td class="date">
-            <span class="wrap">{{TimeSince .Commit.Committer.When}}</span>
-        </td>
-        </tr>
-        {{end}}
+            {{if .HasParentPath}}
+                <tr class="has-parent">
+                    <td class="icon"><a href="{{.BranchLink}}{{.ParentPath}}"><i class="fa fa-reply"></i></a></td>
+                    <td class="name"><a href="{{.BranchLink}}{{.ParentPath}}">..</a></td>
+                    <td class="text"></td>
+                    <td class="date"></td>
+                </tr>
+            {{end}}
+            {{range $item := .Files}}
+                {{$entry := index $item 0}}
+                {{$commit := index $item 1}}
+                <tr {{if $entry.IsDir}}class="is-dir"{{end}}>
+                    <td class="icon">
+                        <i class="fa {{if $entry.IsDir}}fa-folder{{else}}fa-file-text-o{{end}}"></i>
+                    </td>
+                    <td class="name">
+                        <span class="wrap">
+                            <a href="{{$.BranchLink}}/{{$.TreePath}}{{$entry.Name}}">{{$entry.Name}}</a>
+                        </span>
+                    </td>
+                    <td class="text">
+                        <span class="wrap"><a rel="nofollow" href="/{{$.Username}}/{{$.Reponame}}/commit/{{$commit.Id}}">{{$commit.Message}}</a></span>
+                    </td>
+                    <td class="date">
+                        <span class="wrap">{{TimeSince $commit.Committer.When}}</span>
+                    </td>
+                </tr>
+            {{end}}
         </tbody>
     </table>
 </div>
 {{if .ReadmeExist}}
     {{template "repo/single_file" .}}
-{{end}}
+{{end}}

+ 4 - 4
templates/repo/toolbar.tmpl

@@ -11,12 +11,12 @@
                     <li class="{{if .IsRepoToolbarIssues}}active{{end}}"><a href="{{.RepoLink}}/issues">{{if .Repository.NumOpenIssues}}<span class="badge">{{.Repository.NumOpenIssues}}</span> {{end}}Issues <!--<span class="badge">42</span>--></a></li>
                     {{if .IsRepoToolbarIssues}}
                     <li class="tmp">{{if .IsRepoToolbarIssuesList}}<a href="{{.RepoLink}}/issues/new"><button class="btn btn-primary btn-sm">New Issue</button>
-                    </a>{{else}}<a href="{{.RepoLink}}/issues"><button class="btn btn-primary btn-sm">Issues List</button></a>{{end}}</li>
+                    </a>{{end}}</li>
                     {{end}}
-                    <li class="{{if .IsRepoToolbarReleases}}active{{end}}"><a href="{{.RepoLink}}/releases">{{if .Repository.NumReleases}}<span class="badge">{{.Repository.NumReleases}}</span> {{end}}Releases</a></li>
-                    {{if .IsRepoToolbarReleases}}
+                    <li class="{{if .IsRepoToolbarReleases}}active{{end}}"><a href="{{.RepoLink}}/releases">{{if .Repository.NumTags}}<span class="badge">{{.Repository.NumTags}}</span> {{end}}Releases</a></li>
+                    {{if .IsRepoToolbarReleases}}{{if .IsRepositoryOwner}}{{if not .IsRepoReleaseNew}}
                     <li class="tmp"><a href="{{.RepoLink}}/releases/new"><button class="btn btn-primary btn-sm">New Release</button></a></li>
-                    {{end}}
+                    {{end}}{{end}}{{end}}
                     <!-- <li class="dropdown">
                         <a href="#" class="dropdown-toggle" data-toggle="dropdown">More <b class="caret"></b></a>
                         <ul class="dropdown-menu">

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác