Add memcached and redis Docker supported

This commit is contained in:
Meaglith Ma 2014-04-23 12:29:53 +08:00
commit ee7bfe2ebe
118 changed files with 6124 additions and 1637 deletions

12
.fswatch.json Normal file
View File

@ -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
.gitignore vendored
View File

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

View File

@ -2,26 +2,23 @@
path = github.com/gogits/gogs
[deps]
github.com/Unknwon/cae =
github.com/Unknwon/com =
github.com/Unknwon/goconfig =
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/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/gogits/webdav=
github.com/martini-contrib/oauth2=
github.com/martini-contrib/sessions=
code.google.com/p/goauth2=
github.com/lib/pq =
github.com/nfnt/resize =
github.com/qiniu/log =
github.com/robfig/cron =
[res]
include = templates|public|conf

View File

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

View File

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

View File

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

View File

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

View File

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

23
conf/gitignore/Android Normal file
View File

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

12
conf/gitignore/Java Normal file
View File

@ -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*

View File

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

8
conf/supervisor.ini Normal file
View File

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

View File

@ -1,3 +1,5 @@
// +build go1.2
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
@ -14,12 +16,10 @@ import (
"github.com/gogits/gogs/modules/base"
)
// +build go1.2
// Test that go1.2 tag above is included in builds. main.go refers to this definition.
const go12tag = true
const APP_VER = "0.2.0.0403 Alpha"
const APP_VER = "0.3.0.0421 Alpha"
func init() {
base.AppVer = APP_VER

View File

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

View File

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

212
models/git_diff.go Normal file
View File

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

View File

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

15
models/models_sqlite.go Normal file
View File

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

View File

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

View File

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

83
models/release.go Normal file
View File

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

View File

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

84
models/update.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

50
modules/auth/release.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

17
modules/cron/cron.go Normal file
View File

@ -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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

396
modules/social/social.go Normal file
View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

68
routers/repo/download.go Normal file
View File

@ -0,0 +1,68 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"os"
"path/filepath"
"github.com/Unknwon/com"
"github.com/go-martini/martini"
"github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/middleware"
)
func SingleDownload(ctx *middleware.Context, params martini.Params) {
// Get tree path
treename := params["_1"]
blob, err := ctx.Repo.Commit.GetBlobByPath(treename)
if err != nil {
ctx.Handle(404, "repo.SingleDownload(GetBlobByPath)", err)
return
}
data, err := blob.Data()
if err != nil {
ctx.Handle(404, "repo.SingleDownload(Data)", err)
return
}
contentType, isTextFile := base.IsTextFile(data)
_, isImageFile := base.IsImageFile(data)
ctx.Res.Header().Set("Content-Type", contentType)
if !isTextFile && !isImageFile {
ctx.Res.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(treename))
ctx.Res.Header().Set("Content-Transfer-Encoding", "binary")
}
ctx.Res.Write(data)
}
func ZipDownload(ctx *middleware.Context, params martini.Params) {
commitId := ctx.Repo.CommitId
archivesPath := filepath.Join(ctx.Repo.GitRepo.Path, "archives")
if !com.IsDir(archivesPath) {
if err := os.Mkdir(archivesPath, 0755); err != nil {
ctx.Handle(404, "ZipDownload -> os.Mkdir(archivesPath)", err)
return
}
}
zipPath := filepath.Join(archivesPath, commitId+".zip")
if com.IsFile(zipPath) {
ctx.ServeFile(zipPath, ctx.Repo.Repository.Name+".zip")
return
}
err := ctx.Repo.Commit.CreateArchive(zipPath)
if err != nil {
ctx.Handle(404, "ZipDownload -> CreateArchive "+zipPath, err)
return
}
ctx.ServeFile(zipPath, ctx.Repo.Repository.Name+".zip")
}

55
routers/repo/git.go Normal file
View File

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

496
routers/repo/http.go Normal file
View File

@ -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)
}
}*/

View File

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

View File

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

View File

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

196
routers/user/home.go Normal file
View File

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

View File

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

View File

@ -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"
"code.google.com/p/goauth2/oauth"
"github.com/go-martini/martini"
"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(),
}
// 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"`
}
// Make the request.
scope := "https://api.github.com/user"
r, err := transport.Client().Get(scope)
func extractPath(next string) string {
n, err := url.Parse(next)
if err != nil {
log.Error("connect with github error: %s", err)
// FIXME: handle error page
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
}
defer r.Body.Close()
user := &GithubUser{}
err = json.NewDecoder(r.Body).Decode(user)
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
}
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
}
// handle call back
tk, err := connect.Exchange(code)
if err != nil {
log.Error("Get: %s", err)
ctx.Handle(500, "social.SocialSignIn(Exchange)", err)
return
}
log.Info("login: %s", user.Name)
// FIXME: login here, user email to check auth, if not registe, then generate a uniq username
next = extractPath(ctx.Query("state"))
log.Trace("social.SocialSignIn(Got token)")
ui, err := connect.UserInfo(tk, ctx.Req.URL)
if err != nil {
ctx.Handle(500, fmt.Sprintf("social.SocialSignIn(get info from %s)", name), err)
return
}
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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,8 +11,8 @@
<div class="panel-body">
<br/>
<form action="/admin/users/{{.User.Id}}" method="post" class="form-horizontal">
{{if .IsSuccess}}<p class="alert alert-success">Account profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}}
{{.CsrfTokenHtml}}
{{template "base/alert" .}}
<input type="hidden" value="{{.User.Id}}" name="userId"/>
<div class="form-group">
<label class="col-md-3 control-label">Username: </label>

View File

@ -12,7 +12,7 @@
<br/>
<form action="/admin/users/new" method="post" class="form-horizontal">
{{.CsrfTokenHtml}}
<div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div>
{{template "base/alert" .}}
<div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}">
<label class="col-md-3 control-label">Username: </label>
<div class="col-md-7">

View File

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

View File

@ -9,16 +9,27 @@
<meta name="description" content="Gogs(Go Git Service) is a GitHub-like clone in the Go Programming Language" />
<meta name="keywords" content="go, git">
<meta name="_csrf" content="{{.CsrfToken}}" />
{{if .Repository.IsGoget}}<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}">{{end}}
<!-- Stylesheets -->
{{if IsProdMode}}
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
{{else}}
<link href="/css/bootstrap.min.css" rel="stylesheet" />
<link href="/css/todc-bootstrap.min.css" rel="stylesheet" />
<link href="/css/font-awesome.min.css" rel="stylesheet" />
<link href="/css/markdown.css" rel="stylesheet" />
<link href="/css/gogs.css" rel="stylesheet" />
<script src="/js/jquery-1.10.1.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
{{end}}
<link href="/css/todc-bootstrap.min.css" rel="stylesheet" />
<link href="/css/markdown.css" rel="stylesheet" />
<link href="/css/gogs.css" rel="stylesheet" />
<script src="/js/lib.js"></script>
<script src="/js/app.js"></script>
<title>{{if .Title}}{{.Title}} - {{end}}{{AppName}}</title>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
{{template "base/head" .}}
{{template "base/navbar" .}}
<div class="container">
401 Unauthorized
</div>
{{template "base/footer" .}}

Some files were not shown because too many files have changed in this diff Show More