1
0
mirror of https://github.com/go-gitea/gitea.git synced 2024-11-04 08:17:24 -05:00

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~ *.exe~
gogs gogs
__pycache__ __pycache__
*.pem

View File

@ -2,26 +2,23 @@
path = github.com/gogits/gogs path = github.com/gogits/gogs
[deps] [deps]
github.com/Unknwon/cae =
github.com/Unknwon/com =
github.com/Unknwon/goconfig =
github.com/codegangsta/cli = github.com/codegangsta/cli =
github.com/go-martini/martini = 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/go-sql-driver/mysql =
github.com/lib/pq= github.com/go-xorm/xorm =
github.com/gogits/logs=
github.com/gogits/binding=
github.com/gogits/git=
github.com/gogits/gfm=
github.com/gogits/cache = 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/session =
github.com/gogits/webdav= github.com/lib/pq =
github.com/martini-contrib/oauth2= github.com/nfnt/resize =
github.com/martini-contrib/sessions= github.com/qiniu/log =
code.google.com/p/goauth2= github.com/robfig/cron =
[res] [res]
include = templates|public|conf 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). > 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. 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) ![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 #### Other language version
@ -21,7 +24,7 @@ More importantly, Gogs only needs one binary to setup your own project hosting o
## Overview ## 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. - 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! - 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). - 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 ## Features
- Activity timeline - Activity timeline
- SSH/HTTPS(Clone only) protocol support. - SSH/HTTP(S) protocol support.
- Register/delete/rename account. - Register/delete/rename account.
- Create/delete/watch/rename public repository. - Create/migrate/mirror/delete/watch/rename/transfer public/private repository.
- Repository viewer. - Repository viewer/release/issue tracker.
- Issue tracker.
- Gravatar and cache support. - Gravatar and cache support.
- Mail service(register, issue). - Mail service(register, issue).
- Administration panel. - Administration panel.
- Supports MySQL, PostgreSQL and SQLite3(binary release only). - Supports MySQL, PostgreSQL and SQLite3.
- Social account login(GitHub, Google, QQ, Weibo)
## Installation ## Installation
Make sure you install [Prerequirements](https://github.com/gogits/gogs/wiki/Prerequirements) first. 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) - [Install from source](https://github.com/gogits/gogs/wiki/Install-from-source)
- [Ship with Docker](https://github.com/gogits/gogs/tree/master/dockerfiles)
## Acknowledgments ## Acknowledgments
- Logo is inspired by [martini-contrib](https://github.com/martini-contrib).
- Router and middleware mechanism of [martini](http://martini.codegangsta.io/). - Router and middleware mechanism of [martini](http://martini.codegangsta.io/).
- Mail Service, modules design is inspired by [WeTalk](https://github.com/beego/wetalk). - 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). - System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog).
- Usage and modification from [beego](http://beego.me) modules. - 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. - 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 ## 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 ## License

View File

@ -5,7 +5,7 @@ Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。
![Demo](http://gowalker.org/public/gogs_demo.gif) ![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) 跟随开发团队的脚步。 - 您可以到 [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) 跟随开发团队的脚步。
- 想要先睹为快?通过 [在线体验](http://try.gogits.org/Unknown/gogs) 或查看 **安装部署 -> 二进制安装** 小节。 - 想要先睹为快?通过 [在线体验](http://try.gogits.org/Unknown/gogs) 或查看 **安装部署 -> 二进制安装** 小节。
- 使用过程中遇到问题?尝试从 [故障排查](https://github.com/gogits/gogs/wiki/Troubleshooting) 页面获取帮助。 - 使用过程中遇到问题?尝试从 [故障排查](https://github.com/gogits/gogs/wiki/Troubleshooting) 页面获取帮助。
@ -23,37 +23,42 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依
## 功能特性 ## 功能特性
- 活动时间线 - 活动时间线
- SSH/HTTPS仅限 Clone 协议支持 - SSH/HTTP(S) 协议支持
- 注册/删除/重命名用户 - 注册/删除/重命名用户
- 创建/删除/关注/重命名公开仓库 - 创建/迁移/镜像/删除/关注/重命名/转移 公开/私有 仓库
- 仓库浏览器 - 仓库 浏览器/发布/缺陷追踪
- Bug 追踪系统
- Gravatar 以及缓存支持 - Gravatar 以及缓存支持
- 邮件服务注册、Issue - 邮件服务注册、Issue
- 管理员面板 - 管理员面板
- 支持 MySQL、PostgreSQL 以及 SQLite3仅限二进制版本 - 支持 MySQL、PostgreSQL 以及 SQLite3 数据库
- 社交帐号登录GitHub、Google、QQ、微博
## 安装部署 ## 安装部署
在安装 Gogs 之前,您需要先安装 [基本环境](https://github.com/gogits/gogs/wiki/Prerequirements)。 在安装 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) - [源码安装](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) 修改的邮件服务和模块设计。 - 基于 [WeTalk](https://github.com/beego/wetalk) 修改的邮件服务和模块设计。
- 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。 - 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。
- [beego](http://beego.me) 模块的使用与修改。 - [beego](http://beego.me) 模块的使用与修改。
- [martini](http://martini.codegangsta.io/) 的路由与中间件机制。 - [martini](http://martini.codegangsta.io/) 的路由与中间件机制。
- 感谢 [gobuild.io](http://gobuild.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": "", "models": "",
"others": [ "others": [
"modules", "modules",
"$GOPATH/src/github.com/gogits/binding", "$GOPATH/src/github.com/gogits/logs",
"$GOPATH/src/github.com/gogits/git", "$GOPATH/src/github.com/gogits/git",
"$GOPATH/src/github.com/gogits/gfm" "$GOPATH/src/github.com/gogits/gfm"
] ]

View File

@ -8,17 +8,24 @@ RUN_MODE = dev
[repository] [repository]
ROOT = 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 LICENSES = Apache v2 License|GPL v2|MIT License|Affero GPL|Artistic License 2.0|BSD (3-Clause) License
[server] [server]
PROTOCOL = http
DOMAIN = localhost DOMAIN = localhost
ROOT_URL = http://%(DOMAIN)s:%(HTTP_PORT)s/ ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
HTTP_ADDR = HTTP_ADDR =
HTTP_PORT = 3000 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] [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 DB_TYPE = mysql
HOST = 127.0.0.1:3306 HOST = 127.0.0.1:3306
NAME = gogs NAME = gogs
@ -46,7 +53,7 @@ RESET_PASSWD_CODE_LIVE_MINUTES = 180
; User need to confirm e-mail for registration ; User need to confirm e-mail for registration
REGISTER_EMAIL_CONFIRM = false REGISTER_EMAIL_CONFIRM = false
; Does not allow register and admin create account only ; Does not allow register and admin create account only
DISENABLE_REGISTERATION = false DISABLE_REGISTRATION = false
; User must sign in to view anything. ; User must sign in to view anything.
REQUIRE_SIGNIN_VIEW = false REQUIRE_SIGNIN_VIEW = false
; Cache avatar as picture ; Cache avatar as picture
@ -62,6 +69,7 @@ SEND_BUFFER_LEN = 10
SUBJECT = %(APP_NAME)s SUBJECT = %(APP_NAME)s
; Mail server ; Mail server
; Gmail: smtp.gmail.com:587 ; Gmail: smtp.gmail.com:587
; QQ: smtp.qq.com:25
HOST = HOST =
; Mail from address ; Mail from address
FROM = FROM =
@ -69,6 +77,55 @@ FROM =
USER = USER =
PASSWD = 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] [cache]
; Either "memory", "redis", or "memcache", default is "memory" ; Either "memory", "redis", or "memcache", default is "memory"
ADAPTER = 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. // Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -14,12 +16,10 @@ import (
"github.com/gogits/gogs/modules/base" "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. // Test that go1.2 tag above is included in builds. main.go refers to this definition.
const go12tag = true const go12tag = true
const APP_VER = "0.2.0.0403 Alpha" const APP_VER = "0.3.0.0421 Alpha"
func init() { func init() {
base.AppVer = APP_VER base.AppVer = APP_VER

View File

@ -7,6 +7,8 @@ package models
import ( import (
"strings" "strings"
"time" "time"
"github.com/go-xorm/xorm"
) )
// Access types. // Access types.
@ -19,7 +21,7 @@ const (
type Access struct { type Access struct {
Id int64 Id int64
UserName string `xorm:"unique(s)"` UserName string `xorm:"unique(s)"`
RepoName string `xorm:"unique(s)"` RepoName string `xorm:"unique(s)"` // <user name>/<repo name>
Mode int `xorm:"unique(s)"` Mode int `xorm:"unique(s)"`
Created time.Time `xorm:"created"` Created time.Time `xorm:"created"`
} }
@ -40,12 +42,28 @@ func UpdateAccess(access *Access) error {
return err 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. // HasAccess returns true if someone can read or write to given repository.
func HasAccess(userName, repoName string, mode int) (bool, error) { func HasAccess(userName, repoName string, mode int) (bool, error) {
return orm.Get(&Access{ access := &Access{
Id: 0,
UserName: strings.ToLower(userName), UserName: strings.ToLower(userName),
RepoName: strings.ToLower(repoName), 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 ( import (
"encoding/json" "encoding/json"
"strings"
"time" "time"
"github.com/gogits/git"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
) )
@ -22,6 +25,7 @@ const (
OP_CREATE_ISSUE OP_CREATE_ISSUE
OP_PULL_REQUEST OP_PULL_REQUEST
OP_TRANSFER_REPO OP_TRANSFER_REPO
OP_PUSH_TAG
) )
// Action represents user operation type and other information to repository., // 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. // CommitRepoAction adds new action for committing repository.
func CommitRepoAction(userId int64, userName, actEmail string, func CommitRepoAction(userId int64, userName, actEmail string,
repoId int64, repoName string, refName string, commit *base.PushCommits) error { 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) bs, err := json.Marshal(commit)
if err != nil { if err != nil {
@ -76,7 +89,7 @@ func CommitRepoAction(userId int64, userName, actEmail string,
} }
if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, ActEmail: actEmail, 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) log.Error("action.CommitRepoAction(notify watchers): %d/%s", userId, repoName)
return err 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" "fmt"
"os" "os"
"path" "path"
"strings"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/lunny/xorm"
// _ "github.com/mattn/go-sqlite3"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
) )
var ( var (
orm *xorm.Engine orm *xorm.Engine
tables []interface{}
HasEngine bool HasEngine bool
DbCfg struct { DbCfg struct {
Type, Host, Name, User, Pwd, Path, SslMode string Type, Host, Name, User, Pwd, Path, SslMode string
} }
EnableSQLite3 bool
UseSQLite3 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() { func LoadModelsConfig() {
DbCfg.Type = base.Cfg.MustValue("database", "DB_TYPE") DbCfg.Type = base.Cfg.MustValue("database", "DB_TYPE")
if DbCfg.Type == "sqlite3" { 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", x, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8",
DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name)) DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name))
case "postgres": case "postgres":
x, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s", var host, port = "127.0.0.1", "5432"
DbCfg.User, DbCfg.Pwd, DbCfg.Name, DbCfg.SslMode)) fields := strings.Split(DbCfg.Host, ":")
// case "sqlite3": if len(fields) > 0 {
// os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm) host = fields[0]
// x, err = xorm.NewEngine("sqlite3", DbCfg.Path) }
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: default:
return fmt.Errorf("Unknown database type: %s", DbCfg.Type) return fmt.Errorf("Unknown database type: %s", DbCfg.Type)
} }
if err != nil { if err != nil {
return fmt.Errorf("models.init(fail to conntect database): %v", err) return fmt.Errorf("models.init(fail to conntect database): %v", err)
} }
return x.Sync(tables...)
return x.Sync(new(User), new(PublicKey), new(Repository), new(Watch),
new(Action), new(Access), new(Issue), new(Comment))
} }
func SetEngine() (err error) { 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", orm, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8",
DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name)) DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name))
case "postgres": case "postgres":
orm, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s", var host, port = "127.0.0.1", "5432"
DbCfg.User, DbCfg.Pwd, DbCfg.Name, DbCfg.SslMode)) 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": case "sqlite3":
os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm) os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
orm, err = xorm.NewEngine("sqlite3", DbCfg.Path) orm, err = xorm.NewEngine("sqlite3", DbCfg.Path)
@ -91,7 +119,7 @@ func SetEngine() (err error) {
if err != nil { if err != nil {
return fmt.Errorf("models.init(fail to create xorm.log): %v", err) return fmt.Errorf("models.init(fail to create xorm.log): %v", err)
} }
orm.Logger = f orm.Logger = xorm.NewSimpleLogger(f)
orm.ShowSQL = true orm.ShowSQL = true
orm.ShowDebug = true orm.ShowDebug = true
@ -102,16 +130,19 @@ func SetEngine() (err error) {
func NewEngine() (err error) { func NewEngine() (err error) {
if err = SetEngine(); err != nil { if err = SetEngine(); err != nil {
return err 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 { if err = orm.Sync(tables...); err != nil {
return fmt.Errorf("sync database struct error: %v", err) return fmt.Errorf("sync database struct error: %v\n", err)
} }
return nil return nil
} }
type Statistic struct { type Statistic struct {
Counter 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.Watch, _ = orm.Count(new(Watch))
stats.Counter.Action, _ = orm.Count(new(Action)) stats.Counter.Action, _ = orm.Count(new(Action))
stats.Counter.Access, _ = orm.Count(new(Access)) 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 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 package models
import "time" import (
"errors"
)
// OT: Oauth2 Type // OT: Oauth2 Type
const ( const (
OT_GITHUB = iota + 1 OT_GITHUB = iota + 1
OT_GOOGLE OT_GOOGLE
OT_TWITTER 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 { type Oauth2 struct {
Uid int64 `xorm:"pk"` // userId Id int64
Type int `xorm:"pk unique(oauth)"` // twitter,github,google... Uid int64 `xorm:"unique(s)"` // userId
Identity string `xorm:"pk unique(oauth)"` // id.. User *User `xorm:"-"`
Token string `xorm:"VARCHAR(200) not null"` Type int `xorm:"unique(s) unique(oauth)"` // twitter,github,google...
RefreshTime time.Time `xorm:"created"` 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. // PublicKey represents a SSH key of user.
type PublicKey struct { type PublicKey struct {
Id int64 Id int64
OwnerId int64 `xorm:" index not null"` OwnerId int64 `xorm:"unique(s) index not null"`
Name string `xorm:" not null"` //UNIQUE(s) Name string `xorm:"unique(s) not null"`
Fingerprint string Fingerprint string
Content string `xorm:"TEXT not null"` Content string `xorm:"TEXT not null"`
Created time.Time `xorm:"created"` 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") ErrRepoNotExist = errors.New("Repository does not exist")
ErrRepoFileNotExist = errors.New("Target Repo file does not exist") ErrRepoFileNotExist = errors.New("Target Repo file does not exist")
ErrRepoNameIllegal = errors.New("Repository name contains illegal characters") 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 ( var (
@ -65,6 +66,7 @@ func NewRepoContext() {
type Repository struct { type Repository struct {
Id int64 Id int64
OwnerId int64 `xorm:"unique(s)"` OwnerId int64 `xorm:"unique(s)"`
Owner *User `xorm:"-"`
ForkId int64 ForkId int64
LowerName string `xorm:"unique(s) index not null"` LowerName string `xorm:"unique(s) index not null"`
Name string `xorm:"index not null"` Name string `xorm:"index not null"`
@ -74,11 +76,14 @@ type Repository struct {
NumStars int NumStars int
NumForks int NumForks int
NumIssues int NumIssues int
NumReleases int `xorm:"NOT NULL"`
NumClosedIssues int NumClosedIssues int
NumOpenIssues int `xorm:"-"` NumOpenIssues int `xorm:"-"`
NumTags int `xorm:"-"`
IsPrivate bool IsPrivate bool
IsMirror bool
IsBare bool IsBare bool
IsGoget bool
DefaultBranch string
Created time.Time `xorm:"created"` Created time.Time `xorm:"created"`
Updated time.Time `xorm:"updated"` Updated time.Time `xorm:"updated"`
} }
@ -117,13 +122,133 @@ func IsLegalName(repoName string) bool {
return true 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. // CreateRepository creates a repository for given user or orgnaziation.
func CreateRepository(user *User, repoName, desc, repoLang, license string, private bool, initReadme bool) (*Repository, error) { func CreateRepository(user *User, name, desc, lang, license string, private, mirror, initReadme bool) (*Repository, error) {
if !IsLegalName(repoName) { if !IsLegalName(name) {
return nil, ErrRepoNameIllegal return nil, ErrRepoNameIllegal
} }
isExist, err := IsRepositoryExist(user, repoName) isExist, err := IsRepositoryExist(user, name)
if err != nil { if err != nil {
return nil, err return nil, err
} else if isExist { } else if isExist {
@ -132,17 +257,15 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv
repo := &Repository{ repo := &Repository{
OwnerId: user.Id, OwnerId: user.Id,
Name: repoName, Name: name,
LowerName: strings.ToLower(repoName), LowerName: strings.ToLower(name),
Description: desc, Description: desc,
IsPrivate: private, 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() sess := orm.NewSession()
defer sess.Close() defer sess.Close()
sess.Begin() sess.Begin()
@ -151,23 +274,27 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv
if err2 := os.RemoveAll(repoPath); err2 != nil { if err2 := os.RemoveAll(repoPath); err2 != nil {
log.Error("repo.CreateRepository(repo): %v", err) log.Error("repo.CreateRepository(repo): %v", err)
return nil, errors.New(fmt.Sprintf( 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() sess.Rollback()
return nil, err return nil, err
} }
mode := AU_WRITABLE
if mirror {
mode = AU_READABLE
}
access := Access{ access := Access{
UserName: user.LowerName, UserName: user.LowerName,
RepoName: strings.ToLower(path.Join(user.Name, repo.Name)), RepoName: strings.ToLower(path.Join(user.Name, repo.Name)),
Mode: AU_WRITABLE, Mode: mode,
} }
if _, err = sess.Insert(&access); err != nil { if _, err = sess.Insert(&access); err != nil {
sess.Rollback() sess.Rollback()
if err2 := os.RemoveAll(repoPath); err2 != nil { if err2 := os.RemoveAll(repoPath); err2 != nil {
log.Error("repo.CreateRepository(access): %v", err) log.Error("repo.CreateRepository(access): %v", err)
return nil, errors.New(fmt.Sprintf( 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 return nil, err
} }
@ -178,7 +305,7 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv
if err2 := os.RemoveAll(repoPath); err2 != nil { if err2 := os.RemoveAll(repoPath); err2 != nil {
log.Error("repo.CreateRepository(repo count): %v", err) log.Error("repo.CreateRepository(repo count): %v", err)
return nil, errors.New(fmt.Sprintf( 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 return nil, err
} }
@ -188,25 +315,36 @@ func CreateRepository(user *User, repoName, desc, repoLang, license string, priv
if err2 := os.RemoveAll(repoPath); err2 != nil { if err2 := os.RemoveAll(repoPath); err2 != nil {
log.Error("repo.CreateRepository(commit): %v", err) log.Error("repo.CreateRepository(commit): %v", err)
return nil, errors.New(fmt.Sprintf( 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 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 := exec.Command("git", "update-server-info")
c.Dir = repoPath c.Dir = repoPath
if err = c.Run(); err != nil { if err = c.Run(); err != nil {
log.Error("repo.CreateRepository(exec update-server-info): %v", err) 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 return repo, nil
} }
@ -227,24 +365,21 @@ func initRepoCommit(tmpPath string, sig *git.Signature) (err error) {
var stderr string var stderr string
if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "add", "--all"); err != nil { if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "add", "--all"); err != nil {
return err return err
} } else if strings.Contains(stderr, "fatal:") {
if len(stderr) > 0 { return errors.New("git add: " + stderr)
log.Trace("stderr(1): %s", stderr)
} }
if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
"-m", "Init commit"); err != nil { "-m", "Init commit"); err != nil {
return err return err
} } else if strings.Contains(stderr, "fatal:") {
if len(stderr) > 0 { return errors.New("git commit: " + stderr)
log.Trace("stderr(2): %s", stderr)
} }
if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "push", "origin", "master"); err != nil { if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "push", "origin", "master"); err != nil {
return err return err
} } else if strings.Contains(stderr, "fatal:") {
if len(stderr) > 0 { return errors.New("git push: " + stderr)
log.Trace("stderr(3): %s", stderr)
} }
return nil return nil
} }
@ -260,6 +395,13 @@ func createHookUpdate(hookPath, content string) error {
return err 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. // InitRepository initializes README and .gitignore if needed.
func initRepository(f string, user *User, repo *Repository, initReadme bool, repoLang, license string) error { func initRepository(f string, user *User, repo *Repository, initReadme bool, repoLang, license string) error {
repoPath := RepoPath(user.Name, repo.Name) repoPath := RepoPath(user.Name, repo.Name)
@ -271,7 +413,7 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
// hook/post-update // hook/post-update
if err := createHookUpdate(filepath.Join(repoPath, "hooks", "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 { strings.Replace(appPath, "\\", "/", -1))); err != nil {
return err 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())) tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()))
os.MkdirAll(tmpDir, os.ModePerm) 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 return err
} else if strings.Contains(stderr, "fatal:") {
return errors.New("git clone: " + stderr)
} }
// README // README
@ -310,7 +455,7 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
if repoLang != "" { if repoLang != "" {
filePath := "conf/gitignore/" + repoLang filePath := "conf/gitignore/" + repoLang
if com.IsFile(filePath) { if com.IsFile(filePath) {
if _, err := com.Copy(filePath, if err := com.Copy(filePath,
filepath.Join(tmpDir, fileName["gitign"])); err != nil { filepath.Join(tmpDir, fileName["gitign"])); err != nil {
return err return err
} }
@ -321,7 +466,7 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
if license != "" { if license != "" {
filePath := "conf/license/" + license filePath := "conf/license/" + license
if com.IsFile(filePath) { if com.IsFile(filePath) {
if _, err := com.Copy(filePath, if err := com.Copy(filePath,
filepath.Join(tmpDir, fileName["license"])); err != nil { filepath.Join(tmpDir, fileName["license"])); err != nil {
return err return err
} }
@ -332,6 +477,8 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
return nil return nil
} }
SetRepoEnvs(user.Id, user.Name, repo.Name)
// Apply changes and commit. // Apply changes and commit.
return initRepoCommit(tmpDir, user.NewGitSig()) return initRepoCommit(tmpDir, user.NewGitSig())
} }
@ -365,6 +512,7 @@ func GetRepos(num, offset int) ([]UserRepo, error) {
return urepos, nil return urepos, nil
} }
// RepoPath returns repository path by given user and repository name.
func RepoPath(userName, repoName string) string { func RepoPath(userName, repoName string) string {
return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git") 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 { if err = orm.Find(&accesses, &Access{RepoName: user.LowerName + "/" + repo.LowerName}); err != nil {
return err return err
} }
sess := orm.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
for i := range accesses { for i := range accesses {
accesses[i].RepoName = newUser.LowerName + "/" + repo.LowerName accesses[i].RepoName = newUser.LowerName + "/" + repo.LowerName
if accesses[i].UserName == user.LowerName { if accesses[i].UserName == user.LowerName {
accesses[i].UserName = newUser.LowerName accesses[i].UserName = newUser.LowerName
} }
if err = UpdateAccess(&accesses[i]); err != nil { if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil {
return err return err
} }
} }
// Update repository. // Update repository.
repo.OwnerId = newUser.Id 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 return err
} }
// Update user repository number. // Update user repository number.
rawSql := "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?" 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 return err
} }
rawSql = "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?" 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 return err
} }
// Add watch of new owner to repository. // Add watch of new owner to repository.
if !IsWatching(newUser.Id, repo.Id) { if !IsWatching(newUser.Id, repo.Id) {
if err = WatchRepo(newUser.Id, repo.Id, true); err != nil { if err = WatchRepo(newUser.Id, repo.Id, true); err != nil {
sess.Rollback()
return err return err
} }
} }
if err = TransferRepoAction(user, newUser, repo); err != nil { if err = TransferRepoAction(user, newUser, repo); err != nil {
sess.Rollback()
return err return err
} }
// Change repository directory name. // 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. // 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 { if err = orm.Find(&accesses, &Access{RepoName: strings.ToLower(userName + "/" + oldRepoName)}); err != nil {
return err return err
} }
sess := orm.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
for i := range accesses { for i := range accesses {
accesses[i].RepoName = userName + "/" + newRepoName accesses[i].RepoName = userName + "/" + newRepoName
if err = UpdateAccess(&accesses[i]); err != nil { if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil {
return err return err
} }
} }
// Change repository directory name. // 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 { func UpdateRepository(repo *Repository) error {
@ -476,8 +653,7 @@ func DeleteRepository(userId, repoId int64, userName string) (err error) {
sess.Rollback() sess.Rollback()
return err return err
} }
rawSql := "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?" if _, err := sess.Delete(&Action{RepoId: repo.Id}); err != nil {
if _, err = sess.Exec(rawSql, userId); err != nil {
sess.Rollback() sess.Rollback()
return err return err
} }
@ -485,6 +661,16 @@ func DeleteRepository(userId, repoId int64, userName string) (err error) {
sess.Rollback() sess.Rollback()
return err 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 { if err = sess.Commit(); err != nil {
sess.Rollback() sess.Rollback()
return err return err
@ -525,12 +711,24 @@ func GetRepositoryById(id int64) (*Repository, error) {
} }
// GetRepositories returns the list of repositories of given user. // 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) 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 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) { func GetRepositoryCount(user *User) (int64, error) {
return orm.Count(&Repository{OwnerId: user.Id}) 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 package models
import ( import (
"crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
@ -13,8 +14,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/dchest/scrypt"
"github.com/gogits/git" "github.com/gogits/git"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
@ -62,6 +61,7 @@ type User struct {
IsActive bool IsActive bool
IsAdmin bool IsAdmin bool
Rands string `xorm:"VARCHAR(10)"` Rands string `xorm:"VARCHAR(10)"`
Salt string `xorm:"VARCHAR(10)"`
Created time.Time `xorm:"created"` Created time.Time `xorm:"created"`
Updated time.Time `xorm:"updated"` Updated time.Time `xorm:"updated"`
} }
@ -76,7 +76,7 @@ func (user *User) AvatarLink() string {
if base.Service.EnableCacheAvatar { if base.Service.EnableCacheAvatar {
return "/avatar/" + user.Avatar 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. // 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. // EncodePasswd encodes password to safe format.
func (user *User) EncodePasswd() error { func (user *User) EncodePasswd() {
newPasswd, err := scrypt.Key([]byte(user.Passwd), []byte(base.SecretKey), 16384, 8, 1, 64) newPasswd := base.PBKDF2([]byte(user.Passwd), []byte(user.Salt), 10000, 50, sha256.New)
user.Passwd = fmt.Sprintf("%x", newPasswd) user.Passwd = fmt.Sprintf("%x", newPasswd)
return err
} }
// Member represents user is member of organization. // Member represents user is member of organization.
@ -148,9 +147,9 @@ func RegisterUser(user *User) (*User, error) {
user.Avatar = base.EncodeMd5(user.Email) user.Avatar = base.EncodeMd5(user.Email)
user.AvatarEmail = user.Email user.AvatarEmail = user.Email
user.Rands = GetUserSalt() user.Rands = GetUserSalt()
if err = user.EncodePasswd(); err != nil { user.Salt = GetUserSalt()
return nil, err user.EncodePasswd()
} else if _, err = orm.Insert(user); err != nil { if _, err = orm.Insert(user); err != nil {
return nil, err return nil, err
} else if err = os.MkdirAll(UserPath(user.Name), os.ModePerm); err != nil { } else if err = os.MkdirAll(UserPath(user.Name), os.ModePerm); err != nil {
if _, err := orm.Id(user.Id).Delete(&User{}); 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 { if err = orm.Find(&accesses, &Access{UserName: user.LowerName}); err != nil {
return err return err
} }
sess := orm.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
for i := range accesses { for i := range accesses {
accesses[i].UserName = newUserName accesses[i].UserName = newUserName
if strings.HasPrefix(accesses[i].RepoName, user.LowerName+"/") { if strings.HasPrefix(accesses[i].RepoName, user.LowerName+"/") {
accesses[i].RepoName = strings.Replace(accesses[i].RepoName, user.LowerName, newUserName, 1) 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 return err
} }
} }
} }
repos, err := GetRepositories(user) repos, err := GetRepositories(user, true)
if err != nil { if err != nil {
return err return err
} }
@ -241,14 +247,19 @@ func ChangeUserName(user *User, newUserName string) (err error) {
for j := range accesses { for j := range accesses {
accesses[j].RepoName = newUserName + "/" + repos[i].LowerName accesses[j].RepoName = newUserName + "/" + repos[i].LowerName
if err = UpdateAccess(&accesses[j]); err != nil { if err = UpdateAccessWithSession(sess, &accesses[j]); err != nil {
return err return err
} }
} }
} }
// Change user directory name. // 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. // UpdateUser updates user's information.
@ -278,11 +289,26 @@ func DeleteUser(user *User) error {
// TODO: check issues, other repos' commits // 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. // Delete all feeds.
if _, err = orm.Delete(&Action{UserId: user.Id}); err != nil { if _, err = orm.Delete(&Action{UserId: user.Id}); err != nil {
return err return err
} }
// Delete all watches.
if _, err = orm.Delete(&Watch{UserId: user.Id}); err != nil {
return err
}
// Delete all accesses. // Delete all accesses.
if _, err = orm.Delete(&Access{UserName: user.LowerName}); err != nil { if _, err = orm.Delete(&Access{UserName: user.LowerName}); err != nil {
return err return err
@ -305,7 +331,6 @@ func DeleteUser(user *User) error {
} }
_, err = orm.Delete(user) _, err = orm.Delete(user)
// TODO: delete and update follower information.
return err return err
} }
@ -355,20 +380,50 @@ func GetUserByName(name string) (*User, error) {
return user, nil return user, nil
} }
// LoginUserPlain validates user by raw user name and password. // GetUserEmailsByNames returns a slice of e-mails corresponds to names.
func LoginUserPlain(name, passwd string) (*User, error) { func GetUserEmailsByNames(names []string) []string {
user := User{LowerName: strings.ToLower(name), Passwd: passwd} mails := make([]string, 0, len(names))
if err := user.EncodePasswd(); err != nil { for _, name := range names {
return nil, err 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) has, err := orm.Get(&user)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } 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. // Follow is connection request for receiving user notifycation.

View File

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

View File

@ -11,8 +11,6 @@ import (
"github.com/go-martini/martini" "github.com/go-martini/martini"
"github.com/gogits/binding"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
) )
@ -39,7 +37,7 @@ func (f *RegisterForm) Name(field string) string {
return names[field] 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 { if req.Method == "GET" || errors.Count() == 0 {
return return
} }
@ -72,7 +70,7 @@ func (f *LogInForm) Name(field string) string {
return names[field] 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 { if req.Method == "GET" || errors.Count() == 0 {
return return
} }
@ -100,7 +98,7 @@ func getMinMaxSize(field reflect.StructField) string {
return "" 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) typ := reflect.TypeOf(form)
val := reflect.ValueOf(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 { if err, ok := errors.Fields[field.Name]; ok {
data["Err_"+field.Name] = true data["Err_"+field.Name] = true
switch err { switch err {
case binding.RequireError: case base.BindingRequireError:
data["ErrorMsg"] = form.Name(field.Name) + " cannot be empty" 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" 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" 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" data["ErrorMsg"] = form.Name(field.Name) + " must contain at most " + getMinMaxSize(field) + " characters"
case binding.EmailError: case base.BindingEmailError:
data["ErrorMsg"] = form.Name(field.Name) + " is not valid" 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: default:
data["ErrorMsg"] = "Unknown error: " + err data["ErrorMsg"] = "Unknown error: " + err
} }
@ -194,7 +194,7 @@ func (f *InstallForm) Name(field string) string {
return names[field] 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 { if req.Method == "GET" || errors.Count() == 0 {
return return
} }

View File

@ -10,8 +10,6 @@ import (
"github.com/go-martini/martini" "github.com/go-martini/martini"
"github.com/gogits/binding"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
) )
@ -31,7 +29,7 @@ func (f *CreateIssueForm) Name(field string) string {
return names[field] 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 { if req.Method == "GET" || errors.Count() == 0 {
return 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/go-martini/martini"
"github.com/gogits/binding"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
) )
type CreateRepoForm struct { type CreateRepoForm struct {
RepoName string `form:"repo" binding:"Required;AlphaDash"` RepoName string `form:"repo" binding:"Required;AlphaDash"`
Visibility string `form:"visibility"` Private bool `form:"private"`
Description string `form:"desc" binding:"MaxSize(100)"` Description string `form:"desc" binding:"MaxSize(100)"`
Language string `form:"language"` Language string `form:"language"`
License string `form:"license"` License string `form:"license"`
InitReadme string `form:"initReadme"` InitReadme bool `form:"initReadme"`
} }
func (f *CreateRepoForm) Name(field string) string { func (f *CreateRepoForm) Name(field string) string {
@ -33,7 +31,7 @@ func (f *CreateRepoForm) Name(field string) string {
return names[field] 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 { if req.Method == "GET" || errors.Count() == 0 {
return return
} }
@ -51,3 +49,41 @@ func (f *CreateRepoForm) Validate(errors *binding.Errors, req *http.Request, con
validate(errors, data, f) 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/go-martini/martini"
"github.com/gogits/binding"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
) )
@ -30,7 +28,7 @@ func (f *AddSSHKeyForm) Name(field string) string {
return names[field] 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) data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData)
AssignForm(f, data) AssignForm(f, data)

View File

@ -10,7 +10,6 @@ import (
"github.com/go-martini/martini" "github.com/go-martini/martini"
"github.com/gogits/binding"
"github.com/gogits/session" "github.com/gogits/session"
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
@ -93,7 +92,7 @@ func (f *UpdateProfileForm) Name(field string) string {
return names[field] 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 { if req.Method == "GET" || errors.Count() == 0 {
return return
} }
@ -126,7 +125,7 @@ func (f *UpdatePasswdForm) Name(field string) string {
return names[field] 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 { if req.Method == "GET" || errors.Count() == 0 {
return return
} }

View File

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

View File

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

View File

@ -6,9 +6,11 @@ package base
import ( import (
"bytes" "bytes"
"fmt"
"net/http" "net/http"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"github.com/gogits/gfm" "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) 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 { func RenderMarkdown(rawBytes []byte, urlPrefix string) []byte {
body := RenderSpecialLink(rawBytes, urlPrefix)
// fmt.Println(string(body))
htmlFlags := 0 htmlFlags := 0
// htmlFlags |= gfm.HTML_USE_XHTML // htmlFlags |= gfm.HTML_USE_XHTML
// htmlFlags |= gfm.HTML_USE_SMARTYPANTS // htmlFlags |= gfm.HTML_USE_SMARTYPANTS
// htmlFlags |= gfm.HTML_SMARTYPANTS_FRACTIONS // htmlFlags |= gfm.HTML_SMARTYPANTS_FRACTIONS
// htmlFlags |= gfm.HTML_SMARTYPANTS_LATEX_DASHES // 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_STYLE
htmlFlags |= gfm.HTML_SKIP_SCRIPT htmlFlags |= gfm.HTML_SKIP_SCRIPT
htmlFlags |= gfm.HTML_GITHUB_BLOCKCODE htmlFlags |= gfm.HTML_GITHUB_BLOCKCODE
@ -115,7 +162,11 @@ func RenderMarkdown(rawBytes []byte, urlPrefix string) []byte {
extensions |= gfm.EXTENSION_SPACE_HEADERS extensions |= gfm.EXTENSION_SPACE_HEADERS
extensions |= gfm.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK 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 return body
} }
func RenderMarkdownString(raw, urlPrefix string) string {
return string(RenderMarkdown([]byte(raw), urlPrefix))
}

View File

@ -5,7 +5,9 @@
package base package base
import ( import (
"bytes"
"container/list" "container/list"
"encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"strings" "strings"
@ -54,6 +56,9 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{
"AppDomain": func() string { "AppDomain": func() string {
return Domain return Domain
}, },
"IsProdMode": func() bool {
return IsProdMode
},
"LoadTimes": func(startTime time.Time) string { "LoadTimes": func(startTime time.Time) string {
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
}, },
@ -62,11 +67,18 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{
"TimeSince": TimeSince, "TimeSince": TimeSince,
"FileSize": FileSize, "FileSize": FileSize,
"Subtract": Subtract, "Subtract": Subtract,
"Add": func(a, b int) int {
return a + b
},
"ActionIcon": ActionIcon, "ActionIcon": ActionIcon,
"ActionDesc": ActionDesc, "ActionDesc": ActionDesc,
"DateFormat": DateFormat, "DateFormat": DateFormat,
"List": List, "List": List,
"Mail2Domain": func(mail string) string { "Mail2Domain": func(mail string) string {
if !strings.Contains(mail, "@") {
return "try.gogits.org"
}
suffix := strings.SplitN(mail, "@", 2)[1] suffix := strings.SplitN(mail, "@", 2)[1]
domain, ok := mailDomains[suffix] domain, ok := mailDomains[suffix]
if !ok { if !ok {
@ -80,4 +92,128 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{
"DiffTypeToStr": DiffTypeToStr, "DiffTypeToStr": DiffTypeToStr,
"DiffLineTypeToStr": DiffLineTypeToStr, "DiffLineTypeToStr": DiffLineTypeToStr,
"ShortSha": ShortSha, "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 package base
import ( import (
"bytes" "crypto/hmac"
"crypto/md5" "crypto/md5"
"crypto/rand" "crypto/rand"
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"hash"
"math" "math"
"strconv" "strconv"
"strings" "strings"
@ -40,6 +40,44 @@ func GetRandomString(n int, alphabets ...byte) string {
return string(bytes) 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 // verify time limit code
func VerifyTimeLimitCode(data string, minutes int, code string) bool { func VerifyTimeLimitCode(data string, minutes int, code string) bool {
if len(code) <= 18 { if len(code) <= 18 {
@ -105,7 +143,7 @@ func AvatarLink(email string) string {
if Service.EnableCacheAvatar { if Service.EnableCacheAvatar {
return "/avatar/" + EncodeMd5(email) return "/avatar/" + EncodeMd5(email)
} }
return "http://1.gravatar.com/avatar/" + EncodeMd5(email) return "//1.gravatar.com/avatar/" + EncodeMd5(email)
} }
// Seconds-based time units // Seconds-based time units
@ -246,7 +284,6 @@ func TimeSince(then time.Time) string {
default: default:
return fmt.Sprintf("%d years %s", diff/Year, lbl) return fmt.Sprintf("%d years %s", diff/Year, lbl)
} }
return then.String()
} }
const ( const (
@ -474,107 +511,3 @@ func (a argInt) Get(i int, args ...int) (r int) {
} }
return 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) { func NewLogger(bufLen int64, mode, config string) {
Mode, Config = mode, config Mode, Config = mode, config
logger = logs.NewLogger(bufLen) logger = logs.NewLogger(bufLen)
logger.EnableFuncCallDepth(true) logger.SetLogFuncCallDepth(3)
logger.SetLogFuncCallDepth(4)
logger.SetLogger(mode, config) 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 := 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) SendAsync(&msg)
} }
// SendNotifyMail sends mail notification of all watchers. // Send reset password email.
func SendNotifyMail(user, owner *models.User, repo *models.Repository, issue *models.Issue) error { 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) watches, err := models.GetWatches(repo.Id)
if err != nil { 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)) 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) u, err := models.GetUserById(uid)
if err != nil { 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) tos = append(tos, u.Email)
} }
if len(tos) == 0 { if len(tos) == 0 {
return nil return tos, nil
} }
subject := fmt.Sprintf("[%s] %s", repo.Name, issue.Name) 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>.", 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 := 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) SendAsync(&msg)
return nil return nil
} }

View File

@ -47,7 +47,7 @@ func Toggle(options *ToggleOptions) martini.Handler {
return return
} else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm {
ctx.Data["Title"] = "Activate Your Account" ctx.Data["Title"] = "Activate Your Account"
ctx.HTML(200, "user/active") ctx.HTML(200, "user/activate")
return 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" "encoding/base64"
"fmt" "fmt"
"html/template" "html/template"
"io"
"net/http" "net/http"
"net/url"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -34,6 +37,7 @@ type Context struct {
p martini.Params p martini.Params
Req *http.Request Req *http.Request
Res http.ResponseWriter Res http.ResponseWriter
Flash *Flash
Session session.SessionStore Session session.SessionStore
Cache cache.Cache Cache cache.Cache
User *models.User User *models.User
@ -47,6 +51,7 @@ type Context struct {
IsBranch bool IsBranch bool
IsTag bool IsTag bool
IsCommit bool IsCommit bool
HasAccess bool
Repository *models.Repository Repository *models.Repository
Owner *models.User Owner *models.User
Commit *git.Commit Commit *git.Commit
@ -59,6 +64,7 @@ type Context struct {
HTTPS string HTTPS string
Git string Git string
} }
Mirror *models.Mirror
} }
} }
@ -78,6 +84,8 @@ func (ctx *Context) HasError() bool {
if !ok { if !ok {
return false return false
} }
ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string)
ctx.Data["Flash"] = ctx.Flash
return hasErr.(bool) 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. // RenderWithErr used for page has form validation but need to prompt error to users.
func (ctx *Context) RenderWithErr(msg, tpl string, form auth.Form) { func (ctx *Context) RenderWithErr(msg, tpl string, form auth.Form) {
ctx.Data["HasError"] = true
ctx.Data["ErrorMsg"] = msg
if form != nil { if form != nil {
auth.AssignForm(form, ctx.Data) auth.AssignForm(form, ctx.Data)
} }
ctx.Flash.ErrorMsg = msg
ctx.Data["Flash"] = ctx.Flash
ctx.HTML(200, tpl) ctx.HTML(200, tpl)
} }
// Handle handles and logs error by given status. // Handle handles and logs error by given status.
func (ctx *Context) Handle(status int, title string, err error) { func (ctx *Context) Handle(status int, title string, err error) {
log.Error("%s: %v", title, err) log.Error("%s: %v", title, err)
if martini.Dev == martini.Prod { if martini.Dev != martini.Prod {
ctx.HTML(500, "status/500") ctx.Data["ErrorMsg"] = err
return
} }
ctx.Data["ErrorMsg"] = err
ctx.HTML(status, fmt.Sprintf("status/%d", status)) ctx.HTML(status, fmt.Sprintf("status/%d", status))
} }
@ -239,6 +245,56 @@ func (ctx *Context) CsrfTokenValid() bool {
return true 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. // InitContext initializes a classic context for a request.
func InitContext() martini.Handler { func InitContext() martini.Handler {
return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) { return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) {
@ -256,9 +312,27 @@ func InitContext() martini.Handler {
// start session // start session
ctx.Session = base.SessionManager.SessionStart(res, r) 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 := res.(martini.ResponseWriter)
rw.Before(func(martini.ResponseWriter) { rw.Before(func(martini.ResponseWriter) {
ctx.Session.SessionRelease(res) 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. // Get user from session if logined.

View File

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

View File

@ -15,6 +15,7 @@ import (
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log"
) )
func RepoAssignment(redirect bool, args ...bool) martini.Handler { func RepoAssignment(redirect bool, args ...bool) martini.Handler {
@ -39,7 +40,7 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler {
userName := params["username"] userName := params["username"]
repoName := params["reponame"] repoName := params["reponame"]
branchName := params["branchname"] refName := params["branchname"]
// get repository owner // get repository owner
ctx.Repo.IsOwner = ctx.IsSigned && ctx.User.LowerName == strings.ToLower(userName) 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")) ctx.Handle(200, "RepoAssignment", errors.New("invliad user account for single repository"))
return return
} }
ctx.Repo.Owner = user
// get repository // get repository
repo, err := models.GetRepositoryByName(user.Id, repoName) repo, err := models.GetRepositoryByName(user.Id, repoName)
if err != nil { if err != nil {
if err == models.ErrRepoNotExist { if err == models.ErrRepoNotExist {
ctx.Handle(404, "RepoAssignment", err) ctx.Handle(404, "RepoAssignment", err)
return
} else if redirect { } else if redirect {
ctx.Redirect("/") ctx.Redirect("/")
return return
} }
ctx.Handle(404, "RepoAssignment", err) ctx.Handle(500, "RepoAssignment", err)
return 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 repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
ctx.Repo.Repository = repo ctx.Repo.Repository = repo
ctx.Data["IsBareRepo"] = ctx.Repo.Repository.IsBare ctx.Data["IsBareRepo"] = ctx.Repo.Repository.IsBare
gitRepo, err := git.OpenRepository(models.RepoPath(userName, repoName)) gitRepo, err := git.OpenRepository(models.RepoPath(userName, repoName))
if err != nil { 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 return
} }
ctx.Repo.GitRepo = gitRepo ctx.Repo.GitRepo = gitRepo
ctx.Repo.Owner = user
ctx.Repo.RepoLink = "/" + user.Name + "/" + repo.Name 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["Title"] = user.Name + "/" + repo.Name
ctx.Data["Repository"] = repo ctx.Data["Repository"] = repo
ctx.Data["Owner"] = user 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.Repo.CloneLink.HTTPS = fmt.Sprintf("%s%s/%s.git", base.AppUrl, user.LowerName, repo.LowerName)
ctx.Data["CloneLink"] = ctx.Repo.CloneLink 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 // when repo is bare, not valid branch
if !ctx.Repo.Repository.IsBare && validBranch { if !ctx.Repo.Repository.IsBare && validBranch {
detect: detect:
if len(branchName) > 0 { if len(refName) > 0 {
// TODO check tag if gitRepo.IsBranchExist(refName) {
if models.IsBranchExist(user.Name, repoName, branchName) {
ctx.Repo.IsBranch = true 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 { if err != nil {
ctx.Handle(404, "RepoAssignment invalid branch", nil) ctx.Handle(404, "RepoAssignment invalid branch", nil)
return 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.IsCommit = true
ctx.Repo.CommitId = branchName ctx.Repo.CommitId = refName
ctx.Repo.BranchName = branchName ctx.Repo.BranchName = refName
ctx.Repo.Commit, err = gitRepo.GetCommit(branchName) ctx.Repo.Commit, err = gitRepo.GetCommit(refName)
if err != nil { if err != nil {
ctx.Handle(404, "RepoAssignment invalid commit", nil) ctx.Handle(404, "RepoAssignment invalid commit", nil)
return return
@ -138,16 +188,23 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler {
} }
} else { } else {
branchName = "master" refName = ctx.Repo.Repository.DefaultBranch
if len(refName) == 0 {
refName = "master"
}
goto detect goto detect
} }
ctx.Data["IsBranch"] = ctx.Repo.IsBranch ctx.Data["IsBranch"] = ctx.Repo.IsBranch
ctx.Data["IsCommit"] = ctx.Repo.IsCommit 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 // repo is bare and display enable
if displayBare && ctx.Repo.Repository.IsBare { if displayBare && ctx.Repo.Repository.IsBare {
log.Debug("Bare repository: %s", ctx.Repo.RepoLink)
ctx.HTML(200, "repo/single_bare") ctx.HTML(200, "repo/single_bare")
return return
} }
@ -157,6 +214,11 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler {
} }
ctx.Data["BranchName"] = ctx.Repo.BranchName 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["CommitId"] = ctx.Repo.CommitId
ctx.Data["IsRepositoryWatching"] = ctx.Repo.IsWatching 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; color: #EEE;
font-size: 100%; font-size: 100%;
height: 46px; height: 46px;
margin-top: 3px;
} }
#nav-logo { #nav-logo {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
margin-right: 10px; margin-right: 10px;
margin-top: 0;
} }
.nav-item:hover, .nav-item:hover,
@ -81,10 +83,6 @@ html, body {
text-decoration: none; text-decoration: none;
} }
.nav-item.navbar-right {
margin-top: 3px;
}
.nav-item.navbar-btn { .nav-item.navbar-btn {
cursor: pointer; cursor: pointer;
margin-top: 8px; margin-top: 8px;
@ -96,6 +94,30 @@ html, body {
margin: 0; 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 */ /* gogits nav item active status */
#masthead .nav .active { #masthead .nav .active {
color: #fff; color: #fff;
@ -239,14 +261,40 @@ html, body {
} }
#social-login { #social-login {
margin-top: 30px; margin-top: 40px;
padding-top: 20px; padding-top: 40px;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
position: relative;
} }
#social-login .btn { #social-login .btn {
float: none; 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 */ /* gogs-user-profile */
@ -291,6 +339,22 @@ html, body {
padding-right: 18px; 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 { #user-activity .tab-pane {
padding: 20px; padding: 20px;
} }
@ -309,6 +373,18 @@ html, body {
height: 8em; 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 */ /* gogits user setting */
#user-setting-nav > h4, #user-setting-container > h4, #user-setting-container > div > h4, #user-setting-nav > h4, #user-setting-container > h4, #user-setting-container > div > h4,
@ -444,6 +520,43 @@ html, body {
margin-right: 1em; 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 */ /* gogits repo single page */
#body-nav.repo-nav { #body-nav.repo-nav {
@ -614,6 +727,10 @@ html, body {
margin-top: -20px; margin-top: -20px;
} }
#commits-pager {
margin-top: 0;
}
#source .source-toolbar:after { #source .source-toolbar:after {
clear: both; clear: both;
} }
@ -831,6 +948,10 @@ html, body {
margin-left: .5em; margin-left: .5em;
} }
#commits-search-form {
margin-top: 4px;
}
.commit-box .avatar, .diff-head-box .avatar { .commit-box .avatar, .diff-head-box .avatar {
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -838,10 +959,6 @@ html, body {
vertical-align: top; vertical-align: top;
} }
.commit-box .search {
margin-top: 3px;
}
.commit-box td { .commit-box td {
background-color: #FFF; background-color: #FFF;
} }
@ -1305,3 +1422,73 @@ html, body {
#release .release-item .info .avatar { #release .release-item .info .avatar {
vertical-align: middle; 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() { function initUserSetting() {
// ssh confirmation
$('#ssh-keys .delete').confirmation({ $('#ssh-keys .delete').confirmation({
singleton: true, singleton: true,
onConfirm: function (e, $this) { 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() { function initRepository() {
@ -438,6 +451,18 @@ function initRepository() {
$item.find(".bar .add").css("width", addPercent + "%"); $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() { 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 ($) {
$(function () { $(function () {
initCore(); initCore();
@ -539,5 +589,8 @@ function initIssue() {
if ($('#issue').length) { if ($('#issue').length) {
initIssue(); initIssue();
} }
if ($('#release').length) {
initRelease();
}
}); });
})(jQuery); })(jQuery);

View File

@ -153,6 +153,12 @@ func Config(ctx *middleware.Context) {
ctx.Data["Mailer"] = base.MailService 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["CacheAdapter"] = base.CacheAdapter
ctx.Data["CacheConfig"] = base.CacheConfig ctx.Data["CacheConfig"] = base.CacheConfig

View File

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

View File

@ -13,6 +13,6 @@ func Markdown(ctx *middleware.Context) {
content := ctx.Query("content") content := ctx.Query("content")
ctx.Render.JSON(200, map[string]interface{}{ ctx.Render.JSON(200, map[string]interface{}{
"ok": true, "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 package routers
import ( import (
"github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/middleware"
"github.com/gogits/gogs/routers/user" "github.com/gogits/gogs/routers/user"
@ -23,6 +24,11 @@ func Home(ctx *middleware.Context) {
return return
} }
repos, _ := models.GetRecentUpdatedRepositories()
for _, repo := range repos {
repo.Owner, _ = models.GetUserById(repo.OwnerId)
}
ctx.Data["Repos"] = repos
ctx.Data["PageIsHome"] = true ctx.Data["PageIsHome"] = true
ctx.HTML(200, "home") ctx.HTML(200, "home")
} }

View File

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

View File

@ -7,12 +7,11 @@ package repo
import ( import (
"github.com/go-martini/martini" "github.com/go-martini/martini"
"github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/middleware"
) )
func Branches(ctx *middleware.Context, params martini.Params) { 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 { if err != nil {
ctx.Handle(404, "repo.Branches", err) ctx.Handle(404, "repo.Branches", err)
return return

View File

@ -5,7 +5,6 @@
package repo package repo
import ( import (
"container/list"
"path" "path"
"github.com/go-martini/martini" "github.com/go-martini/martini"
@ -16,35 +15,51 @@ import (
) )
func Commits(ctx *middleware.Context, params martini.Params) { func Commits(ctx *middleware.Context, params martini.Params) {
userName := params["username"] userName := ctx.Repo.Owner.Name
repoName := params["reponame"] repoName := ctx.Repo.Repository.Name
branchName := params["branchname"]
brs, err := models.GetBranches(userName, repoName) brs, err := ctx.Repo.GitRepo.GetBranches()
if err != nil { if err != nil {
ctx.Handle(200, "repo.Commits", err) ctx.Handle(500, "repo.Commits", err)
return return
} else if len(brs) == 0 { } else if len(brs) == 0 {
ctx.Handle(404, "repo.Commits", nil) ctx.Handle(404, "repo.Commits", nil)
return return
} }
var commits *list.List commitsCount, err := ctx.Repo.Commit.CommitsCount()
if models.IsBranchExist(userName, repoName, branchName) { if err != nil {
commits, err = models.GetCommitsByBranch(userName, repoName, branchName) ctx.Handle(500, "repo.Commits(GetCommitsCount)", err)
} else { return
commits, err = models.GetCommitsByCommitId(userName, repoName, branchName)
} }
// 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 { if err != nil {
ctx.Handle(404, "repo.Commits", err) ctx.Handle(500, "repo.Commits(get commits)", err)
return return
} }
ctx.Data["Username"] = userName ctx.Data["Username"] = userName
ctx.Data["Reponame"] = repoName ctx.Data["Reponame"] = repoName
ctx.Data["CommitCount"] = commits.Len() ctx.Data["CommitCount"] = commitsCount
ctx.Data["Commits"] = commits ctx.Data["Commits"] = commits
ctx.Data["LastPageNum"] = lastPage
ctx.Data["NextPageNum"] = nextPage
ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["IsRepoToolbarCommits"] = true
ctx.HTML(200, "repo/commits") 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) { func Diff(ctx *middleware.Context, params martini.Params) {
userName := ctx.Repo.Owner.Name userName := ctx.Repo.Owner.Name
repoName := ctx.Repo.Repository.Name repoName := ctx.Repo.Repository.Name
branchName := ctx.Repo.BranchName
commitId := ctx.Repo.CommitId commitId := ctx.Repo.CommitId
commit := ctx.Repo.Commit commit := ctx.Repo.Commit
@ -64,19 +78,15 @@ func Diff(ctx *middleware.Context, params martini.Params) {
} }
isImageFile := func(name string) bool { isImageFile := func(name string) bool {
repoFile, err := models.GetTargetFile(userName, repoName, blob, err := ctx.Repo.Commit.GetBlobByPath(name)
branchName, commitId, name)
if err != nil { if err != nil {
return false return false
} }
blob, err := repoFile.LookupBlob() data, err := blob.Data()
if err != nil { if err != nil {
return false return false
} }
data := blob.Contents()
_, isImage := base.IsImageFile(data) _, isImage := base.IsImageFile(data)
return isImage return isImage
} }
@ -85,8 +95,44 @@ func Diff(ctx *middleware.Context, params martini.Params) {
ctx.Data["Title"] = commit.Message() + " · " + base.ShortSha(commitId) ctx.Data["Title"] = commit.Message() + " · " + base.ShortSha(commitId)
ctx.Data["Commit"] = commit ctx.Data["Commit"] = commit
ctx.Data["Diff"] = diff ctx.Data["Diff"] = diff
ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0
ctx.Data["IsRepoToolbarCommits"] = true ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["SourcePath"] = "/" + path.Join(userName, repoName, "src", commitId) ctx.Data["SourcePath"] = "/" + path.Join(userName, repoName, "src", commitId)
ctx.Data["RawPath"] = "/" + path.Join(userName, repoName, "raw", commitId) ctx.Data["RawPath"] = "/" + path.Join(userName, repoName, "raw", commitId)
ctx.HTML(200, "repo/diff") 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" "net/url"
"strings" "strings"
"github.com/Unknwon/com"
"github.com/go-martini/martini" "github.com/go-martini/martini"
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
@ -81,16 +82,18 @@ func Issues(ctx *middleware.Context) {
ctx.HTML(200, "issue/list") 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["Title"] = "Create issue"
ctx.Data["IsRepoToolbarIssues"] = true ctx.Data["IsRepoToolbarIssues"] = true
ctx.Data["IsRepoToolbarIssuesList"] = false ctx.Data["IsRepoToolbarIssuesList"] = false
if ctx.Req.Method == "GET" {
ctx.HTML(200, "issue/create") 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() { if ctx.HasError() {
ctx.HTML(200, "issue/create") ctx.HTML(200, "issue/create")
return 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, 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) ctx.Repo.Repository.NumIssues, form.IssueName, form.Labels, form.Content, false)
if err != nil { if err != nil {
ctx.Handle(200, "issue.CreateIssue", err) ctx.Handle(500, "issue.CreateIssue(CreateIssue)", err)
return 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, 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), 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 { 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 return
} }
// Mail watchers. // Mail watchers and mentions.
if base.Service.NotifyMail { if base.Service.NotifyMail {
if err = mailer.SendNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue); err != nil { tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
ctx.Handle(200, "issue.CreateIssue", err) 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 return
} }
} }
log.Trace("%d Issue created: %d", ctx.Repo.Repository.Id, issue.Id) 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)) 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 return
} }
issue.Poster = u issue.Poster = u
issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), "")) issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink))
// Get comments. // Get comments.
comments, err := models.GetIssueComments(issue.Id) comments, err := models.GetIssueComments(issue.Id)
@ -164,7 +184,7 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) {
return return
} }
comments[i].Poster = u 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 ctx.Data["Title"] = issue.Name
@ -193,7 +213,7 @@ func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat
return return
} }
if ctx.User.Id != issue.PosterId { if ctx.User.Id != issue.PosterId && !ctx.Repo.IsOwner {
ctx.Handle(404, "issue.UpdateIssue", nil) ctx.Handle(404, "issue.UpdateIssue", nil)
return return
} }
@ -211,7 +231,7 @@ func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat
ctx.JSON(200, map[string]interface{}{ ctx.JSON(200, map[string]interface{}{
"ok": true, "ok": true,
"title": issue.Name, "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 package repo
import ( import (
"sort"
"github.com/gogits/gogs/models" "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" "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) { func Releases(ctx *middleware.Context) {
ctx.Data["Title"] = "Releases" ctx.Data["Title"] = "Releases"
ctx.Data["IsRepoToolbarReleases"] = true 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 { if err != nil {
ctx.Handle(404, "repo.Releases(GetTags)", err) ctx.Handle(500, "release.Releases(GetTags)", err)
return 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") 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 package repo
import ( import (
"encoding/base64"
"errors"
"fmt" "fmt"
"github.com/gogits/git"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/go-martini/martini" "github.com/go-martini/martini"
"github.com/gogits/webdav"
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/auth"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
@ -21,24 +22,27 @@ import (
"github.com/gogits/gogs/modules/middleware" "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["Title"] = "Create repository"
ctx.Data["PageIsNewRepo"] = true // For navbar arrow. ctx.Data["PageIsNewRepo"] = true
ctx.Data["LanguageIgns"] = models.LanguageIgns ctx.Data["LanguageIgns"] = models.LanguageIgns
ctx.Data["Licenses"] = models.Licenses ctx.Data["Licenses"] = models.Licenses
if ctx.Req.Method == "GET" {
ctx.HTML(200, "repo/create") 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() { if ctx.HasError() {
ctx.HTML(200, "repo/create") ctx.HTML(200, "repo/create")
return return
} }
_, err := models.CreateRepository(ctx.User, form.RepoName, form.Description, repo, err := models.CreateRepository(ctx.User, form.RepoName, form.Description,
form.Language, form.License, form.Visibility == "private", form.InitReadme == "on") form.Language, form.License, form.Private, false, form.InitReadme)
if err == nil { if err == nil {
log.Trace("%s Repository created: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, form.RepoName) log.Trace("%s Repository created: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, form.RepoName)
ctx.Redirect("/" + ctx.User.Name + "/" + 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) ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "repo/create", &form)
return 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) { func Single(ctx *middleware.Context, params martini.Params) {
branchName := ctx.Repo.BranchName branchName := ctx.Repo.BranchName
commitId := ctx.Repo.CommitId
userName := ctx.Repo.Owner.Name userName := ctx.Repo.Owner.Name
repoName := ctx.Repo.Repository.Name repoName := ctx.Repo.Repository.Name
@ -73,46 +125,42 @@ func Single(ctx *middleware.Context, params martini.Params) {
ctx.Data["IsRepoToolbarSource"] = true 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 isViewBranch := ctx.Repo.IsBranch
ctx.Data["IsViewBranch"] = isViewBranch ctx.Data["IsViewBranch"] = isViewBranch
repoFile, err := models.GetTargetFile(userName, repoName, treePath := treename
branchName, commitId, treename) if len(treePath) != 0 {
treePath = treePath + "/"
}
if err != nil && err != models.ErrRepoFileNotExist { entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treename)
ctx.Handle(404, "repo.Single(GetTargetFile)", err)
if err != nil && err != git.ErrNotExist {
ctx.Handle(404, "repo.Single(GetTreeEntryByPath)", err)
return return
} }
if len(treename) != 0 && repoFile == nil { if len(treename) != 0 && entry == nil {
ctx.Handle(404, "repo.Single", nil) ctx.Handle(404, "repo.Single", nil)
return return
} }
if repoFile != nil && repoFile.IsFile() { if entry != nil && !entry.IsDir() {
if blob, err := repoFile.LookupBlob(); err != nil { blob := entry.Blob()
ctx.Handle(404, "repo.Single(repoFile.LookupBlob)", err)
if data, err := blob.Data(); err != nil {
ctx.Handle(404, "repo.Single(blob.Data)", err)
} else { } else {
ctx.Data["FileSize"] = repoFile.Size ctx.Data["FileSize"] = blob.Size()
ctx.Data["IsFile"] = true ctx.Data["IsFile"] = true
ctx.Data["FileName"] = repoFile.Name ctx.Data["FileName"] = blob.Name()
ext := path.Ext(repoFile.Name) ext := path.Ext(blob.Name())
if len(ext) > 0 { if len(ext) > 0 {
ext = ext[1:] ext = ext[1:]
} }
ctx.Data["FileExt"] = ext ctx.Data["FileExt"] = ext
ctx.Data["FileLink"] = rawLink + "/" + treename ctx.Data["FileLink"] = rawLink + "/" + treename
data := blob.Contents()
_, isTextFile := base.IsTextFile(data) _, isTextFile := base.IsTextFile(data)
_, isImageFile := base.IsImageFile(data) _, isImageFile := base.IsImageFile(data)
ctx.Data["FileIsText"] = isTextFile ctx.Data["FileIsText"] = isTextFile
@ -120,7 +168,7 @@ func Single(ctx *middleware.Context, params martini.Params) {
if isImageFile { if isImageFile {
ctx.Data["IsImageFile"] = true ctx.Data["IsImageFile"] = true
} else { } else {
readmeExist := base.IsMarkdownFile(repoFile.Name) || base.IsReadmeFile(repoFile.Name) readmeExist := base.IsMarkdownFile(blob.Name()) || base.IsReadmeFile(blob.Name())
ctx.Data["ReadmeExist"] = readmeExist ctx.Data["ReadmeExist"] = readmeExist
if readmeExist { if readmeExist {
ctx.Data["FileContent"] = string(base.RenderMarkdown(data, "")) ctx.Data["FileContent"] = string(base.RenderMarkdown(data, ""))
@ -134,21 +182,35 @@ func Single(ctx *middleware.Context, params martini.Params) {
} else { } else {
// Directory and file list. // Directory and file list.
files, err := models.GetReposFiles(userName, repoName, ctx.Repo.CommitId, treename) tree, err := ctx.Repo.Commit.SubTree(treename)
if err != nil { if err != nil {
ctx.Handle(404, "repo.Single(GetReposFiles)", err) ctx.Handle(404, "repo.Single(SubTree)", err)
return 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 ctx.Data["Files"] = files
var readmeFile *models.RepoFile var readmeFile *git.Blob
for _, f := range files { for _, f := range entries {
if !f.IsFile() || !base.IsReadmeFile(f.Name) { if f.IsDir() || !base.IsReadmeFile(f.Name()) {
continue continue
} else { } else {
readmeFile = f readmeFile = f.Blob()
break break
} }
} }
@ -156,16 +218,15 @@ func Single(ctx *middleware.Context, params martini.Params) {
if readmeFile != nil { if readmeFile != nil {
ctx.Data["ReadmeInSingle"] = true ctx.Data["ReadmeInSingle"] = true
ctx.Data["ReadmeExist"] = 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) ctx.Handle(404, "repo.Single(readmeFile.LookupBlob)", err)
return return
} else { } else {
ctx.Data["FileSize"] = readmeFile.Size ctx.Data["FileSize"] = readmeFile.Size
ctx.Data["FileLink"] = rawLink + "/" + treename ctx.Data["FileLink"] = rawLink + "/" + treename
data := blob.Contents()
_, isTextFile := base.IsTextFile(data) _, isTextFile := base.IsTextFile(data)
ctx.Data["FileIsText"] = isTextFile ctx.Data["FileIsText"] = isTextFile
ctx.Data["FileName"] = readmeFile.Name ctx.Data["FileName"] = readmeFile.Name()
if isTextFile { if isTextFile {
ctx.Data["FileContent"] = string(base.RenderMarkdown(data, branchLink)) 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["LastCommit"] = ctx.Repo.Commit
ctx.Data["Paths"] = Paths ctx.Data["Paths"] = Paths
ctx.Data["Treenames"] = treenames ctx.Data["Treenames"] = treenames
ctx.Data["TreePath"] = treePath
ctx.Data["BranchLink"] = branchLink ctx.Data["BranchLink"] = branchLink
ctx.HTML(200, "repo/single") ctx.HTML(200, "repo/single")
} }
func SingleDownload(ctx *middleware.Context, params martini.Params) { func basicEncode(username, password string) string {
// Get tree path auth := username + ":" + password
treename := params["_1"] return base64.StdEncoding.EncodeToString([]byte(auth))
branchName := params["branchname"]
userName := params["username"]
repoName := params["reponame"]
var commitId string
if !models.IsBranchExist(userName, repoName, branchName) {
commitId = branchName
branchName = ""
} }
repoFile, err := models.GetTargetFile(userName, repoName, func basicDecode(encoded string) (user string, name string, err error) {
branchName, commitId, treename) var s []byte
s, err = base64.StdEncoding.DecodeString(encoded)
if err != nil { if err != nil {
ctx.Handle(404, "repo.SingleDownload(GetTargetFile)", err)
return return
} }
blob, err := repoFile.LookupBlob() a := strings.Split(string(s), ":")
if err != nil { if len(a) == 2 {
ctx.Handle(404, "repo.SingleDownload(LookupBlob)", err) user, name = a[0], a[1]
} else {
err = errors.New("decode failed")
}
return return
} }
data := blob.Contents() func authRequired(ctx *middleware.Context) {
contentType, isTextFile := base.IsTextFile(data) ctx.ResponseWriter.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
_, isImageFile := base.IsImageFile(data) ctx.Data["ErrorMsg"] = "no basic auth and digit auth"
ctx.Res.Header().Set("Content-Type", contentType) ctx.HTML(401, fmt.Sprintf("status/401"))
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 Setting(ctx *middleware.Context, params martini.Params) { func Setting(ctx *middleware.Context, params martini.Params) {
@ -277,43 +310,58 @@ func SettingPost(ctx *middleware.Context) {
return return
} }
ctx.Data["IsRepoToolbarSetting"] = true
switch ctx.Query("action") { switch ctx.Query("action") {
case "update": case "update":
isNameChanged := false
newRepoName := ctx.Query("name") newRepoName := ctx.Query("name")
// Check if repository name has been changed. // Check if repository name has been changed.
if ctx.Repo.Repository.Name != newRepoName { if ctx.Repo.Repository.Name != newRepoName {
isExist, err := models.IsRepositoryExist(ctx.Repo.Owner, newRepoName) isExist, err := models.IsRepositoryExist(ctx.Repo.Owner, newRepoName)
if err != nil { if err != nil {
ctx.Handle(404, "repo.SettingPost(update: check existence)", err) ctx.Handle(500, "repo.SettingPost(update: check existence)", err)
return return
} else if isExist { } else if isExist {
ctx.RenderWithErr("Repository name has been taken in your repositories.", "repo/setting", nil) ctx.RenderWithErr("Repository name has been taken in your repositories.", "repo/setting", nil)
return return
} else if err = models.ChangeRepositoryName(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName); err != nil { } 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 return
} }
log.Trace("%s Repository name changed: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newRepoName) 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 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.Description = ctx.Query("desc")
ctx.Repo.Repository.Website = ctx.Query("site") 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 { if err := models.UpdateRepository(ctx.Repo.Repository); err != nil {
ctx.Handle(404, "repo.SettingPost(update)", err) ctx.Handle(404, "repo.SettingPost(update)", err)
return 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) 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": case "transfer":
if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { 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) 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. // Check if new owner exists.
isExist, err := models.IsUserExist(newOwner) isExist, err := models.IsUserExist(newOwner)
if err != nil { if err != nil {
ctx.Handle(404, "repo.SettingPost(transfer: check existence)", err) ctx.Handle(500, "repo.SettingPost(transfer: check existence)", err)
return return
} else if !isExist { } else if !isExist {
ctx.RenderWithErr("Please make sure you entered owner name is correct.", "repo/setting", nil) ctx.RenderWithErr("Please make sure you entered owner name is correct.", "repo/setting", nil)
return return
} else if err = models.TransferOwnership(ctx.User, newOwner, ctx.Repo.Repository); err != nil { } 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 return
} }
log.Trace("%s Repository transfered: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newOwner) log.Trace("%s Repository transfered: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newOwner)
ctx.Redirect("/") ctx.Redirect("/")
return
case "delete": case "delete":
if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { 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) 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 { 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 return
} }
log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName)
ctx.Redirect("/") 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" "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) // 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["Title"] = "Setting"
ctx.Data["PageIsUserSetting"] = true // For navbar arrow. ctx.Data["PageIsUserSetting"] = true // For navbar arrow.
ctx.Data["IsUserPageSetting"] = true // For setting nav highlight. ctx.Data["IsUserPageSetting"] = true // For setting nav highlight.
@ -23,7 +31,7 @@ func Setting(ctx *middleware.Context, form auth.UpdateProfileForm) {
user := ctx.User user := ctx.User
ctx.Data["Owner"] = user ctx.Data["Owner"] = user
if ctx.Req.Method == "GET" || ctx.HasError() { if ctx.HasError() {
ctx.HTML(200, "user/setting") ctx.HTML(200, "user/setting")
return return
} }
@ -32,13 +40,13 @@ func Setting(ctx *middleware.Context, form auth.UpdateProfileForm) {
if user.Name != form.UserName { if user.Name != form.UserName {
isExist, err := models.IsUserExist(form.UserName) isExist, err := models.IsUserExist(form.UserName)
if err != nil { if err != nil {
ctx.Handle(404, "user.Setting(update: check existence)", err) ctx.Handle(500, "user.Setting(update: check existence)", err)
return return
} else if isExist { } else if isExist {
ctx.RenderWithErr("User name has been taken.", "user/setting", &form) ctx.RenderWithErr("User name has been taken.", "user/setting", &form)
return return
} else if err = models.ChangeUserName(user, form.UserName); err != nil { } 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 return
} }
log.Trace("%s User name changed: %s -> %s", ctx.Req.RequestURI, user.Name, form.UserName) 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.Avatar = base.EncodeMd5(form.Avatar)
user.AvatarEmail = form.Avatar user.AvatarEmail = form.Avatar
if err := models.UpdateUser(user); err != nil { 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 return
} }
ctx.Data["IsSuccess"] = true ctx.Data["Socials"] = socials
ctx.HTML(200, "user/setting") ctx.HTML(200, "user/social")
log.Trace("%s User setting updated: %s", ctx.Req.RequestURI, ctx.User.LowerName)
} }
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["Title"] = "Password"
ctx.Data["PageIsUserSetting"] = true ctx.Data["PageIsUserSetting"] = true
ctx.Data["IsUserPageSettingPasswd"] = true ctx.Data["IsUserPageSettingPasswd"] = true
if ctx.Req.Method == "GET" { if ctx.HasError() {
ctx.HTML(200, "user/password") ctx.HTML(200, "user/password")
return return
} }
user := ctx.User user := ctx.User
newUser := &models.User{Passwd: form.NewPasswd} tmpUser := &models.User{
if err := newUser.EncodePasswd(); err != nil { Passwd: form.OldPasswd,
ctx.Handle(200, "setting.SettingPassword", err) Salt: user.Salt,
return
} }
tmpUser.EncodePasswd()
if user.Passwd != newUser.Passwd { if user.Passwd != tmpUser.Passwd {
ctx.Data["HasError"] = true ctx.Flash.Error("Old password is not correct")
ctx.Data["ErrorMsg"] = "Old password is not correct"
} else if form.NewPasswd != form.RetypePasswd { } else if form.NewPasswd != form.RetypePasswd {
ctx.Data["HasError"] = true ctx.Flash.Error("New password and re-type password are not same")
ctx.Data["ErrorMsg"] = "New password and re-type password are not same"
} else { } else {
user.Passwd = newUser.Passwd user.Passwd = form.NewPasswd
user.Salt = models.GetUserSalt()
user.EncodePasswd()
if err := models.UpdateUser(user); err != nil { if err := models.UpdateUser(user); err != nil {
ctx.Handle(200, "setting.SettingPassword", err) ctx.Handle(200, "setting.SettingPassword", err)
return 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.Redirect("/user/setting/password")
ctx.HTML(200, "user/password")
log.Trace("%s User password updated: %s", ctx.Req.RequestURI, ctx.User.LowerName)
} }
func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) { func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
@ -134,7 +161,7 @@ func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
// Add new SSH key. // Add new SSH key.
if ctx.Req.Method == "POST" { if ctx.Req.Method == "POST" {
if hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) { if ctx.HasError() {
ctx.HTML(200, "user/publickey") ctx.HTML(200, "user/publickey")
return return
} }
@ -149,11 +176,13 @@ func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
ctx.RenderWithErr("Public key name has been used", "user/publickey", &form) ctx.RenderWithErr("Public key name has been used", "user/publickey", &form)
return return
} }
ctx.Handle(200, "ssh.AddPublicKey", err) ctx.Handle(500, "ssh.AddPublicKey", err)
log.Trace("%s User SSH key added: %s", ctx.Req.RequestURI, ctx.User.LowerName)
return return
} else { } 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. // Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package user package user
import ( import (
"encoding/json" "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/log"
"github.com/gogits/gogs/modules/oauth2" "github.com/gogits/gogs/modules/middleware"
"github.com/gogits/gogs/modules/social"
) )
// github && google && ... func extractPath(next string) string {
func SocialSignIn(tokens oauth2.Tokens) { n, err := url.Parse(next)
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)
if err != nil { if err != nil {
log.Error("connect with github error: %s", err) return "/"
// FIXME: handle error page }
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 return
} }
defer r.Body.Close()
user := &GithubUser{} next := extractPath(ctx.Query("next"))
err = json.NewDecoder(r.Body).Decode(user) 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 { if err != nil {
log.Error("Get: %s", err) ctx.Handle(500, "social.SocialSignIn(Exchange)", err)
return
} }
log.Info("login: %s", user.Name) next = extractPath(ctx.Query("state"))
// FIXME: login here, user email to check auth, if not registe, then generate a uniq username 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 package user
import ( import (
"fmt"
"net/url" "net/url"
"strings" "strings"
"github.com/go-martini/martini"
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/auth"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
@ -19,65 +16,20 @@ import (
"github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/middleware"
) )
func Dashboard(ctx *middleware.Context) { func SignIn(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) {
ctx.Data["Title"] = "Log In" 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. // Check auto-login.
userName := ctx.GetCookie(base.CookieUserName) userName := ctx.GetCookie(base.CookieUserName)
if len(userName) == 0 { if len(userName) == 0 {
@ -88,36 +40,48 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) {
isSucceed := false isSucceed := false
defer func() { defer func() {
if !isSucceed { 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.CookieUserName, "", -1)
ctx.SetCookie(base.CookieRememberName, "", -1) ctx.SetCookie(base.CookieRememberName, "", -1)
return
} }
}() }()
user, err := models.GetUserByName(userName) user, err := models.GetUserByName(userName)
if err != nil { if err != nil {
ctx.HTML(200, "user/signin") ctx.HTML(500, "user/signin")
return return
} }
secret := base.EncodeMd5(user.Rands + user.Passwd) secret := base.EncodeMd5(user.Rands + user.Passwd)
value, _ := ctx.GetSecureCookie(secret, base.CookieRememberName) value, _ := ctx.GetSecureCookie(secret, base.CookieRememberName)
if value != user.Name { if value != user.Name {
ctx.HTML(200, "user/signin") ctx.HTML(500, "user/signin")
return return
} }
isSucceed = true isSucceed = true
ctx.Session.Set("userId", user.Id) ctx.Session.Set("userId", user.Id)
ctx.Session.Set("userName", user.Name) ctx.Session.Set("userName", user.Name)
redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")) if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
if len(redirectTo) > 0 {
ctx.SetCookie("redirect_to", "", -1) ctx.SetCookie("redirect_to", "", -1)
ctx.Redirect(redirectTo) ctx.Redirect(redirectTo)
} else { return
}
ctx.Redirect("/") 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() { if ctx.HasError() {
@ -133,7 +97,7 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) {
return return
} }
ctx.Handle(200, "user.SignIn", err) ctx.Handle(500, "user.SignIn", err)
return return
} }
@ -144,40 +108,116 @@ func SignIn(ctx *middleware.Context, form auth.LogInForm) {
ctx.SetSecureCookie(secret, base.CookieRememberName, user.Name, days) 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("userId", user.Id)
ctx.Session.Set("userName", user.Name) ctx.Session.Set("userName", user.Name)
redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")) if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
if len(redirectTo) > 0 {
ctx.SetCookie("redirect_to", "", -1) ctx.SetCookie("redirect_to", "", -1)
ctx.Redirect(redirectTo) ctx.Redirect(redirectTo)
} else { return
}
ctx.Redirect("/") 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) { func SignOut(ctx *middleware.Context) {
ctx.Session.Delete("userId") ctx.Session.Delete("userId")
ctx.Session.Delete("userName") ctx.Session.Delete("userName")
ctx.Session.Delete("socialId")
ctx.Session.Delete("socialName")
ctx.Session.Delete("socialEmail")
ctx.SetCookie(base.CookieUserName, "", -1) ctx.SetCookie(base.CookieUserName, "", -1)
ctx.SetCookie(base.CookieRememberName, "", -1) ctx.SetCookie(base.CookieRememberName, "", -1)
ctx.Redirect("/") ctx.Redirect("/")
} }
func SignUp(ctx *middleware.Context, form auth.RegisterForm) { func SignUp(ctx *middleware.Context) {
ctx.Data["Title"] = "Sign Up" ctx.Data["Title"] = "Sign Up"
ctx.Data["PageIsSignUp"] = true ctx.Data["PageIsSignUp"] = true
if base.Service.DisenableRegisteration { if base.Service.DisableRegistration {
ctx.Data["DisenableRegisteration"] = true ctx.Data["DisableRegistration"] = true
ctx.HTML(200, "user/signup") ctx.HTML(200, "user/signup")
return return
} }
if ctx.Req.Method == "GET" { if sid, ok := ctx.Session.Get("socialId").(int64); ok {
ctx.HTML(200, "user/signup") oauthSignUp(ctx, sid)
return 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 { if form.Password != form.RetypePasswd {
ctx.Data["HasError"] = true ctx.Data["HasError"] = true
ctx.Data["Err_Password"] = true ctx.Data["Err_Password"] = true
@ -195,7 +235,7 @@ func SignUp(ctx *middleware.Context, form auth.RegisterForm) {
Name: form.UserName, Name: form.UserName,
Email: form.Email, Email: form.Email,
Passwd: form.Password, Passwd: form.Password,
IsActive: !base.Service.RegisterEmailConfirm, IsActive: !base.Service.RegisterEmailConfirm || isOauth,
} }
var err error var err error
@ -208,20 +248,30 @@ func SignUp(ctx *middleware.Context, form auth.RegisterForm) {
case models.ErrUserNameIllegal: case models.ErrUserNameIllegal:
ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "user/signup", &form) ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "user/signup", &form)
default: default:
ctx.Handle(200, "user.SignUp", err) ctx.Handle(500, "user.SignUp(RegisterUser)", err)
} }
return 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. // Bind social account.
if base.Service.RegisterEmailConfirm && u.Id > 1 { 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) mailer.SendRegisterMail(ctx.Render, u)
ctx.Data["IsSendRegisterMail"] = true ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email ctx.Data["Email"] = u.Email
ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60 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 { if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err) log.Error("Set cache(MailResendLimit) fail: %v", err)
@ -235,25 +285,28 @@ func Delete(ctx *middleware.Context) {
ctx.Data["Title"] = "Delete Account" ctx.Data["Title"] = "Delete Account"
ctx.Data["PageIsUserSetting"] = true ctx.Data["PageIsUserSetting"] = true
ctx.Data["IsUserPageSettingDelete"] = true ctx.Data["IsUserPageSettingDelete"] = true
if ctx.Req.Method == "GET" {
ctx.HTML(200, "user/delete") 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() tmpUser.EncodePasswd()
if len(tmpUser.Passwd) == 0 || tmpUser.Passwd != ctx.User.Passwd { if tmpUser.Passwd != ctx.User.Passwd {
ctx.Data["HasError"] = true ctx.Flash.Error("Password is not correct. Make sure you are owner of this account.")
ctx.Data["ErrorMsg"] = "Password is not correct. Make sure you are owner of this account."
} else { } else {
if err := models.DeleteUser(ctx.User); err != nil { if err := models.DeleteUser(ctx.User); err != nil {
ctx.Data["HasError"] = true
switch err { switch err {
case models.ErrUserOwnRepos: 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: default:
ctx.Handle(200, "user.Delete", err) ctx.Handle(500, "user.Delete", err)
return return
} }
} else { } else {
@ -262,118 +315,7 @@ func Delete(ctx *middleware.Context) {
} }
} }
ctx.HTML(200, "user/delete") ctx.Redirect("/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")
} }
func Activate(ctx *middleware.Context) { func Activate(ctx *middleware.Context) {
@ -391,11 +333,15 @@ func Activate(ctx *middleware.Context) {
} else { } else {
ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60 ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60
mailer.SendActiveMail(ctx.Render, ctx.User) 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 { } else {
ctx.Data["ServiceNotEnabled"] = true ctx.Data["ServiceNotEnabled"] = true
} }
ctx.HTML(200, "user/active") ctx.HTML(200, "user/activate")
return return
} }
@ -403,9 +349,12 @@ func Activate(ctx *middleware.Context) {
if user := models.VerifyUserActiveCode(code); user != nil { if user := models.VerifyUserActiveCode(code); user != nil {
user.IsActive = true user.IsActive = true
user.Rands = models.GetUserSalt() 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("userId", user.Id)
ctx.Session.Set("userName", user.Name) ctx.Session.Set("userName", user.Name)
@ -414,5 +363,106 @@ func Activate(ctx *middleware.Context) {
} }
ctx.Data["IsActivateFailed"] = true 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" "strings"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
"github.com/gogits/gogs/modules/log" qlog "github.com/qiniu/log"
//"github.com/gogits/git" //"github.com/gogits/git"
"github.com/gogits/gogs/models" "github.com/gogits/gogs/models"
@ -44,11 +44,16 @@ gogs serv provide access auth for repositories`,
} }
func newLogger(execDir string) { func newLogger(execDir string) {
level := "0"
logPath := execDir + "/log/serv.log" logPath := execDir + "/log/serv.log"
os.MkdirAll(path.Dir(logPath), os.ModePerm) 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) { func parseCmd(cmd string) (string, string) {
@ -87,21 +92,18 @@ func runServ(k *cli.Context) {
keys := strings.Split(os.Args[2], "-") keys := strings.Split(os.Args[2], "-")
if len(keys) != 2 { if len(keys) != 2 {
println("auth file format error") println("auth file format error")
log.Error("auth file format error") qlog.Fatal("auth file format error")
return
} }
keyId, err := strconv.ParseInt(keys[1], 10, 64) keyId, err := strconv.ParseInt(keys[1], 10, 64)
if err != nil { if err != nil {
println("auth file format error") println("auth file format error")
log.Error("auth file format error", err) qlog.Fatal("auth file format error", err)
return
} }
user, err := models.GetUserByKeyId(keyId) user, err := models.GetUserByKeyId(keyId)
if err != nil { if err != nil {
println("You have no right to access") println("You have no right to access")
log.Error("SSH visit error: %v", err) qlog.Fatalf("SSH visit error: %v", err)
return
} }
cmd := os.Getenv("SSH_ORIGINAL_COMMAND") cmd := os.Getenv("SSH_ORIGINAL_COMMAND")
@ -114,24 +116,19 @@ func runServ(k *cli.Context) {
repoPath := strings.Trim(args, "'") repoPath := strings.Trim(args, "'")
rr := strings.SplitN(repoPath, "/", 2) rr := strings.SplitN(repoPath, "/", 2)
if len(rr) != 2 { if len(rr) != 2 {
println("Unavilable repository", args) println("Unavailable repository", args)
log.Error("Unavilable repository %v", args) qlog.Fatalf("Unavailable repository %v", args)
return
} }
repoUserName := rr[0] repoUserName := rr[0]
repoName := rr[1] repoName := strings.TrimSuffix(rr[1], ".git")
if strings.HasSuffix(repoName, ".git") {
repoName = repoName[:len(repoName)-4]
}
isWrite := In(verb, COMMANDS_WRITE) isWrite := In(verb, COMMANDS_WRITE)
isRead := In(verb, COMMANDS_READONLY) isRead := In(verb, COMMANDS_READONLY)
repoUser, err := models.GetUserByName(repoUserName) repoUser, err := models.GetUserByName(repoUserName)
if err != nil { if err != nil {
fmt.Println("You have no right to access") println("You have no right to access")
log.Error("Get user failed", err) qlog.Fatal("Get user failed", err)
return
} }
// access check // access check
@ -139,55 +136,45 @@ func runServ(k *cli.Context) {
case isWrite: case isWrite:
has, err := models.HasAccess(user.LowerName, path.Join(repoUserName, repoName), models.AU_WRITABLE) has, err := models.HasAccess(user.LowerName, path.Join(repoUserName, repoName), models.AU_WRITABLE)
if err != nil { if err != nil {
println("Inernel error:", err) println("Internal error:", err)
log.Error(err.Error()) qlog.Fatal(err)
return
} else if !has { } else if !has {
println("You have no right to write this repository") println("You have no right to write this repository")
log.Error("User %s has no right to write repository %s", user.Name, repoPath) qlog.Fatalf("User %s has no right to write repository %s", user.Name, repoPath)
return
} }
case isRead: case isRead:
repo, err := models.GetRepositoryByName(repoUser.Id, repoName) repo, err := models.GetRepositoryByName(repoUser.Id, repoName)
if err != nil { if err != nil {
println("Get repository error:", err) println("Get repository error:", err)
log.Error("Get repository error: " + err.Error()) qlog.Fatal("Get repository error: " + err.Error())
return
} }
if !repo.IsPrivate { if !repo.IsPrivate {
break 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 { if err != nil {
println("Inernel error") println("Internal error")
log.Error(err.Error()) qlog.Fatal(err)
return
} }
if !has { if !has {
has, err = models.HasAccess(user.Name, repoPath, models.AU_WRITABLE) has, err = models.HasAccess(user.Name, repoPath, models.AU_WRITABLE)
if err != nil { if err != nil {
println("Inernel error") println("Internal error")
log.Error(err.Error()) qlog.Fatal(err)
return
} }
} }
if !has { if !has {
println("You have no right to access this repository") println("You have no right to access this repository")
log.Error("You have no right to access this repository") qlog.Fatal("You have no right to access this repository")
return
} }
default: default:
println("Unknown command") println("Unknown command")
log.Error("Unknown command") qlog.Fatal("Unknown command")
return
} }
// for update use models.SetRepoEnvs(user.Id, user.Name, repoName)
os.Setenv("userName", user.Name)
os.Setenv("userId", strconv.Itoa(int(user.Id)))
os.Setenv("repoName", repoName)
gitcmd := exec.Command(verb, repoPath) gitcmd := exec.Command(verb, repoPath)
gitcmd.Dir = base.RepoRootPath gitcmd.Dir = base.RepoRootPath
@ -197,7 +184,15 @@ func runServ(k *cli.Context) {
if err = gitcmd.Run(); err != nil { if err = gitcmd.Run(); err != nil {
println("execute command error:", err.Error()) println("execute command error:", err.Error())
log.Error("execute command error: " + err.Error()) qlog.Fatal("execute command error: " + err.Error())
return
} }
//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 # start gogs web
# #
cd "$(dirname $0)" IFS='
./gogs web '
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"> <dl class="dl-horizontal admin-dl-horizontal">
<dt>Register Email Confirmation</dt> <dt>Register Email Confirmation</dt>
<dd><i class="fa fa{{if .Service.RegisterEmailConfirm}}-check{{end}}-square-o"></i></dd> <dd><i class="fa fa{{if .Service.RegisterEmailConfirm}}-check{{end}}-square-o"></i></dd>
<dt>Disenable Registeration</dt> <dt>Disable Registration</dt>
<dd><i class="fa fa{{if .Service.DisenableRegisteration}}-check{{end}}-square-o"></i></dd> <dd><i class="fa fa{{if .Service.DisableRegistration}}-check{{end}}-square-o"></i></dd>
<dt>Require Sign In View</dt> <dt>Require Sign In View</dt>
<dd><i class="fa fa{{if .Service.RequireSignInView}}-check{{end}}-square-o"></i></dd> <dd><i class="fa fa{{if .Service.RequireSignInView}}-check{{end}}-square-o"></i></dd>
<dt>Mail Notification</dt> <dt>Mail Notification</dt>
@ -88,12 +88,34 @@
<dl class="dl-horizontal admin-dl-horizontal"> <dl class="dl-horizontal admin-dl-horizontal">
<dt>Enabled</dt> <dt>Enabled</dt>
<dd><i class="fa fa{{if .MailerEnabled}}-check{{end}}-square-o"></i></dd> <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> <dd>{{.Mailer.Name}}</dd>
<dt>Host</dt> <dt>Host</dt>
<dd>{{.Mailer.Host}}</dd> <dd>{{.Mailer.Host}}</dd>
<dt>User</dt> <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> </dl>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@
</div> </div>
<div class="panel-body"> <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>
</div> </div>

View File

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

View File

@ -12,7 +12,7 @@
<br/> <br/>
<form action="/admin/users/new" method="post" class="form-horizontal"> <form action="/admin/users/new" method="post" class="form-horizontal">
{{.CsrfTokenHtml}} {{.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}}"> <div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}">
<label class="col-md-3 control-label">Username: </label> <label class="col-md-3 control-label">Username: </label>
<div class="col-md-7"> <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="description" content="Gogs(Go Git Service) is a GitHub-like clone in the Go Programming Language" />
<meta name="keywords" content="go, git"> <meta name="keywords" content="go, git">
<meta name="_csrf" content="{{.CsrfToken}}" /> <meta name="_csrf" content="{{.CsrfToken}}" />
{{if .Repository.IsGoget}}<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}">{{end}}
<!-- Stylesheets --> <!-- 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/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/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/jquery-1.10.1.min.js"></script>
<script src="/js/bootstrap.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/lib.js"></script>
<script src="/js/app.js"></script> <script src="/js/app.js"></script>
<title>{{if .Title}}{{.Title}} - {{end}}{{AppName}}</title> <title>{{if .Title}}{{.Title}} - {{end}}{{AppName}}</title>

View File

@ -1,16 +1,38 @@
<div class="masthead navbar" id="masthead"> <div class="masthead navbar" id="masthead">
<div class="container"> <div class="container">
<nav class="nav"> <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 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{{if .PageIsUserDashboard}} active{{end}}" href="/">Dashboard</a> <a class="nav-item pull-left{{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 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-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}}"> <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"/> <img src="{{.SignedUser.AvatarLink}}?s=28" alt="user-avatar" title="username"/>
</a> </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> <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}} {{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> {{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}} <a id="nav-signup" class="nav-item navbar-right" href="/user/sign_up/">Sign Up</a>{{end}}
</nav> </nav>

View File

@ -1,8 +1,27 @@
{{template "base/head" .}} {{template "base/head" .}}
{{template "base/navbar" .}} {{template "base/navbar" .}}
<div id="body" class="container"> <div id="body" class="container">
{{if not .Repos}}
<h4>Hey there, welcome to the land of Gogs!</h4> <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"> <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> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View File

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

View File

@ -6,6 +6,7 @@
<div id="issue"> <div id="issue">
<form class="form" action="{{.RepoLink}}/issues/new" method="post" id="issue-create-form"> <form class="form" action="{{.RepoLink}}/issues/new" method="post" id="issue-create-form">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
{{template "base/alert" .}}
<div class="col-md-1"> <div class="col-md-1">
<img class="avatar" src="{{.SignedUser.AvatarLink}}" alt=""/> <img class="avatar" src="{{.SignedUser.AvatarLink}}" alt=""/>
</div> </div>
@ -19,7 +20,7 @@
</div> </div>
<ul class="nav nav-tabs" data-init="tabs"> <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="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> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane" id="issue-textarea"> <div class="tab-pane" id="issue-textarea">

View File

@ -72,7 +72,7 @@
</div> </div>
<ul class="nav nav-tabs" data-init="tabs"> <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="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> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane" id="issue-textarea"> <div class="tab-pane" id="issue-textarea">

View File

@ -15,11 +15,11 @@
Hi <span style="color: #00BFFF;">{{.User.Name}}</span>, Hi <span style="color: #00BFFF;">{{.User.Name}}</span>,
</div> </div>
<div style="font-size:14px; padding:0 15px;"> <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;"> <p style="margin:0;padding:0 0 9px 0;">
<a href="{{.AppUrl}}user/activate?code={{.Code}}">{{.AppUrl}}user/activate?code={{.Code}}</a> <a href="{{.AppUrl}}user/activate?code={{.Code}}">{{.AppUrl}}user/activate?code={{.Code}}</a>
</p> </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> </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> <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="padding:40px 15px;">
<div style="font-size:16px; padding-bottom:30px; font-weight:bold;"> <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>
<div style="font-size:14px; padding:0 15px;"> <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;"> <p style="margin:0;padding:0 0 9px 0;">
<a href="{{.AppUrl}}user/activate?code={{.Code}}">{{.AppUrl}}user/activate?code={{.Code}}</a> <a href="{{.AppUrl}}user/activate?code={{.Code}}">{{.AppUrl}}user/activate?code={{.Code}}</a>
</p> </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> </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="body" class="container">
<div id="release"> <div id="release">
<h4 id="release-head"> <h4 id="release-head">
<span class="release"><strong>Release</strong></span> / <span class="release"><strong>Releases</strong></span><!-- /
<a class="tag" href="/{tag_link}">Tags</a> <a class="tag" href="/{tag_link}">Tags</a> -->
<!-- comment : if in tag page, show a.release and span.tag please --> <!-- comment : if in tag page, show a.release and span.tag please -->
</h4> </h4>
<ul id="release-list" class="list-unstyled"> <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"> <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>
<div class="col-md-10"> <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"> <p class="info">
<span class="author"><img class="avatar" src="http://1.gravatar.com/avatar/f72f7454ce9d710baa506394f68f4132" alt="" width="20">&nbsp;&nbsp; <span class="author"><img class="avatar" src="{{.Publisher.AvatarLink}}" alt="" width="20">&nbsp;&nbsp;
<a href="/user/fuxiaohei">fuxiaohei</a></span> <a href="/user/{{.Publisher.Name}}">{{.Publisher.Name}}</a></span>
<span class="time">1 week ago</span> {{if .Created}}<span class="time">{{TimeSince .Created}}</span>{{end}}
<span class="ahead"><strong>0</strong> commits since this tag</span> <span class="ahead"><strong>{{.NumCommitsBehind}}</strong> commits since this release</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>
</p> </p>
<div class="markdown desc"> <div class="markdown desc">
release descriptions, support markdown content {{str2html .Note}}
</div> </div>
<p class="download"> <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="{{$.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> <!-- <a class="btn btn-default" href="{release_download_link}"><i class="fa fa-download"></i>Source Code (TAR.GZ)</a> -->
</p> </p>
<span class="dot">&nbsp;</span> <span class="dot">&nbsp;</span>
</div> </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>
<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"> <div class="col-md-2 text-right">
<span class="btn btn-warning status pre-release">Pre-Release</span> <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> <a class="tag" href="{commit_link}"><i class="fa fa-tag"></i>release tag</a>
@ -76,11 +73,8 @@
</p> </p>
<span class="dot">&nbsp;</span> <span class="dot">&nbsp;</span>
</div> </div>
</li> </li> -->
</ul> </ul>
</div> </div>
{{range .Releases}}
{{.}}
{{end}}
</div> </div>
{{template "base/footer" .}} {{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 id="commits">
<div class="panel panel-default commit-box info-box"> <div class="panel panel-default commit-box info-box">
<div class="panel-heading info-head"> <div class="panel-heading info-head">
<div class="search pull-right form"> <form class="search pull-right col-md-3" action="{{.RepoLink}}/commits/{{.BranchName}}/search" method="get" id="commits-search-form">
<input class="form-control search" type="search" placeholder="search commit"/> <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>
</div>
</form>
<h4>{{.CommitCount}} Commits</h4> <h4>{{.CommitCount}} Commits</h4>
</div> </div>
<table class="panel-footer table commit-list table table-striped"> <table class="panel-footer table commit-list table table-striped">
<thead> <thead>
<tr> <tr>
<th class="author">Author</th> <th class="author">Author</th>
<th class="sha">Commit</th> <th class="sha">SHA1</th>
<th class="message">Message</th> <th class="message">Message</th>
<th class="date">Date</th> <th class="date">Date</th>
</tr> </tr>
@ -26,15 +31,19 @@
{{$r := List .Commits}} {{$r := List .Commits}}
{{range $r}} {{range $r}}
<tr> <tr>
<td class="author"><img class="avatar" src="{{AvatarLink .Committer.Email}}" alt=""/><a href="/user/{{.Committer.Name}}">{{.Committer.Name}}</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 class="label label-success" href="/{{$username}}/{{$reponame}}/commit/{{.Id}} ">{{SubStr .Id.String 0 10}} </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="message">{{.Message}} </td>
<td class="date">{{TimeSince .Committer.When}}</td> <td class="date">{{TimeSince .Author.When}}</td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
</div> </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>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View File

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

View File

@ -5,7 +5,7 @@
<div id="source"> <div id="source">
<div class="panel panel-info diff-box diff-head-box"> <div class="panel panel-info diff-box diff-head-box">
<div class="panel-heading"> <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> <h4>{{.Commit.Message}}</h4>
</div> </div>
<div class="panel-body"> <div class="panel-body">
@ -14,12 +14,15 @@
</span> </span>
<p class="author"> <p class="author">
<img class="avatar" src="{{AvatarLink .Commit.Author.Email}}" alt=""/> <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> <span class="time">{{TimeSince .Commit.Author.When}}</span>
</p> </p>
</div> </div>
</div> </div>
{{if .DiffNotAvailable}}
<h4>Diff Data Not Available.</h4>
{{else}}
<div class="diff-detail-box diff-box"> <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> <a class="pull-right btn btn-default" data-toggle="collapse" data-target="#diff-files">Show Diff Stats</a>
<p class="showing"> <p class="showing">
@ -30,12 +33,16 @@
{{range .Diff.Files}} {{range .Diff.Files}}
<li> <li>
<div class="diff-counter count pull-right"> <div class="diff-counter count pull-right">
{{if not .IsBin}}
<span class="add" data-line="{{.Addition}}">{{.Addition}}</span> <span class="add" data-line="{{.Addition}}">{{.Addition}}</span>
<span class="bar"> <span class="bar">
<span class="pull-left add"></span> <span class="pull-left add"></span>
<span class="pull-left del"></span> <span class="pull-left del"></span>
</span> </span>
<span class="del" data-line="{{.Deletion}}">{{.Deletion}}</span> <span class="del" data-line="{{.Deletion}}">{{.Deletion}}</span>
{{else}}
<span>BIN</span>
{{end}}
</div> </div>
<!-- todo finish all file status, now modify, add, delete and rename --> <!-- 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> <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 panel-default diff-file-box diff-box file-content" id="diff-2">
<div class="panel-heading"> <div class="panel-heading">
<div class="diff-counter count pull-left"> <div class="diff-counter count pull-left">
{{if not .IsBin}}
<span class="add" data-line="{{.Addition}}">+ {{.Addition}}</span> <span class="add" data-line="{{.Addition}}">+ {{.Addition}}</span>
<span class="bar"> <span class="bar">
<span class="pull-left add"></span> <span class="pull-left add"></span>
<span class="pull-left del"></span> <span class="pull-left del"></span>
</span> </span>
<span class="del" data-line="{{.Deletion}}">- {{.Deletion}}</span> <span class="del" data-line="{{.Deletion}}">- {{.Deletion}}</span>
{{else}}
BIN
{{end}}
</div> </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> <span class="file">{{.Name}}</span>
</div> </div>
{{$isImage := (call $.IsImageFile .Name)}} {{$isImage := (call $.IsImageFile .Name)}}
@ -83,338 +94,13 @@
</tr> </tr>
{{end}} {{end}}
{{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> </tbody>
</table> </table>
{{end}} {{end}}
</div> </div>
</div> </div>
{{end}} {{end}}
{{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> -->
</div> </div>
</div> </div>
{{template "base/footer" .}} {{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="container">
<div class="row"> <div class="row">
<div class="col-md-7"> <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> <p class="desc">{{.Repository.Description}}{{if .Repository.Website}} <a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}</p>
</div> </div>
<div class="col-md-5 actions text-right clone-group-btn"> <div class="col-md-5 actions text-right clone-group-btn">
{{if not .IsBareRepo}} {{if not .IsBareRepo}}
<div class="btn-group" id="repo-clone"> <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"> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span> <span class="caret"></span>
</button> </button>
@ -24,10 +24,10 @@
</span> </span>
</div> </div>
<p class="help-block text-center">Need help cloning? Visit <a href="#">Help</a>!</p> <p class="help-block text-center">Need help cloning? Visit <a href="#">Help</a>!</p>
<!-- <hr/> <hr/>
<div class="clone-zip text-center"> <div class="clone-zip text-center">
<a class="btn btn-success btn-lg" href="#"><i class="fa fa-suitcase"></i>Download ZIP</a> <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>
</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"> <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>
<div id="repo-setting-container" class="col-md-9"> <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 panel-default">
<div class="panel-heading"> <div class="panel-heading">
Repository Options Repository Options
@ -23,9 +23,10 @@
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input type="hidden" name="action" value="update"> <input type="hidden" name="action" value="update">
<div class="form-group"> <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"> <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>
</div> </div>
@ -42,14 +43,44 @@
<input type="url" class="form-control" name="site" value="{{.Repository.Website}}" /> <input type="url" class="form-control" name="site" value="{{.Repository.Website}}" />
</div> </div>
</div> </div>
<!-- <div class="form-group"> <hr>
<div class="form-group">
<label class="col-md-3 text-right">Default Branch</label> <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"> <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> </select>
</div> </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="form-group">
<div class="col-md-9 col-md-offset-3"> <div class="col-md-9 col-md-offset-3">
<button class="btn btn-primary" type="submit">Save Options</button> <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 panel-default info-box">
<div class="panel-heading info-head"> <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>
<div class="panel-body info-content"> <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> <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> <td class="date"></td>
</tr> </tr>
{{end}} {{end}}
{{range .Files}} {{range $item := .Files}}
<tr {{$entry := index $item 0}}
{{if .IsDir}}class="is-dir"{{end}}> {{$commit := index $item 1}}
<tr {{if $entry.IsDir}}class="is-dir"{{end}}>
<td class="icon"> <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>
<td class="name"> <td class="name">
<span class="wrap"> <span class="wrap">
{{if .IsDir}} <a href="{{$.BranchLink}}/{{$.TreePath}}{{$entry.Name}}">{{$entry.Name}}</a>
<a href="{{$.BranchLink}}/{{.Path}}">{{.Name}}</a>
{{else}}
<a href="{{$.BranchLink}}/{{.Path}}">{{.Name}}</a>
{{end}}
</span> </span>
</td> </td>
<td class="text"> <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>
<td class="date"> <td class="date">
<span class="wrap">{{TimeSince .Commit.Committer.When}}</span> <span class="wrap">{{TimeSince $commit.Committer.When}}</span>
</td> </td>
</tr> </tr>
{{end}} {{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> <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}} {{if .IsRepoToolbarIssues}}
<li class="tmp">{{if .IsRepoToolbarIssuesList}}<a href="{{.RepoLink}}/issues/new"><button class="btn btn-primary btn-sm">New Issue</button> <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}} {{end}}
<li class="{{if .IsRepoToolbarReleases}}active{{end}}"><a href="{{.RepoLink}}/releases">{{if .Repository.NumReleases}}<span class="badge">{{.Repository.NumReleases}}</span> {{end}}Releases</a></li> <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 .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> <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"> <!-- <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">More <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">More <b class="caret"></b></a>
<ul class="dropdown-menu"> <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