Explorar el Código

Merge branch 'dev' of github.com:gogits/gogs into dev

Lunny Xiao hace 11 años
padre
commit
88d873c67f

+ 2 - 3
.fswatch.json

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

+ 2 - 2
README.md

@@ -5,7 +5,7 @@ Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language
 
 ![Demo](http://gowalker.org/public/gogs_demo.gif)
 
-##### Current version: 0.2.2 Alpha
+##### Current version: 0.2.3 Alpha
 
 #### Due to testing purpose, data of [try.gogits.org](http://try.gogits.org) has been reset in April 6, 2014 and will reset multiple times after. Please do NOT put your important data on the site.
 
@@ -29,7 +29,7 @@ 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/transfer public repository.
 - Repository viewer.

+ 2 - 2
README_ZH.md

@@ -5,7 +5,7 @@ Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。
 
 ![Demo](http://gowalker.org/public/gogs_demo.gif)
 
-##### 当前版本:0.2.2 Alpha
+##### 当前版本:0.2.3 Alpha
 
 ## 开发目的
 
@@ -23,7 +23,7 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依
 ## 功能特性
 
 - 活动时间线
-- SSH/HTTPS(仅限 Clone) 协议支持
+- SSH/HTTP(S) 协议支持
 - 注册/删除/重命名用户
 - 创建/删除/关注/重命名/转移公开仓库
 - 仓库浏览器

+ 1 - 1
gogs.go

@@ -19,7 +19,7 @@ import (
 // Test that go1.2 tag above is included in builds. main.go refers to this definition.
 const go12tag = true
 
-const APP_VER = "0.2.2.0407 Alpha"
+const APP_VER = "0.2.3.0409 Alpha"
 
 func init() {
 	base.AppVer = APP_VER

+ 22 - 12
models/oauth2.go

@@ -1,6 +1,10 @@
+// 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 "fmt"
+import "errors"
 
 // OT: Oauth2 Type
 const (
@@ -9,12 +13,18 @@ const (
 	OT_TWITTER
 )
 
+var (
+	ErrOauth2RecordNotExists       = errors.New("not exists oauth2 record")
+	ErrOauth2NotAssociatedWithUser = errors.New("not associated with user")
+)
+
 type Oauth2 struct {
-	Uid      int64  `xorm:"pk"`               // userId
+	Id       int64
+	Uid      int64  // userId
+	User     *User  `xorm:"-"`
 	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"`
 }
 
 func AddOauth2(oa *Oauth2) (err error) {
@@ -24,16 +34,16 @@ func AddOauth2(oa *Oauth2) (err error) {
 	return nil
 }
 
-func GetOauth2User(identity string) (u *User, err error) {
-	oa := &Oauth2{}
-	oa.Identity = identity
-	exists, err := orm.Get(oa)
+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, ErrOauth2RecordNotExists
+	} else if oa.Uid == 0 {
+		return oa, ErrOauth2NotAssociatedWithUser
 	}
-	if !exists {
-		err = fmt.Errorf("not exists oauth2: %s", identity)
-		return
-	}
-	return GetUserById(oa.Uid)
+	oa.User, err = GetUserById(oa.Uid)
+	return oa, err
 }

+ 9 - 4
models/repo.go

@@ -79,6 +79,7 @@ type Repository struct {
 	NumOpenIssues   int `xorm:"-"`
 	IsPrivate       bool
 	IsBare          bool
+	IsGoget         bool
 	Created         time.Time `xorm:"created"`
 	Updated         time.Time `xorm:"updated"`
 }
@@ -261,6 +262,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)
@@ -333,10 +341,7 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
 		return nil
 	}
 
-	// for update use
-	os.Setenv("userName", user.Name)
-	os.Setenv("userId", base.ToStr(user.Id))
-	os.Setenv("repoName", repo.Name)
+	SetRepoEnvs(user.Id, user.Name, repo.Name)
 
 	// Apply changes and commit.
 	return initRepoCommit(tmpDir, user.NewGitSig())

+ 10 - 1
models/user.go

@@ -289,11 +289,21 @@ 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 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
@@ -316,7 +326,6 @@ func DeleteUser(user *User) error {
 	}
 
 	_, err = orm.Delete(user)
-	// TODO: delete and update follower information.
 	return err
 }
 

+ 1 - 0
modules/base/conf.go

@@ -43,6 +43,7 @@ var (
 	AppName      string
 	AppLogo      string
 	AppUrl       string
+	IsProdMode   bool
 	Domain       string
 	SecretKey    string
 	RunUser      string

+ 3 - 3
modules/base/markdown.go

@@ -133,14 +133,14 @@ func RenderSpecialLink(rawBytes []byte, urlPrefix string) []byte {
 }
 
 func RenderMarkdown(rawBytes []byte, urlPrefix string) []byte {
-	// body := RenderSpecialLink(rawBytes, urlPrefix)
+	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
@@ -162,7 +162,7 @@ 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
 }

+ 3 - 0
modules/base/template.go

@@ -56,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"
 	},

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

+ 3 - 12
modules/oauth2/oauth2.go

@@ -1,16 +1,7 @@
 // Copyright 2014 Google Inc. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+// 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 oauth2 contains Martini handlers to provide
 // user login via an OAuth 2.0 backend.

+ 50 - 1
public/css/gogs.css

@@ -309,6 +309,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 +456,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 {
@@ -1372,6 +1421,6 @@ html, body {
     margin: 16px 0;
 }
 
-#release-preview{
+#release-preview {
     margin: 6px 0;
 }

+ 7 - 0
routers/install.go

@@ -7,6 +7,7 @@ package routers
 import (
 	"errors"
 	"os"
+	"os/exec"
 	"strings"
 
 	"github.com/Unknwon/goconfig"
@@ -27,6 +28,7 @@ func checkRunMode() {
 	switch base.Cfg.MustValue("", "RUN_MODE") {
 	case "prod":
 		martini.Env = martini.Prod
+		base.IsProdMode = true
 	case "test":
 		martini.Env = martini.Test
 	}
@@ -102,6 +104,11 @@ func Install(ctx *middleware.Context, form auth.InstallForm) {
 		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"}

+ 31 - 0
routers/repo/repo.go

@@ -53,6 +53,36 @@ func Create(ctx *middleware.Context, form auth.CreateRepoForm) {
 	ctx.Handle(200, "repo.Create", err)
 }
 
+func Mirror(ctx *middleware.Context, form auth.CreateRepoForm) {
+	ctx.Data["Title"] = "Mirror repository"
+	ctx.Data["PageIsNewRepo"] = true // For navbar arrow.
+
+	if ctx.Req.Method == "GET" {
+		ctx.HTML(200, "repo/mirror")
+		return
+	}
+
+	if ctx.HasError() {
+		ctx.HTML(200, "repo/mirror")
+		return
+	}
+
+	_, err := models.CreateRepository(ctx.User, form.RepoName, form.Description,
+		"", form.License, form.Visibility == "private", false)
+	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)
+		return
+	} else if err == models.ErrRepoAlreadyExist {
+		ctx.RenderWithErr("Repository name has already been used", "repo/mirror", &form)
+		return
+	} else if err == models.ErrRepoNameIllegal {
+		ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "repo/mirror", &form)
+		return
+	}
+	ctx.Handle(200, "repo.Mirror", err)
+}
+
 func Single(ctx *middleware.Context, params martini.Params) {
 	branchName := ctx.Repo.BranchName
 	commitId := ctx.Repo.CommitId
@@ -312,6 +342,7 @@ func SettingPost(ctx *middleware.Context) {
 
 		ctx.Repo.Repository.Description = ctx.Query("desc")
 		ctx.Repo.Repository.Website = ctx.Query("site")
+		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

+ 68 - 31
routers/user/social.go

@@ -6,7 +6,10 @@ package user
 
 import (
 	"encoding/json"
+	"net/http"
+	"net/url"
 	"strconv"
+	"strings"
 
 	"code.google.com/p/goauth2/oauth"
 
@@ -70,53 +73,87 @@ func (s *SocialGithub) Update() error {
 	return json.NewDecoder(r.Body).Decode(&s.data)
 }
 
+func extractPath(next string) string {
+	n, err := url.Parse(next)
+	if err != nil {
+		return "/"
+	}
+	return n.Path
+}
+
 // github && google && ...
 func SocialSignIn(ctx *middleware.Context, tokens oauth2.Tokens) {
-	gh := &SocialGithub{
-		WebToken: &oauth.Token{
-			AccessToken:  tokens.Access(),
-			RefreshToken: tokens.Refresh(),
-			Expiry:       tokens.ExpiryTime(),
-			Extra:        tokens.ExtraData(),
-		},
+	var socid int64
+	var ok bool
+	next := extractPath(ctx.Query("next"))
+	log.Debug("social signed check %s", next)
+	if socid, ok = ctx.Session.Get("socialId").(int64); ok && socid != 0 {
+		// already login
+		ctx.Redirect(next)
+		log.Info("login soc id: %v", socid)
+		return
+	}
+	config := &oauth.Config{
+		//ClientId: base.OauthService.Github.ClientId,
+		//ClientSecret: base.OauthService.Github.ClientSecret, // FIXME: I don't know why compile error here
+		ClientId:     "09383403ff2dc16daaa1",
+		ClientSecret: "0e4aa0c3630df396cdcea01a9d45cacf79925fea",
+		RedirectURL:  strings.TrimSuffix(base.AppUrl, "/") + ctx.Req.URL.RequestURI(),
+		Scope:        base.OauthService.GitHub.Scopes,
+		AuthURL:      "https://github.com/login/oauth/authorize",
+		TokenURL:     "https://github.com/login/oauth/access_token",
+	}
+	transport := &oauth.Transport{
+		Config:    config,
+		Transport: http.DefaultTransport,
 	}
-	if len(tokens.Access()) == 0 {
-		log.Error("empty access")
+	code := ctx.Query("code")
+	if code == "" {
+		// redirect to social login page
+		ctx.Redirect(config.AuthCodeURL(next))
 		return
 	}
-	var err error
-	var u *models.User
+
+	// handle call back
+	tk, err := transport.Exchange(code)
+	if err != nil {
+		log.Error("oauth2 handle callback error: %v", err)
+		return // FIXME, need error page 501
+	}
+	next = extractPath(ctx.Query("state"))
+	log.Debug("success token: %v", tk)
+
+	gh := &SocialGithub{WebToken: tk}
 	if err = gh.Update(); err != nil {
-		// FIXME: handle error page
+		// FIXME: handle error page 501
 		log.Error("connect with github error: %s", err)
 		return
 	}
 	var soc SocialConnector = gh
 	log.Info("login: %s", soc.Name())
-	// FIXME: login here, user email to check auth, if not registe, then generate a uniq username
-	if u, err = models.GetOauth2User(soc.Identity()); err != nil {
-		u = &models.User{
-			Name:     soc.Name(),
-			Email:    soc.Email(),
-			Passwd:   "123456",
-			IsActive: !base.Service.RegisterEmailConfirm,
-		}
-		if u, err = models.RegisterUser(u); err != nil {
-			log.Error("register user: %v", err)
-			return
-		}
-		oa := &models.Oauth2{}
-		oa.Uid = u.Id
+	oa, err := models.GetOauth2(soc.Identity())
+	switch err {
+	case nil:
+		ctx.Session.Set("userId", oa.User.Id)
+		ctx.Session.Set("userName", oa.User.Name)
+	case models.ErrOauth2RecordNotExists:
+		oa = &models.Oauth2{}
+		oa.Uid = 0
 		oa.Type = soc.Type()
 		oa.Token = soc.Token()
 		oa.Identity = soc.Identity()
-		log.Info("oa: %v", oa)
+		log.Debug("oa: %v", oa)
 		if err = models.AddOauth2(oa); err != nil {
-			log.Error("add oauth2 %v", err)
+			log.Error("add oauth2 %v", err) // 501
 			return
 		}
+	case models.ErrOauth2NotAssociatedWithUser:
+		// ignore it. judge in /usr/login page
+	default:
+		log.Error(err.Error()) // FIXME: handle error page
+		return
 	}
-	ctx.Session.Set("userId", u.Id)
-	ctx.Session.Set("userName", u.Name)
-	ctx.Redirect("/")
+	ctx.Session.Set("socialId", oa.Id)
+	log.Debug("socialId: %v", oa.Id)
+	ctx.Redirect(next)
 }

+ 14 - 0
routers/user/user.go

@@ -396,6 +396,10 @@ 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
@@ -451,7 +455,17 @@ func ForgotPasswd(ctx *middleware.Context) {
 		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

+ 1 - 4
serve.go

@@ -177,10 +177,7 @@ func runServ(k *cli.Context) {
 		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

+ 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="{{AppDomain}} 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>

+ 10 - 1
templates/base/navbar.tmpl

@@ -8,9 +8,18 @@
             <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/mirror"><i class="fa fa-clipboard"></i>Mirror</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>

+ 2 - 2
templates/install.tmpl

@@ -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}}">

+ 81 - 0
templates/repo/mirror.tmpl

@@ -0,0 +1,81 @@
+{{template "base/head" .}}
+{{template "base/navbar" .}}
+<div class="container" id="body">
+    <form action="/repo/create" method="post" class="form-horizontal card" id="repo-create">
+        {{.CsrfTokenHtml}}
+        <h3>Create Repository Mirror</h3>
+        <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div>
+        <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</option>
+                </select>
+            </div>
+        </div>
+        <div class="form-group">
+            <label class="col-md-2 control-label">URL<strong class="text-danger">*</strong></label>
+            <div class="col-md-8">
+                <input name="url" type="text" class="form-control" placeholder="Type your mirror repository url link" 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">
+                    </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="text" class="form-control">
+                    </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">Visibility<strong class="text-danger">*</strong></label>
+            <div class="col-md-8">
+                <p class="form-control-static">Public</p>
+                <input type="hidden" value="public" name="visibility"/>
+            </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">Mirror repository</button>
+                <a href="/" class="text-danger">Cancel</a>
+            </div>
+        </div>
+    </form>
+</div>
+{{template "base/footer" .}}

+ 13 - 0
templates/repo/setting.tmpl

@@ -43,6 +43,7 @@
                             <input type="url" class="form-control" name="site" value="{{.Repository.Website}}" />
                         </div>
                     </div>
+                    <hr>
                     <!-- <div class="form-group">
                         <label class="col-md-3 text-right">Default Branch</label>
                         <div class="col-md-9">
@@ -51,6 +52,18 @@
                             </select>
                         </div>
                     </div> -->
+
+                    <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="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>

+ 14 - 0
templates/repo/single_bare.tmpl

@@ -9,6 +9,20 @@
                 <h4>Quick Guide</h4>
             </div>
             <div class="panel-body guide-content text-center">
+                <form action="{{.RepoLink}}/import" method="post">
+                    {{.CsrfTokenHtml}}
+                    <h3>Clone from existing repository</h3>
+                    <div class="input-group col-md-6 col-md-offset-3">
+                        <span class="input-group-btn">
+                            <button class="btn btn-default" type="button">URL</button>
+                        </span>
+                        <input name="passwd" type="password" class="form-control" placeholder="Type existing repository address" required="required">
+                        <span class="input-group-btn">
+                            <button type="submit" class="btn btn-default" type="button">Clone</button>
+                        </span>
+                    </div>
+                </form>
+
                 <h3>Clone this repository</h3>
                 <div class="input-group col-md-8 col-md-offset-2 guide-buttons">
                     <span class="input-group-btn">

+ 1 - 1
templates/repo/toolbar.tmpl

@@ -11,7 +11,7 @@
                     <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}}

+ 10 - 1
templates/user/dashboard.tmpl

@@ -29,7 +29,16 @@
     <div id="feed-right" class="col-md-4">
         <div class="panel panel-default repo-panel">
             <div class="panel-heading">Your Repositories
-                <a class="btn btn-success pull-right btn-sm" href="/repo/create"><i class="fa fa-plus-square"></i>New Repo</a>
+                <div class="btn-group pull-right" id="user-dashboard-repo-new">
+                    <button type="button" class="btn btn-success btn-sm dropdown-toggle" data-toggle="dropdown"><i class="fa fa-plus-square"></i>New</button>
+                    <div class="dropdown-menu dropdown-menu-right">
+                       <ul class="list-unstyled">
+                           <li><a href="/repo/create"><i class="fa fa-book"></i>Repository</a></li>
+                           <li><a href="/repo/mirror"><i class="fa fa-clipboard"></i>Mirror</a></li>
+                           <li><a href="#"><i class="fa fa-users"></i>Organization</a></li>
+                       </ul>
+                    </div>
+                </div>
             </div>
             <div class="panel-body">
                 <ul class="list-group">{{range .MyRepos}}

+ 2 - 0
templates/user/forgot_passwd.tmpl

@@ -24,6 +24,8 @@
         </div>
         {{else if .IsResetDisable}}
         <p>Sorry, mail service is not enabled.</p>
+        {{else if .ResendLimited}}
+        <p>Sorry, you are sending e-mail too frequently, please wait 3 minutes.</p>
         {{end}}
     </form>
 </div>

+ 2 - 1
web.go

@@ -96,7 +96,7 @@ func runWeb(*cli.Context) {
 
 	m.Group("/user", func(r martini.Router) {
 		r.Any("/login", binding.BindIgnErr(auth.LogInForm{}), user.SignIn)
-		r.Any("/login/github", oauth2.LoginRequired, user.SocialSignIn)
+		r.Any("/login/github", user.SocialSignIn)
 		r.Any("/sign_up", binding.BindIgnErr(auth.RegisterForm{}), user.SignUp)
 		r.Any("/forget_password", user.ForgotPasswd)
 		r.Any("/reset_password", user.ResetPasswd)
@@ -121,6 +121,7 @@ func runWeb(*cli.Context) {
 	m.Get("/user/:username", ignSignIn, user.Profile)
 
 	m.Any("/repo/create", reqSignIn, binding.BindIgnErr(auth.CreateRepoForm{}), repo.Create)
+	m.Any("/repo/mirror", reqSignIn, binding.BindIgnErr(auth.CreateRepoForm{}), repo.Mirror)
 
 	adminReq := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true, AdminRequire: true})