commit
ee7bfe2ebe
@ -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,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 |
||||
|
||||
|
@ -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/ |
@ -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* |
@ -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/ |
@ -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" |
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
||||
|
@ -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 |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -0,0 +1,11 @@ |
||||
// +build memcache
|
||||
|
||||
package base |
||||
|
||||
import ( |
||||
_ "github.com/gogits/cache/memcache" |
||||
) |
||||
|
||||
func init() { |
||||
EnableMemcache = true |
||||
} |
@ -0,0 +1,11 @@ |
||||
// +build redis
|
||||
|
||||
package base |
||||
|
||||
import ( |
||||
_ "github.com/gogits/cache/redis" |
||||
) |
||||
|
||||
func init() { |
||||
EnableRedis = true |
||||
} |
@ -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() |
||||
} |
@ -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\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?`) |
||||
) |
||||
|
||||
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) |
||||
) |
@ -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 |
||||
) |
@ -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 |
||||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 16 KiB |
@ -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") |
||||
} |
@ -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 |
||||
*/ |
@ -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) |
||||
} |
||||
}*/ |
@ -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") |
||||
} |
@ -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) |
||||
} |
||||
|
@ -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 |
||||
|
@ -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}} |
@ -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" .}} |
||||
|
@ -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> |
@ -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&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" .}} |
@ -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" .}} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue