Browse Source

Merge pull request #70 from zhsso/git

Git
无闻 11 years ago
parent
commit
8faa0dbcd7

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

+ 1 - 0
.gitignore

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

+ 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

+ 12 - 65
models/git.go

@@ -142,7 +142,8 @@ func GetReposFiles(userName, repoName, commitId, rpath string) ([]*RepoFile, err
 }
 
 func getReposFiles(userName, repoName, commitId string, rpath string) ([]*RepoFile, error) {
-	repo, err := git.OpenRepository(RepoPath(userName, repoName))
+	repopath := RepoPath(userName, repoName)
+	repo, err := git.OpenRepository(repopath)
 	if err != nil {
 		return nil, err
 	}
@@ -162,77 +163,23 @@ func getReposFiles(userName, repoName, commitId string, rpath string) ([]*RepoFi
 				return 0
 			}
 
-			var cm = commit
-			var i int
-			for {
-				i = i + 1
-				//fmt.Println(".....", i, cm.Id(), cm.ParentCount())
-				if cm.ParentCount() == 0 {
-					break
-				} else if cm.ParentCount() == 1 {
-					pt, _ := repo.SubTree(cm.Parent(0).Tree, dirname)
-					if pt == nil {
-						break
-					}
-					pEntry := pt.EntryByName(entry.Name)
-					if pEntry == nil || !pEntry.Id.Equal(entry.Id) {
-						break
-					} else {
-						cm = cm.Parent(0)
-					}
-				} else {
-					var emptyCnt = 0
-					var sameIdcnt = 0
-					var lastSameCm *git.Commit
-					//fmt.Println(".....", cm.ParentCount())
-					for i := 0; i < cm.ParentCount(); i++ {
-						//fmt.Println("parent", i, cm.Parent(i).Id())
-						p := cm.Parent(i)
-						pt, _ := repo.SubTree(p.Tree, dirname)
-						var pEntry *git.TreeEntry
-						if pt != nil {
-							pEntry = pt.EntryByName(entry.Name)
-						}
-
-						//fmt.Println("pEntry", pEntry)
-
-						if pEntry == nil {
-							emptyCnt = emptyCnt + 1
-							if emptyCnt+sameIdcnt == cm.ParentCount() {
-								if lastSameCm == nil {
-									goto loop
-								} else {
-									cm = lastSameCm
-									break
-								}
-							}
-						} else {
-							//fmt.Println(i, "pEntry", pEntry.Id, "entry", entry.Id)
-							if !pEntry.Id.Equal(entry.Id) {
-								goto loop
-							} else {
-								lastSameCm = cm.Parent(i)
-								sameIdcnt = sameIdcnt + 1
-								if emptyCnt+sameIdcnt == cm.ParentCount() {
-									// TODO: now follow the first parent commit?
-									cm = lastSameCm
-									//fmt.Println("sameId...")
-									break
-								}
-							}
-						}
-					}
-				}
+			cmd := exec.Command("git", "log", "-1", "--pretty=format:%H", commitId, "--", path.Join(dirname, entry.Name))
+			cmd.Dir = repopath
+			out, err := cmd.Output()
+			if err != nil {
+				return 0
+			}
+			filecm, err := repo.GetCommit(string(out))
+			if err != nil {
+				return 0
 			}
-
-		loop:
 
 			rp := &RepoFile{
 				entry,
 				path.Join(dirname, entry.Name),
 				size,
 				repo,
-				cm,
+				filecm,
 			}
 
 			if entry.IsFile() {

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

+ 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
+*/

+ 471 - 0
routers/repo/http.go

@@ -0,0 +1,471 @@
+package repo
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"path"
+	"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
+	var askAuth = !(!repo.IsPrivate && isPull)
+
+	// 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
+		}
+
+		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) {
+		//fmt.Println("rpc:", rpc)
+		//fmt.Println("input:", string(input))
+	}}
+
+	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, filePath string) (string, error) {
+	root := config.ReposRoot
+
+	if root == "" {
+		cwd, err := os.Getwd()
+
+		if err != nil {
+			log.Print(err)
+			return "", err
+		}
+
+		root = cwd
+	}
+
+	f := path.Join(root, filePath)
+	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)
+	}
+}*/

+ 31 - 85
routers/repo/repo.go

@@ -14,8 +14,6 @@ import (
 
 	"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"
@@ -55,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
@@ -266,89 +294,6 @@ func authRequired(ctx *middleware.Context) {
 	ctx.HTML(401, fmt.Sprintf("status/401"))
 }
 
-func Http(ctx *middleware.Context, params martini.Params) {
-	username := params["username"]
-	reponame := params["reponame"]
-	if strings.HasSuffix(reponame, ".git") {
-		reponame = reponame[:len(reponame)-4]
-	}
-
-	//fmt.Println("req:", ctx.Req.Header)
-
-	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
-	}
-
-	isPull := webdav.IsPullMethod(ctx.Req.Method)
-	var askAuth = !(!repo.IsPrivate && isPull)
-
-	//authRequired(ctx)
-	//return
-
-	// check access
-	if askAuth {
-		// check digit auth
-
-		// check basic auth
-		baHead := ctx.Req.Header.Get("Authorization")
-		if baHead == "" {
-			authRequired(ctx)
-			return
-		}
-
-		auths := strings.Fields(baHead)
-		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}
-		newUser.EncodePasswd()
-		if authUser.Passwd != newUser.Passwd {
-			ctx.Handle(401, "no basic auth and digit auth", nil)
-			return
-		}
-
-		var tp = models.AU_WRITABLE
-		if isPull {
-			tp = models.AU_READABLE
-		}
-
-		has, err := models.HasAccess(authUsername, username+"/"+reponame, tp)
-		if err != nil || !has {
-			ctx.Handle(401, "no basic auth and digit auth", nil)
-			return
-		}
-	}
-
-	dir := models.RepoPath(username, reponame)
-
-	prefix := path.Join("/", username, params["reponame"])
-	server := webdav.NewServer(
-		dir, prefix, true)
-
-	server.ServeHTTP(ctx.ResponseWriter, ctx.Req)
-}
-
 func Setting(ctx *middleware.Context, params martini.Params) {
 	if !ctx.Repo.IsOwner {
 		ctx.Handle(404, "repo.Setting", nil)
@@ -397,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>

+ 30 - 26
update.go

@@ -42,32 +42,7 @@ func newUpdateLogger(execDir string) {
 	qlog.Info("Start logging update...")
 }
 
-// for command: ./gogs update
-func runUpdate(c *cli.Context) {
-	execDir, _ := base.ExecDir()
-	newUpdateLogger(execDir)
-
-	base.NewConfigContext()
-	models.LoadModelsConfig()
-
-	if models.UseSQLite3 {
-		os.Chdir(execDir)
-	}
-
-	models.SetEngine()
-
-	args := c.Args()
-	if len(args) != 3 {
-		qlog.Fatal("received less 3 parameters")
-	}
-
-	refName := args[0]
-	if refName == "" {
-		qlog.Fatal("refName is empty, shouldn't use")
-	}
-	oldCommitId := args[1]
-	newCommitId := args[2]
-
+func update(refName, oldCommitId, newCommitId string) {
 	isNew := strings.HasPrefix(oldCommitId, "0000000")
 	if isNew &&
 		strings.HasPrefix(newCommitId, "0000000") {
@@ -158,3 +133,32 @@ func runUpdate(c *cli.Context) {
 		qlog.Fatalf("runUpdate.models.CommitRepoAction: %v", err)
 	}
 }
+
+// for command: ./gogs update
+func runUpdate(c *cli.Context) {
+	execDir, _ := base.ExecDir()
+	newUpdateLogger(execDir)
+
+	base.NewConfigContext()
+	models.LoadModelsConfig()
+
+	if models.UseSQLite3 {
+		os.Chdir(execDir)
+	}
+
+	models.SetEngine()
+
+	args := c.Args()
+	if len(args) != 3 {
+		qlog.Fatal("received less 3 parameters")
+	}
+
+	refName := args[0]
+	if refName == "" {
+		qlog.Fatal("refName is empty, shouldn't use")
+	}
+	oldCommitId := args[1]
+	newCommitId := args[2]
+
+	update(refName, oldCommitId, newCommitId)
+}

+ 9 - 3
web.go

@@ -11,10 +11,10 @@ import (
 
 	"github.com/codegangsta/cli"
 	"github.com/go-martini/martini"
+
 	qlog "github.com/qiniu/log"
 
 	"github.com/gogits/binding"
-
 	"github.com/gogits/gogs/modules/auth"
 	"github.com/gogits/gogs/modules/avatar"
 	"github.com/gogits/gogs/modules/base"
@@ -72,6 +72,11 @@ func runWeb(*cli.Context) {
 
 	reqSignIn := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true})
 	ignSignIn := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: base.Service.RequireSignInView})
+	ignSignInAndCsrf := middleware.Toggle(&middleware.ToggleOptions{
+		SignInRequire: base.Service.RequireSignInView,
+		DisableCsrf:   true,
+	})
+
 	reqSignOut := middleware.Toggle(&middleware.ToggleOptions{SignOutRequire: true})
 
 	// Routers.
@@ -91,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)
@@ -116,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})
 
@@ -165,7 +171,7 @@ func runWeb(*cli.Context) {
 	m.Group("/:username", func(r martini.Router) {
 		r.Any("/:reponame/**", repo.Http)
 		r.Get("/:reponame", middleware.RepoAssignment(true, true, true), repo.Single)
-	}, ignSignIn)
+	}, ignSignInAndCsrf)
 
 	// Not found handler.
 	m.NotFound(routers.NotFound)