release/v1.15
Lunny Xiao 2014-03-22 00:50:47 +08:00
commit 17da2fd2e3
58 changed files with 1831 additions and 344 deletions

4
.gitignore vendored
View File

@ -5,4 +5,6 @@ gogs
*.db
*.log
custom/
.vendor/
.vendor/
.idea/
*.iml

View File

@ -9,13 +9,12 @@ github.com/Unknwon/com=
github.com/Unknwon/cae=
github.com/Unknwon/goconfig=
github.com/dchest/scrypt=
github.com/go-sql-driver/mysql=
github.com/lib/pq=
github.com/lunny/xorm=
github.com/gogits/logs=
github.com/gogits/binding=
github.com/gogits/git=
github.com/gogits/gfm=
github.com/gogits/cache=
[res]
include=templates|public|conf

49
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,49 @@
# Contributing to Gogs
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.
## Contribution guidelines
### Pull requests are always welcome
We are always thrilled to receive pull requests, and do our best to
process them as fast as possible. Not sure if that typo is worth a pull
request? Do it! We will appreciate it.
If your pull request is not accepted on the first try, don't be
discouraged! If there's a problem with the implementation, hopefully you
received feedback on what to improve.
We're trying very hard to keep Gogs lean and focused. We don't want it
to do everything for everybody. This means that we might decide against
incorporating a new feature.
### Discuss your design on the mailing list
We recommend discussing your plans [on the mailing
list](https://groups.google.com/forum/#!forum/gogits)
before starting to code - especially for more ambitious contributions.
This gives other contributors a chance to point you in the right
direction, give feedback on your design, and maybe point out if someone
else is working on the same thing.
We may close your pull request if not first discussed on the mailing
list. We aren't doing this to be jerks. We are doing this to prevent
people from spending large amounts of time on changes that may need
to be designed or architected in a specific way, or may not align with
the vision of the project.
### Create issues...
Any significant improvement should be documented as [a GitHub
issue](https://github.com/gogits/gogs/issues) before anybody
starts working on it.
### ...but check for existing issues first!
Please take a moment to check that an issue doesn't already exist
documenting your bug report or improvement proposal. If it does, it
never hurts to add a quick "+1" or "I have this problem too". This will
help prioritize the most common problems and requests.

View File

@ -5,7 +5,7 @@ Gogs(Go Git Service) is a GitHub-like clone in the Go Programming Language.
Since we choose to use pure Go implementation of Git manipulation, Gogs certainly supports **ALL platforms** that Go supports, including Linux, Max OS X, and Windows with **ZERO** dependency.
##### Current version: 0.1.1 Alpha
##### Current version: 0.1.5 Alpha
## Purpose
@ -15,7 +15,7 @@ There are some very good products in this category such as [gitlab](http://gitla
- Please see [Wiki](https://github.com/gogits/gogs/wiki) for project design, develop specification, change log and road map.
- See [Trello Broad](https://trello.com/b/uxAoeLUl/gogs-go-git-service) to follow the develop team.
- Try it before anything? 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).
## Features
@ -28,7 +28,8 @@ There are some very good products in this category such as [gitlab](http://gitla
- Repository viewer.
- Gravatar support.
- Mail service(register).
- Supports MySQL and PostgreSQL.
- Administration panel.
- Supports MySQL, PostgreSQL and SQLite3(binary release only).
## Installation

View File

@ -8,8 +8,8 @@ RUN_MODE = dev
[repository]
ROOT = /Users/%(RUN_USER)s/git/gogs-repositories
LANG_IGNS = Google Go|C|Python|Ruby|C Sharp
LICENSES = Apache v2 License|GPL v2|MIT License|Affero GPL|BSD (3-Clause) License
LANG_IGNS = Google Go|C|C++|Python|Ruby|C Sharp
LICENSES = Apache v2 License|GPL v2|MIT License|Affero GPL|Artistic License 2.0|BSD (3-Clause) License
[server]
DOMAIN = localhost
@ -18,7 +18,7 @@ HTTP_ADDR =
HTTP_PORT = 3000
[database]
; Either "mysql" or "postgres", it's your choice
; Either "mysql", "postgres" or "sqlite3"(binary release only), it's your choice
DB_TYPE = mysql
HOST =
NAME = gogs
@ -26,6 +26,10 @@ USER = root
PASSWD =
; For "postgres" only, either "disable", "require" or "verify-full"
SSL_MODE = disable
; For "sqlite3" only
PATH = data/gogs.db
[admin]
[security]
; !!CHANGE THIS TO KEEP YOUR USER DATA SAFE!!
@ -36,6 +40,10 @@ ACTIVE_CODE_LIVE_MINUTES = 180
RESET_PASSWD_CODE_LIVE_MINUTES = 180
; User need to confirm e-mail for registration
REGISTER_EMAIL_CONFIRM = false
; Does not allow register and admin create account only
DISENABLE_REGISTERATION = false
; User must sign in to view anything.
REQUIRE_SIGNIN_VIEW = false
[mailer]
ENABLED = false
@ -52,6 +60,16 @@ FROM =
USER =
PASSWD =
[cache]
; Either "memory", "redis", or "memcache", default is "memory"
ADAPTER = memory
; For "memory" only, GC interval in seconds, default is 60
INTERVAL = 60
; For "redis" and "memcache", connection host address
; redis: ":6039"
; memcache: "127.0.0.1:11211"
HOST =
[log]
; Either "console", "file", "conn" or "smtp", default is "console"
MODE = console

13
conf/gitignore/C++ Normal file
View File

@ -0,0 +1,13 @@
# Compiled Object files
*.slo
*.lo
*.o
# Compiled Dynamic libraries
*.so
*.dylib
# Compiled Static libraries
*.lai
*.la
*.a

View File

@ -0,0 +1,201 @@
The Artistic License 2.0
Copyright (c) 2014
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
This license establishes the terms under which a given free software
Package may be copied, modified, distributed, and/or redistributed.
The intent is that the Copyright Holder maintains some artistic
control over the development of that Package while still keeping the
Package available as open source and free software.
You are always permitted to make arrangements wholly outside of this
license directly with the Copyright Holder of a given Package. If the
terms of this license do not permit the full use that you propose to
make of the Package, you should contact the Copyright Holder and seek
a different licensing arrangement.
Definitions
"Copyright Holder" means the individual(s) or organization(s)
named in the copyright notice for the entire Package.
"Contributor" means any party that has contributed code or other
material to the Package, in accordance with the Copyright Holder's
procedures.
"You" and "your" means any person who would like to copy,
distribute, or modify the Package.
"Package" means the collection of files distributed by the
Copyright Holder, and derivatives of that collection and/or of
those files. A given Package may consist of either the Standard
Version, or a Modified Version.
"Distribute" means providing a copy of the Package or making it
accessible to anyone else, or in the case of a company or
organization, to others outside of your company or organization.
"Distributor Fee" means any fee that you charge for Distributing
this Package or providing support for this Package to another
party. It does not mean licensing fees.
"Standard Version" refers to the Package if it has not been
modified, or has been modified only in ways explicitly requested
by the Copyright Holder.
"Modified Version" means the Package, if it has been changed, and
such changes were not explicitly requested by the Copyright
Holder.
"Original License" means this Artistic License as Distributed with
the Standard Version of the Package, in its current version or as
it may be modified by The Perl Foundation in the future.
"Source" form means the source code, documentation source, and
configuration files for the Package.
"Compiled" form means the compiled bytecode, object code, binary,
or any other form resulting from mechanical transformation or
translation of the Source form.
Permission for Use and Modification Without Distribution
(1) You are permitted to use the Standard Version and create and use
Modified Versions for any purpose without restriction, provided that
you do not Distribute the Modified Version.
Permissions for Redistribution of the Standard Version
(2) You may Distribute verbatim copies of the Source form of the
Standard Version of this Package in any medium without restriction,
either gratis or for a Distributor Fee, provided that you duplicate
all of the original copyright notices and associated disclaimers. At
your discretion, such verbatim copies may or may not include a
Compiled form of the Package.
(3) You may apply any bug fixes, portability changes, and other
modifications made available from the Copyright Holder. The resulting
Package will still be considered the Standard Version, and as such
will be subject to the Original License.
Distribution of Modified Versions of the Package as Source
(4) You may Distribute your Modified Version as Source (either gratis
or for a Distributor Fee, and with or without a Compiled form of the
Modified Version) provided that you clearly document how it differs
from the Standard Version, including, but not limited to, documenting
any non-standard features, executables, or modules, and provided that
you do at least ONE of the following:
(a) make the Modified Version available to the Copyright Holder
of the Standard Version, under the Original License, so that the
Copyright Holder may include your modifications in the Standard
Version.
(b) ensure that installation of your Modified Version does not
prevent the user installing or running the Standard Version. In
addition, the Modified Version must bear a name that is different
from the name of the Standard Version.
(c) allow anyone who receives a copy of the Modified Version to
make the Source form of the Modified Version available to others
under
(i) the Original License or
(ii) a license that permits the licensee to freely copy,
modify and redistribute the Modified Version using the same
licensing terms that apply to the copy that the licensee
received, and requires that the Source form of the Modified
Version, and of any works derived from it, be made freely
available in that license fees are prohibited but Distributor
Fees are allowed.
Distribution of Compiled Forms of the Standard Version
or Modified Versions without the Source
(5) You may Distribute Compiled forms of the Standard Version without
the Source, provided that you include complete instructions on how to
get the Source of the Standard Version. Such instructions must be
valid at the time of your distribution. If these instructions, at any
time while you are carrying out such distribution, become invalid, you
must provide new instructions on demand or cease further distribution.
If you provide valid instructions or cease distribution within thirty
days after you become aware that the instructions are invalid, then
you do not forfeit any of your rights under this license.
(6) You may Distribute a Modified Version in Compiled form without
the Source, provided that you comply with Section 4 with respect to
the Source of the Modified Version.
Aggregating or Linking the Package
(7) You may aggregate the Package (either the Standard Version or
Modified Version) with other packages and Distribute the resulting
aggregation provided that you do not charge a licensing fee for the
Package. Distributor Fees are permitted, and licensing fees for other
components in the aggregation are permitted. The terms of this license
apply to the use and Distribution of the Standard or Modified Versions
as included in the aggregation.
(8) You are permitted to link Modified and Standard Versions with
other works, to embed the Package in a larger work of your own, or to
build stand-alone binary or bytecode versions of applications that
include the Package, and Distribute the result without restriction,
provided the result does not expose a direct interface to the Package.
Items That are Not Considered Part of a Modified Version
(9) Works (including, but not limited to, modules and scripts) that
merely extend or make use of the Package, do not, by themselves, cause
the Package to be a Modified Version. In addition, such works are not
considered parts of the Package itself, and are not subject to the
terms of this license.
General Provisions
(10) Any use, modification, and distribution of the Standard or
Modified Versions is governed by this Artistic License. By using,
modifying or distributing the Package, you accept this license. Do not
use, modify, or distribute the Package, if you do not accept this
license.
(11) If your Modified Version has been derived from a Modified
Version made by someone other than you, you are nevertheless required
to ensure that your Modified Version complies with the requirements of
this license.
(12) This license does not grant you the right to use any trademark,
service mark, tradename, or logo of the Copyright Holder.
(13) This license includes the non-exclusive, worldwide,
free-of-charge patent license to make, have made, use, offer to sell,
sell, import and otherwise transfer the Package with respect to any
patent claims licensable by the Copyright Holder that are necessarily
infringed by the Package. If you institute patent litigation
(including a cross-claim or counterclaim) against any party alleging
that the Package constitutes direct or contributory patent
infringement, then this Artistic License to you shall terminate on the
date that such litigation is filed.
(14) Disclaimer of Warranty:
THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS
IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL
LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -15,12 +15,12 @@ import (
"github.com/gogits/gogs/modules/base"
)
// +build go1.1
// +build go1.2
// Test that go1.1 tag above is included in builds. main.go refers to this definition.
const go11tag = true
// Test that go1.2 tag above is included in builds. main.go refers to this definition.
const go12tag = true
const APP_VER = "0.1.1.0320.1"
const APP_VER = "0.1.5.0321"
func init() {
base.AppVer = APP_VER

View File

@ -64,6 +64,10 @@ func CommitRepoAction(userId int64, userName string,
watches = append(watches, Watch{UserId: userId})
for i := range watches {
if userId == watches[i].UserId && i > 0 {
continue // Do not add twice in case author watches his/her repository.
}
_, err = orm.InsertOne(&Action{
UserId: watches[i].UserId,
ActUserId: userId,

19
models/issue.go Normal file
View File

@ -0,0 +1,19 @@
// 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
type Issue struct {
Id int64
RepoId int64 `xorm:"index"`
PosterId int64
}
type PullRequest struct {
Id int64
}
type Comment struct {
Id int64
}

View File

@ -7,6 +7,7 @@ package models
import (
"fmt"
"os"
"path"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
@ -16,48 +17,37 @@ import (
)
var (
orm *xorm.Engine
RepoRootPath string
orm *xorm.Engine
DbCfg struct {
Type, Host, Name, User, Pwd, Path, SslMode string
}
)
type Members struct {
Id int64
OrgId int64 `xorm:"unique(s) index"`
UserId int64 `xorm:"unique(s)"`
}
type Issue struct {
Id int64
RepoId int64 `xorm:"index"`
PosterId int64
}
type PullRequest struct {
Id int64
}
type Comment struct {
Id int64
func LoadModelsConfig() {
DbCfg.Type = base.Cfg.MustValue("database", "DB_TYPE")
DbCfg.Host = base.Cfg.MustValue("database", "HOST")
DbCfg.Name = base.Cfg.MustValue("database", "NAME")
DbCfg.User = base.Cfg.MustValue("database", "USER")
DbCfg.Pwd = base.Cfg.MustValue("database", "PASSWD")
DbCfg.SslMode = base.Cfg.MustValue("database", "SSL_MODE")
DbCfg.Path = base.Cfg.MustValue("database", "PATH", "data/gogs.db")
}
func setEngine() {
dbType := base.Cfg.MustValue("database", "DB_TYPE")
dbHost := base.Cfg.MustValue("database", "HOST")
dbName := base.Cfg.MustValue("database", "NAME")
dbUser := base.Cfg.MustValue("database", "USER")
dbPwd := base.Cfg.MustValue("database", "PASSWD")
sslMode := base.Cfg.MustValue("database", "SSL_MODE")
var err error
switch dbType {
switch DbCfg.Type {
case "mysql":
orm, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@%s/%s?charset=utf8",
dbUser, dbPwd, dbHost, dbName))
DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name))
case "postgres":
orm, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s",
dbUser, dbPwd, dbName, sslMode))
DbCfg.User, DbCfg.Pwd, DbCfg.Name, DbCfg.SslMode))
case "sqlite3":
os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
orm, err = xorm.NewEngine("sqlite3", DbCfg.Path)
default:
fmt.Printf("Unknown database type: %s\n", dbType)
fmt.Printf("Unknown database type: %s\n", DbCfg.Type)
os.Exit(2)
}
if err != nil {
@ -65,8 +55,8 @@ func setEngine() {
os.Exit(2)
}
// TODO: for serv command, MUST remove the output to os.stdout, so
// use log file to instead print to stdout
// WARNNING: for serv command, MUST remove the output to os.stdout,
// so use log file to instead print to stdout.
//x.ShowDebug = true
//orm.ShowErr = true
@ -77,20 +67,29 @@ func setEngine() {
}
orm.Logger = f
orm.ShowSQL = true
// Determine and create root git reposiroty path.
RepoRootPath = base.Cfg.MustValue("repository", "ROOT")
if err = os.MkdirAll(RepoRootPath, os.ModePerm); err != nil {
fmt.Printf("models.init(fail to create RepoRootPath(%s)): %v\n", RepoRootPath, err)
os.Exit(2)
}
}
func init() {
func NewEngine() {
setEngine()
if err := orm.Sync(new(User), new(PublicKey), new(Repository), new(Access),
new(Action), new(Watch)); err != nil {
if err := orm.Sync(new(User), new(PublicKey), new(Repository), new(Watch),
new(Action), new(Access)); err != nil {
fmt.Printf("sync database struct error: %v\n", err)
os.Exit(2)
}
}
type Statistic struct {
Counter struct {
User, PublicKey, Repo, Watch, Action, Access int64
}
}
func GetStatistic() (stats Statistic) {
stats.Counter.User, _ = orm.Count(new(User))
stats.Counter.PublicKey, _ = orm.Count(new(PublicKey))
stats.Counter.Repo, _ = orm.Count(new(Repository))
stats.Counter.Watch, _ = orm.Count(new(Watch))
stats.Counter.Action, _ = orm.Count(new(Action))
stats.Counter.Access, _ = orm.Count(new(Access))
return stats
}

View File

@ -13,6 +13,9 @@ import (
)
func init() {
LoadModelsConfig()
NewEngine()
var err error
orm, err = xorm.NewEngine("sqlite3", "./test.db")
if err != nil {

View File

@ -27,8 +27,12 @@ const (
)
var (
sshOpLocker = sync.Mutex{}
ErrKeyAlreadyExist = errors.New("Public key already exist")
)
var sshOpLocker = sync.Mutex{}
var (
sshPath string
appPath string
)
@ -79,10 +83,6 @@ type PublicKey struct {
Updated time.Time `xorm:"updated"`
}
var (
ErrKeyAlreadyExist = errors.New("Public key already exist")
)
// GenAuthorizedKey returns formatted public key string.
func GenAuthorizedKey(keyId int64, key string) string {
return fmt.Sprintf(TPL_PUBLICK_KEY+"\n", appPath, keyId, key)

View File

@ -12,6 +12,7 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
@ -26,68 +27,26 @@ import (
"github.com/gogits/gogs/modules/log"
)
// Repository represents a git repository.
type Repository struct {
Id int64
OwnerId int64 `xorm:"unique(s)"`
ForkId int64
LowerName string `xorm:"unique(s) index not null"`
Name string `xorm:"index not null"`
Description string
Website string
Private bool
NumWatchs int
NumStars int
NumForks int
Created time.Time `xorm:"created"`
Updated time.Time `xorm:"updated"`
}
var (
ErrRepoAlreadyExist = errors.New("Repository already exist")
ErrRepoNotExist = errors.New("Repository does not exist")
ErrRepoFileNotExist = errors.New("Target Repo file does not exist")
ErrRepoNameIllegal = errors.New("Repository name contains illegal characters")
ErrRepoFileNotLoaded = fmt.Errorf("repo file not loaded")
)
// Watch is connection request for receiving repository notifycation.
type Watch struct {
Id int64
RepoId int64 `xorm:"UNIQUE(watch)"`
UserId int64 `xorm:"UNIQUE(watch)"`
}
// Watch or unwatch repository.
func WatchRepo(userId, repoId int64, watch bool) (err error) {
if watch {
_, err = orm.Insert(&Watch{RepoId: repoId, UserId: userId})
} else {
_, err = orm.Delete(&Watch{0, repoId, userId})
}
return err
}
// GetWatches returns all watches of given repository.
func GetWatches(repoId int64) ([]Watch, error) {
watches := make([]Watch, 0, 10)
err := orm.Find(&watches, &Watch{RepoId: repoId})
return watches, err
}
// IsWatching checks if user has watched given repository.
func IsWatching(userId, repoId int64) bool {
has, _ := orm.Get(&Watch{0, repoId, userId})
return has
}
var gitInitLocker = sync.Mutex{}
var (
gitInitLocker = sync.Mutex{}
LanguageIgns, Licenses []string
)
var (
ErrRepoAlreadyExist = errors.New("Repository already exist")
ErrRepoNotExist = errors.New("Repository does not exist")
ErrRepoFileNotExist = errors.New("Target Repo file does not exist")
)
func init() {
func LoadRepoConfig() {
LanguageIgns = strings.Split(base.Cfg.MustValue("repository", "LANG_IGNS"), "|")
Licenses = strings.Split(base.Cfg.MustValue("repository", "LICENSES"), "|")
}
func NewRepoContext() {
zip.Verbose = false
// Check if server has basic git setting.
@ -104,6 +63,32 @@ func init() {
os.Exit(2)
}
}
// Initialize illegal patterns.
for i := range illegalPatterns[1:] {
pattern := ""
for j := range illegalPatterns[i+1] {
pattern += "[" + string(illegalPatterns[i+1][j]-32) + string(illegalPatterns[i+1][j]) + "]"
}
illegalPatterns[i+1] = pattern
}
}
// Repository represents a git repository.
type Repository struct {
Id int64
OwnerId int64 `xorm:"unique(s)"`
ForkId int64
LowerName string `xorm:"unique(s) index not null"`
Name string `xorm:"index not null"`
Description string
Website string
Private bool
NumWatches int
NumStars int
NumForks int
Created time.Time `xorm:"created"`
Updated time.Time `xorm:"updated"`
}
// IsRepositoryExist returns true if the repository with given name under user has already existed.
@ -120,8 +105,28 @@ func IsRepositoryExist(user *User, repoName string) (bool, error) {
return s.IsDir(), nil
}
var (
// Define as all lower case!!
illegalPatterns = []string{"[.][Gg][Ii][Tt]", "user", "help", "stars", "issues", "pulls", "commits", "admin", "repo", "template", "admin"}
)
// IsLegalName returns false if name contains illegal characters.
func IsLegalName(repoName string) bool {
for _, pattern := range illegalPatterns {
has, _ := regexp.MatchString(pattern, repoName)
if has {
return false
}
}
return true
}
// CreateRepository creates a repository for given user or orgnaziation.
func CreateRepository(user *User, repoName, desc, repoLang, license string, private bool, initReadme bool) (*Repository, error) {
if !IsLegalName(repoName) {
return nil, ErrRepoNameIllegal
}
isExist, err := IsRepositoryExist(user, repoName)
if err != nil {
return nil, err
@ -331,6 +336,82 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
return nil
}
// UserRepo reporesents a repository with user name.
type UserRepo struct {
*Repository
UserName string
}
// GetRepos returns given number of repository objects with offset.
func GetRepos(num, offset int) ([]UserRepo, error) {
repos := make([]Repository, 0, num)
if err := orm.Limit(num, offset).Asc("id").Find(&repos); err != nil {
return nil, err
}
urepos := make([]UserRepo, len(repos))
for i := range repos {
urepos[i].Repository = &repos[i]
u := new(User)
has, err := orm.Id(urepos[i].Repository.OwnerId).Get(u)
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist
}
urepos[i].UserName = u.Name
}
return urepos, nil
}
func RepoPath(userName, repoName string) string {
return filepath.Join(UserPath(userName), repoName+".git")
}
// DeleteRepository deletes a repository for a user or orgnaztion.
func DeleteRepository(userId, repoId int64, userName string) (err error) {
repo := &Repository{Id: repoId, OwnerId: userId}
has, err := orm.Get(repo)
if err != nil {
return err
} else if !has {
return ErrRepoNotExist
}
session := orm.NewSession()
if err = session.Begin(); err != nil {
return err
}
if _, err = session.Delete(&Repository{Id: repoId}); err != nil {
session.Rollback()
return err
}
if _, err := session.Delete(&Access{UserName: userName, RepoName: repo.Name}); err != nil {
session.Rollback()
return err
}
rawSql := "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?"
if _, err = session.Exec(rawSql, userId); err != nil {
session.Rollback()
return err
}
if _, err = session.Delete(&Watch{RepoId: repoId}); err != nil {
session.Rollback()
return err
}
if err = session.Commit(); err != nil {
session.Rollback()
return err
}
if err = os.RemoveAll(RepoPath(userName, repo.Name)); err != nil {
// TODO: log and delete manully
log.Error("delete repo %s/%s failed: %v", userName, repo.Name, err)
return err
}
return nil
}
// GetRepositoryByName returns the repository by given name under user if exists.
func GetRepositoryByName(user *User, repoName string) (*Repository, error) {
repo := &Repository{
@ -368,6 +449,45 @@ func GetRepositoryCount(user *User) (int64, error) {
return orm.Count(&Repository{OwnerId: user.Id})
}
// Watch is connection request for receiving repository notifycation.
type Watch struct {
Id int64
RepoId int64 `xorm:"UNIQUE(watch)"`
UserId int64 `xorm:"UNIQUE(watch)"`
}
// Watch or unwatch repository.
func WatchRepo(userId, repoId int64, watch bool) (err error) {
if watch {
if _, err = orm.Insert(&Watch{RepoId: repoId, UserId: userId}); err != nil {
return err
}
rawSql := "UPDATE `repository` SET num_watches = num_watches + 1 WHERE id = ?"
_, err = orm.Exec(rawSql, repoId)
} else {
if _, err = orm.Delete(&Watch{0, repoId, userId}); err != nil {
return err
}
rawSql := "UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?"
_, err = orm.Exec(rawSql, repoId)
}
return err
}
// GetWatches returns all watches of given repository.
func GetWatches(repoId int64) ([]Watch, error) {
watches := make([]Watch, 0, 10)
err := orm.Find(&watches, &Watch{RepoId: repoId})
return watches, err
}
// IsWatching checks if user has watched given repository.
func IsWatching(userId, repoId int64) bool {
has, _ := orm.Get(&Watch{0, repoId, userId})
return has
}
func StarReposiory(user *User, repoName string) error {
return nil
}
@ -388,56 +508,6 @@ func ForkRepository(reposName string, userId int64) {
}
func RepoPath(userName, repoName string) string {
return filepath.Join(UserPath(userName), repoName+".git")
}
// DeleteRepository deletes a repository for a user or orgnaztion.
func DeleteRepository(userId, repoId int64, userName string) (err error) {
repo := &Repository{Id: repoId, OwnerId: userId}
has, err := orm.Get(repo)
if err != nil {
return err
} else if !has {
return ErrRepoNotExist
}
session := orm.NewSession()
if err = session.Begin(); err != nil {
return err
}
if _, err = session.Delete(&Repository{Id: repoId}); err != nil {
session.Rollback()
return err
}
if _, err := session.Delete(&Access{UserName: userName, RepoName: repo.Name}); err != nil {
session.Rollback()
return err
}
rawSql := "UPDATE user SET num_repos = num_repos - 1 WHERE id = ?"
if base.Cfg.MustValue("database", "DB_TYPE") == "postgres" {
rawSql = "UPDATE \"user\" SET num_repos = num_repos - 1 WHERE id = ?"
}
if _, err = session.Exec(rawSql, userId); err != nil {
session.Rollback()
return err
}
if err = session.Commit(); err != nil {
session.Rollback()
return err
}
if err = os.RemoveAll(RepoPath(userName, repo.Name)); err != nil {
// TODO: log and delete manully
log.Error("delete repo %s/%s failed: %v", userName, repo.Name, err)
return err
}
return nil
}
var (
ErrRepoFileNotLoaded = fmt.Errorf("repo file not loaded")
)
// RepoFile represents a file object in git repository.
type RepoFile struct {
*git.TreeEntry

View File

@ -33,6 +33,14 @@ const (
LT_LDAP
)
var (
ErrUserOwnRepos = errors.New("User still have ownership of repositories")
ErrUserAlreadyExist = errors.New("User already exist")
ErrUserNotExist = errors.New("User does not exist")
ErrEmailAlreadyUsed = errors.New("E-mail already used")
ErrUserNameIllegal = errors.New("User name contains illegal characters")
)
// User represents the object of individual and member of organization.
type User struct {
Id int64
@ -51,6 +59,7 @@ type User struct {
Location string
Website string
IsActive bool
IsAdmin bool
Rands string `xorm:"VARCHAR(10)"`
Created time.Time `xorm:"created"`
Updated time.Time `xorm:"updated"`
@ -66,19 +75,28 @@ func (user *User) AvatarLink() string {
return "http://1.gravatar.com/avatar/" + user.Avatar
}
type Follow struct {
Id int64
UserId int64 `xorm:"unique(s)"`
FollowId int64 `xorm:"unique(s)"`
Created time.Time `xorm:"created"`
// NewGitSig generates and returns the signature of given user.
func (user *User) NewGitSig() *git.Signature {
return &git.Signature{
Name: user.Name,
Email: user.Email,
When: time.Now(),
}
}
var (
ErrUserOwnRepos = errors.New("User still have ownership of repositories")
ErrUserAlreadyExist = errors.New("User already exist")
ErrUserNotExist = errors.New("User does not exist")
ErrEmailAlreadyUsed = errors.New("E-mail already used")
)
// EncodePasswd encodes password to safe format.
func (user *User) EncodePasswd() error {
newPasswd, err := scrypt.Key([]byte(user.Passwd), []byte(base.SecretKey), 16384, 8, 1, 64)
user.Passwd = fmt.Sprintf("%x", newPasswd)
return err
}
// Member represents user is member of organization.
type Member struct {
Id int64
OrgId int64 `xorm:"unique(member) index"`
UserId int64 `xorm:"unique(member)"`
}
// IsUserExist checks if given user name exist,
// the user name should be noncased unique.
@ -91,15 +109,6 @@ func IsEmailUsed(email string) (bool, error) {
return orm.Get(&User{Email: email})
}
// NewGitSig generates and returns the signature of given user.
func (user *User) NewGitSig() *git.Signature {
return &git.Signature{
Name: user.Name,
Email: user.Email,
When: time.Now(),
}
}
// return a user salt token
func GetUserSalt() string {
return base.GetRandomString(10)
@ -107,6 +116,10 @@ func GetUserSalt() string {
// RegisterUser creates record of a new user.
func RegisterUser(user *User) (*User, error) {
if !IsLegalName(user.Name) {
return nil, ErrUserNameIllegal
}
isExist, err := IsUserExist(user.Name)
if err != nil {
return nil, err
@ -136,7 +149,20 @@ func RegisterUser(user *User) (*User, error) {
}
return nil, err
}
return user, nil
if user.Id == 1 {
user.IsAdmin = true
user.IsActive = true
_, err = orm.Id(user.Id).UseBool().Update(user)
}
return user, err
}
// GetUsers returns given number of user objects with offset.
func GetUsers(num, offset int) ([]User, error) {
users := make([]User, 0, num)
err := orm.Limit(num, offset).Asc("id").Find(&users)
return users, err
}
// get user by erify code
@ -217,22 +243,14 @@ func DeleteUser(user *User) error {
return err
}
// EncodePasswd encodes password to safe format.
func (user *User) EncodePasswd() error {
newPasswd, err := scrypt.Key([]byte(user.Passwd), []byte(base.SecretKey), 16384, 8, 1, 64)
user.Passwd = fmt.Sprintf("%x", newPasswd)
return err
}
// UserPath returns the path absolute path of user repositories.
func UserPath(userName string) string {
return filepath.Join(RepoRootPath, strings.ToLower(userName))
return filepath.Join(base.RepoRootPath, strings.ToLower(userName))
}
func GetUserByKeyId(keyId int64) (*User, error) {
user := new(User)
rawSql := "SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?"
has, err := orm.Sql(rawSql, keyId).Get(user)
if err != nil {
return nil, err
@ -289,6 +307,13 @@ func LoginUserPlain(name, passwd string) (*User, error) {
return &user, err
}
// Follow is connection request for receiving user notifycation.
type Follow struct {
Id int64
UserId int64 `xorm:"unique(follow)"`
FollowId int64 `xorm:"unique(follow)"`
}
// FollowUser marks someone be another's follower.
func FollowUser(userId int64, followId int64) (err error) {
session := orm.NewSession()
@ -300,19 +325,13 @@ func FollowUser(userId int64, followId int64) (err error) {
return err
}
rawSql := "UPDATE user SET num_followers = num_followers + 1 WHERE id = ?"
if base.Cfg.MustValue("database", "DB_TYPE") == "postgres" {
rawSql = "UPDATE \"user\" SET num_followers = num_followers + 1 WHERE id = ?"
}
rawSql := "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?"
if _, err = session.Exec(rawSql, followId); err != nil {
session.Rollback()
return err
}
rawSql = "UPDATE user SET num_followings = num_followings + 1 WHERE id = ?"
if base.Cfg.MustValue("database", "DB_TYPE") == "postgres" {
rawSql = "UPDATE \"user\" SET num_followings = num_followings + 1 WHERE id = ?"
}
rawSql = "UPDATE `user` SET num_followings = num_followings + 1 WHERE id = ?"
if _, err = session.Exec(rawSql, userId); err != nil {
session.Rollback()
return err
@ -331,19 +350,13 @@ func UnFollowUser(userId int64, unFollowId int64) (err error) {
return err
}
rawSql := "UPDATE user SET num_followers = num_followers - 1 WHERE id = ?"
if base.Cfg.MustValue("database", "DB_TYPE") == "postgres" {
rawSql = "UPDATE \"user\" SET num_followers = num_followers - 1 WHERE id = ?"
}
rawSql := "UPDATE `user` SET num_followers = num_followers - 1 WHERE id = ?"
if _, err = session.Exec(rawSql, unFollowId); err != nil {
session.Rollback()
return err
}
rawSql = "UPDATE user SET num_followings = num_followings - 1 WHERE id = ?"
if base.Cfg.MustValue("database", "DB_TYPE") == "postgres" {
rawSql = "UPDATE \"user\" SET num_followings = num_followings - 1 WHERE id = ?"
}
rawSql = "UPDATE `user` SET num_followings = num_followings - 1 WHERE id = ?"
if _, err = session.Exec(rawSql, userId); err != nil {
session.Rollback()
return err

55
modules/auth/admin.go Normal file
View File

@ -0,0 +1,55 @@
// 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/codegangsta/martini"
"github.com/gogits/binding"
"github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log"
)
type AdminEditUserForm struct {
Email string `form:"email" binding:"Required;Email;MaxSize(50)"`
Website string `form:"website" binding:"MaxSize(50)"`
Location string `form:"location" binding:"MaxSize(50)"`
Avatar string `form:"avatar" binding:"Required;Email;MaxSize(50)"`
Active string `form:"active"`
Admin string `form:"admin"`
}
func (f *AdminEditUserForm) Name(field string) string {
names := map[string]string{
"Email": "E-mail address",
"Website": "Website",
"Location": "Location",
"Avatar": "Gravatar Email",
}
return names[field]
}
func (f *AdminEditUserForm) Validate(errors *binding.Errors, 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("AdminEditUserForm.Validate: %v", err)
}
return
}
validate(errors, data, f)
}

View File

@ -79,7 +79,7 @@ type UpdateProfileForm struct {
func (f *UpdateProfileForm) Name(field string) string {
names := map[string]string{
"Email": "Email address",
"Email": "E-mail address",
"Website": "Website",
"Location": "Location",
"Avatar": "Gravatar Email",

View File

@ -15,6 +15,8 @@ import (
"github.com/Unknwon/com"
"github.com/Unknwon/goconfig"
"github.com/gogits/cache"
"github.com/gogits/gogs/modules/log"
)
@ -26,20 +28,32 @@ type Mailer struct {
}
var (
AppVer string
AppName string
AppLogo string
AppUrl string
Domain string
SecretKey string
AppVer string
AppName string
AppLogo string
AppUrl string
Domain string
SecretKey string
RunUser string
RepoRootPath string
Cfg *goconfig.ConfigFile
MailService *Mailer
Cache cache.Cache
CacheAdapter string
CacheConfig string
LogMode string
LogConfig string
)
var Service struct {
RegisterEmailConfirm bool
ActiveCodeLives int
ResetPwdCodeLives int
RegisterEmailConfirm bool
DisenableRegisteration bool
RequireSignInView bool
ActiveCodeLives int
ResetPwdCodeLives int
}
func exeDir() (string, error) {
@ -66,19 +80,21 @@ var logLevels = map[string]string{
func newService() {
Service.ActiveCodeLives = Cfg.MustInt("service", "ACTIVE_CODE_LIVE_MINUTES", 180)
Service.ResetPwdCodeLives = Cfg.MustInt("service", "RESET_PASSWD_CODE_LIVE_MINUTES", 180)
Service.DisenableRegisteration = Cfg.MustBool("service", "DISENABLE_REGISTERATION", false)
Service.RequireSignInView = Cfg.MustBool("service", "REQUIRE_SIGNIN_VIEW", false)
}
func newLogService() {
// Get and check log mode.
mode := Cfg.MustValue("log", "MODE", "console")
modeSec := "log." + mode
LogMode = Cfg.MustValue("log", "MODE", "console")
modeSec := "log." + LogMode
if _, err := Cfg.GetSection(modeSec); err != nil {
fmt.Printf("Unknown log mode: %s\n", mode)
fmt.Printf("Unknown log mode: %s\n", LogMode)
os.Exit(2)
}
// Log level.
levelName := Cfg.MustValue("log."+mode, "LEVEL", "Trace")
levelName := Cfg.MustValue("log."+LogMode, "LEVEL", "Trace")
level, ok := logLevels[levelName]
if !ok {
fmt.Printf("Unknown log level: %s\n", levelName)
@ -86,14 +102,13 @@ func newLogService() {
}
// Generate log configuration.
var config string
switch mode {
switch LogMode {
case "console":
config = fmt.Sprintf(`{"level":%s}`, level)
LogConfig = fmt.Sprintf(`{"level":%s}`, level)
case "file":
logPath := Cfg.MustValue(modeSec, "FILE_NAME", "log/gogs.log")
os.MkdirAll(path.Dir(logPath), os.ModePerm)
config = fmt.Sprintf(
LogConfig = fmt.Sprintf(
`{"level":%s,"filename":%s,"rotate":%v,"maxlines":%d,"maxsize",%d,"daily":%v,"maxdays":%d}`, level,
logPath,
Cfg.MustBool(modeSec, "LOG_ROTATE", true),
@ -102,13 +117,13 @@ func newLogService() {
Cfg.MustBool(modeSec, "DAILY_ROTATE", true),
Cfg.MustInt(modeSec, "MAX_DAYS", 7))
case "conn":
config = fmt.Sprintf(`{"level":%s,"reconnectOnMsg":%v,"reconnect":%v,"net":%s,"addr":%s}`, level,
LogConfig = fmt.Sprintf(`{"level":%s,"reconnectOnMsg":%v,"reconnect":%v,"net":%s,"addr":%s}`, level,
Cfg.MustBool(modeSec, "RECONNECT_ON_MSG", false),
Cfg.MustBool(modeSec, "RECONNECT", false),
Cfg.MustValue(modeSec, "PROTOCOL", "tcp"),
Cfg.MustValue(modeSec, "ADDR", ":7020"))
case "smtp":
config = fmt.Sprintf(`{"level":%s,"username":%s,"password":%s,"host":%s,"sendTos":%s,"subject":%s}`, level,
LogConfig = fmt.Sprintf(`{"level":%s,"username":%s,"password":%s,"host":%s,"sendTos":%s,"subject":%s}`, level,
Cfg.MustValue(modeSec, "USER", "example@example.com"),
Cfg.MustValue(modeSec, "PASSWD", "******"),
Cfg.MustValue(modeSec, "HOST", "127.0.0.1:25"),
@ -116,8 +131,32 @@ func newLogService() {
Cfg.MustValue(modeSec, "SUBJECT", "Diagnostic message from serve"))
}
log.NewLogger(Cfg.MustInt64("log", "BUFFER_LEN", 10000), mode, config)
log.Info("Log Mode: %s(%s)", strings.Title(mode), levelName)
log.NewLogger(Cfg.MustInt64("log", "BUFFER_LEN", 10000), LogMode, LogConfig)
log.Info("Log Mode: %s(%s)", strings.Title(LogMode), levelName)
}
func newCacheService() {
CacheAdapter = Cfg.MustValue("cache", "ADAPTER", "memory")
switch CacheAdapter {
case "memory":
CacheConfig = fmt.Sprintf(`{"interval":%d}`, Cfg.MustInt("cache", "INTERVAL", 60))
case "redis", "memcache":
CacheConfig = fmt.Sprintf(`{"conn":"%s"}`, Cfg.MustValue("cache", "HOST"))
default:
fmt.Printf("Unknown cache adapter: %s\n", CacheAdapter)
os.Exit(2)
}
var err error
Cache, err = cache.NewCache(CacheAdapter, CacheConfig)
if err != nil {
fmt.Printf("Init cache system failed, adapter: %s, config: %s, %v\n",
CacheAdapter, CacheConfig, err)
os.Exit(2)
}
log.Info("Cache Service Enabled")
}
func newMailService() {
@ -144,7 +183,7 @@ func newRegisterMailService() {
log.Info("Register Mail Service Enabled")
}
func init() {
func NewConfigContext() {
var err error
workDir, err := exeDir()
if err != nil {
@ -173,11 +212,20 @@ func init() {
AppUrl = Cfg.MustValue("server", "ROOT_URL")
Domain = Cfg.MustValue("server", "DOMAIN")
SecretKey = Cfg.MustValue("security", "SECRET_KEY")
RunUser = Cfg.MustValue("", "RUN_USER")
// Determine and create root git reposiroty path.
RepoRootPath = Cfg.MustValue("repository", "ROOT")
if err = os.MkdirAll(RepoRootPath, os.ModePerm); err != nil {
fmt.Printf("models.init(fail to create RepoRootPath(%s)): %v\n", RepoRootPath, err)
os.Exit(2)
}
}
func NewServices() {
newService()
newLogService()
newCacheService()
newMailService()
newRegisterMailService()
}

View File

@ -36,7 +36,7 @@ func isLink(link []byte) bool {
func IsMarkdownFile(name string) bool {
name = strings.ToLower(name)
switch filepath.Ext(name) {
case "md", "markdown":
case ".md", ".markdown", ".mdown":
return true
}
return false
@ -61,7 +61,7 @@ type CustomRender struct {
func (options *CustomRender) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
if len(link) > 0 && !isLink(link) {
if link[0] == '#' {
link = append([]byte(options.urlPrefix), link...)
// link = append([]byte(options.urlPrefix), link...)
} else {
link = []byte(path.Join(options.urlPrefix, string(link)))
}

View File

@ -33,6 +33,10 @@ func List(l *list.List) chan interface{} {
return c
}
var mailDomains = map[string]string{
"gmail.com": "gmail.com",
}
var TemplateFuncs template.FuncMap = map[string]interface{}{
"AppName": func() string {
return AppName
@ -56,7 +60,12 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{
"DateFormat": DateFormat,
"List": List,
"Mail2Domain": func(mail string) string {
return "mail." + strings.Split(mail, "@")[1]
suffix := strings.SplitN(mail, "@", 2)[1]
domain, ok := mailDomains[suffix]
if !ok {
return "mail." + suffix
}
return domain
},
"SubStr": func(str string, start, length int) string {
return str[start : start+length]

View File

@ -40,7 +40,7 @@ func (m Message) Content() string {
var mailQueue chan *Message
func init() {
func NewMailerContext() {
mailQueue = make(chan *Message, base.Cfg.MustInt("mailer", "SEND_BUFFER_LEN", 10))
go processMailQueue()
}

View File

@ -15,12 +15,12 @@ func SignInRequire(redirect bool) martini.Handler {
return func(ctx *Context) {
if !ctx.IsSigned {
if redirect {
ctx.Redirect("/")
ctx.Redirect("/user/login")
}
return
} else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm {
ctx.Data["Title"] = "Activate Your Account"
ctx.Render.HTML(200, "user/active", ctx.Data)
ctx.HTML(200, "user/active")
return
}
}
@ -31,6 +31,18 @@ func SignOutRequire() martini.Handler {
return func(ctx *Context) {
if ctx.IsSigned {
ctx.Redirect("/")
return
}
}
}
// AdminRequire requires user signed in as administor.
func AdminRequire() martini.Handler {
return func(ctx *Context) {
if !ctx.User.IsAdmin {
ctx.Error(403)
return
}
ctx.Data["PageIsAdmin"] = true
}
}

View File

@ -12,8 +12,11 @@ import (
"github.com/codegangsta/martini"
"github.com/martini-contrib/sessions"
"github.com/gogits/cache"
"github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/auth"
"github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log"
)
@ -25,6 +28,7 @@ type Context struct {
Req *http.Request
Res http.ResponseWriter
Session sessions.Session
Cache cache.Cache
User *models.User
IsSigned bool
@ -61,24 +65,29 @@ func (ctx *Context) HasError() bool {
return hasErr.(bool)
}
// HTML calls render.HTML underlying but reduce one argument.
func (ctx *Context) HTML(status int, name string, htmlOpt ...HTMLOptions) {
ctx.Render.HTML(status, name, ctx.Data, htmlOpt...)
}
// RenderWithErr used for page has form validation but need to prompt error to users.
func (ctx *Context) RenderWithErr(msg, tpl string, form auth.Form) {
ctx.Data["HasError"] = true
ctx.Data["ErrorMsg"] = msg
auth.AssignForm(form, ctx.Data)
ctx.HTML(200, tpl, ctx.Data)
ctx.HTML(200, tpl)
}
// Handle handles and logs error by given status.
func (ctx *Context) Handle(status int, title string, err error) {
log.Error("%s: %v", title, err)
if martini.Dev == martini.Prod {
ctx.HTML(500, "status/500", ctx.Data)
ctx.HTML(500, "status/500")
return
}
ctx.Data["ErrorMsg"] = err
ctx.HTML(status, fmt.Sprintf("status/%d", status), ctx.Data)
ctx.HTML(status, fmt.Sprintf("status/%d", status))
}
// InitContext initializes a classic context for a request.
@ -92,6 +101,7 @@ func InitContext() martini.Handler {
Req: r,
Res: res,
Session: session,
Cache: base.Cache,
Render: rd,
}
@ -106,6 +116,7 @@ func InitContext() martini.Handler {
ctx.Data["SignedUser"] = user
ctx.Data["SignedUserId"] = user.Id
ctx.Data["SignedUserName"] = user.LowerName
ctx.Data["IsAdmin"] = ctx.User.IsAdmin
}
ctx.Data["PageStartTime"] = time.Now()

View File

@ -70,6 +70,7 @@ func RepoAssignment(redirect bool) martini.Handler {
}
ctx.Repo.Repository = repo
ctx.Repo.CloneLink.SSH = fmt.Sprintf("git@%s:%s/%s.git", base.Domain, user.LowerName, repo.LowerName)
ctx.Repo.CloneLink.HTTPS = fmt.Sprintf("https://%s/%s/%s.git", base.Domain, user.LowerName, repo.LowerName)
ctx.Data["IsRepositoryValid"] = true
ctx.Data["Repository"] = repo
@ -78,5 +79,6 @@ func RepoAssignment(redirect bool) martini.Handler {
ctx.Data["CloneLink"] = ctx.Repo.CloneLink
ctx.Data["RepositoryLink"] = ctx.Data["Title"]
ctx.Data["IsRepositoryOwner"] = ctx.Repo.IsOwner
ctx.Data["IsRepositoryWatching"] = ctx.Repo.IsWatching
}
}

View File

@ -145,8 +145,9 @@ html, body {
float: right;
}
#gogs-nav-signin {
#gogs-nav-signin, #gogs-nav-signup {
float: right;
margin-left: 1em;
}
#gogs-nav-out .fa {
@ -324,6 +325,27 @@ html, body {
color: #444;
}
.gogs-admin-nav {
background-color: #FFF;
padding-top: 10px;
padding-left: 0;
padding-right: 0;
border: 1px solid #D8D8D8;
}
.gogs-admin-nav li {
margin-bottom: 8px;
border-left: 4px solid transparent;
}
.gogs-admin-nav li:hover {
border-left-color: #EEE;
}
.gogs-admin-nav li.active:hover {
border-left: 4px solid #DD4B39;
}
/* gogits user ssh keys */
#gogs-ssh-keys .list-group-item {
@ -376,7 +398,7 @@ html, body {
}
#gogs-feed-right .repo-panel .list-group-item a {
display: inline-block;
display: block;
margin-left: 0;
background-color: transparent;
padding-left: 0;
@ -406,7 +428,6 @@ html, body {
#gogs-feed-right .repo-panel span.stars {
color: #666;
line-height: 44px;
margin-right: 1em;
}
@ -445,7 +466,7 @@ html, body {
padding: 0;
}
#gogs-repo-watching .dropdown-menu .dropdown-item:hover .dropdown-header {
#gogs-repo-watching .dropdown-menu .dropdown-item:hover .dropdown-header, #gogs-repo-watching .dropdown-item .dropdown-header.text-primary {
color: rgb(65, 131, 196);
cursor: pointer;
}
@ -501,8 +522,7 @@ html, body {
}
.activity-list .info {
float: left;
padding: 0 0 0 10px;
margin: 0 0 0 40px;
line-height: 1.7em;
}
@ -555,6 +575,15 @@ html, body {
min-width: 200px;
}
#gogs-repo-clone .dropdown-menu{
width: 400px;
padding: 20px;
}
#gogs-repo-clone .input-group{
margin-bottom: 15px;
}
/* #gogs-source */
#gogs-source {
margin-top: -20px;
@ -645,6 +674,62 @@ html, body {
.file-content .file-body {
padding: 30px 30px 50px;
border: none;
background-color: #FFF;
overflow: auto;
overflow-x: auto;
overflow-y: hidden;
}
.file-content .file-body.file-code pre {
background-color: #FFF;
border: none;
}
.file-content .file-body.file-code {
padding: 0;
}
.file-content .file-body.file-code .lines-code > pre {
border: none;
background: none;
border-left: 1px solid #ddd;
}
.file-content .file-body.file-code .lines-code ol.linenums > .active {
background: #ffffdd;
}
.file-content .file-body.file-code .lines-num {
text-align: right;
color: #999;
background: #fafafa;
width: 1%;
}
.file-content .file-body.file-code .lines-num span {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
line-height: 1.6;
padding: 0 8px 0 10px;
cursor: pointer;
display: block;
margin-top: 2px;
font-size: 90%;
}
.file-content .file-body.file-code .lines-num span:first-child {
margin-top: 0;
}
.file-content .file-body.file-code > table {
width: 100%;
}
.file-content .file-body.file-code > table > tbody > tr,
.file-content .file-body.file-code > table > tbody > tr > td,
.file-content .file-body.file-code > table {
border: none;
background: none;
}
.branch-list th, .commit-list th {
@ -724,6 +809,28 @@ html, body {
background-color: #FFF;
}
.commit-list .date {
width: 120px;
}
.commit-list .author {
min-width: 180px;
}
.guide-box pre, .guide-box .input-group {
margin-top: 20px;
margin-bottom: 30px;
line-height: 24px;
}
.guide-box input[readonly] {
background-color: #FFF;
}
.guide-box {
margin-top: 20px;
}
/* wrapper and footer */
#wrapper {

View File

@ -15,7 +15,8 @@
line-height: 1.7;
padding: 15px 0 0;
margin: 0 0 15px;
color: #666;
color: #444;
font-weight: bold;
}
.markdown h1,
@ -86,6 +87,10 @@
margin-top: 6px;
}
.markdown li:first-child {
margin-top: 0;
}
.markdown dl dt {
font-style: italic;
margin-top: 9px;
@ -106,8 +111,8 @@
line-height: 1.6;
overflow: auto;
background: #f8f8f8;
padding: 6px 10px;
border: 1px solid #ddd;
padding: 0;
}
.markdown > pre.linenums {
@ -115,6 +120,17 @@
}
.markdown > pre > ol.linenums {
list-style: none;
padding: 0;
}
.markdown > pre > ol.linenums > li {
margin-top: 2px;
}
.markdown > pre.nums-style > ol.linenums {
list-style-type: decimal;
padding: 0 0 0 40px;
-webkit-box-shadow: inset 40px 0 0 #f5f5f5, inset 41px 0 0 #ccc;
box-shadow: inset 40px 0 0 #f5f5f5, inset 41px 0 0 #ccc;
}
@ -130,14 +146,14 @@
}
.markdown > pre > ol.linenums > li:first-child {
padding-top: 6px;
padding-top: 12px;
}
.markdown > pre > ol.linenums > li:last-child {
padding-bottom: 6px;
padding-bottom: 12px;
}
.markdown > pre > ol.linenums > li {
.markdown > pre.nums-style > ol.linenums > li {
border-left: 1px solid #ddd;
}
@ -163,6 +179,54 @@
color: #fff;
}
.markdown .anchor-wrap {
/*margin-top: -50px;*/
/*padding-top: 50px;*/
}
.markdown h1 a, .markdown h2 a, .markdown h3 a {
text-decoration: none;
}
.markdown h1 a.anchor,
.markdown h2 a.anchor,
.markdown h3 a.anchor,
.markdown h4 a.anchor,
.markdown h5 a.anchor,
.markdown h6 a.anchor {
text-decoration:none;
line-height:1;
padding-left:0;
margin-left:5px;
top:15%;
}
.markdown a span.octicon {
font-size: 16px;
font-family: "FontAwesome";
line-height: 1;
display: inline-block;
text-decoration: none;
-webkit-font-smoothing: antialiased;
}
.markdown a span.octicon-link {
display: none;
color: #000;
}
.markdown a span.octicon-link:before {
content: "\f0c1";
}
.markdown h1:hover .octicon-link,
.markdown h2:hover .octicon-link,
.markdown h3:hover .octicon-link,
.markdown h4:hover .octicon-link,
.markdown h5:hover .octicon-link,
.markdown h6:hover .octicon-link {
display:inline-block
}
/* Author: jmblog */
/* Project: https://github.com/jmblog/color-themes-for-google-code-prettify */
/* GitHub Theme */
@ -187,6 +251,7 @@
/* a comment */
.com {
color: #999988;
font-style: italic;
}
/* a type name */

View File

@ -41,15 +41,15 @@ var Gogits = {
});
};
Gogits.initPopovers = function () {
var hideAllPopovers = function() {
$('[data-toggle=popover]').each(function() {
var hideAllPopovers = function () {
$('[data-toggle=popover]').each(function () {
$(this).popover('hide');
});
});
};
$(document).on('click', function(e) {
$(document).on('click', function (e) {
var $e = $(e.target);
if($e.data('toggle') == 'popover'||$e.parents("[data-toggle=popover], .popover").length > 0){
if ($e.data('toggle') == 'popover' || $e.parents("[data-toggle=popover], .popover").length > 0) {
return;
}
hideAllPopovers();
@ -63,12 +63,56 @@ var Gogits = {
var $tabs = $('[data-init=tabs]');
$tabs.find("li:eq(0) a").tab("show");
};
// fix dropdown inside click
Gogits.initDropDown = function(){
$('.dropdown-menu').on('click','a,button,input,select',function(e){
e.stopPropagation();
});
};
// render markdown
Gogits.renderMarkdown = function () {
var $pre = $('.markdown').find('pre > code').parent();
$pre.addClass("prettyprint");
var $md = $('.markdown');
var $pre = $md.find('pre > code').parent();
$pre.addClass('prettyprint linenums');
prettyPrint();
var $lineNums = $pre.parent().siblings('.lines-num');
if ($lineNums.length > 0) {
var nums = $pre.find('ol.linenums > li').length;
for (var i = 1; i <= nums; i++) {
$lineNums.append('<span id="L' + i + '" rel=".L' + i + '">' + i + '</span>');
}
var last;
$(document).on('click', '.lines-num span', function () {
var $e = $(this);
if (last) {
last.removeClass('active');
}
last = $e.parent().siblings('.lines-code').find('ol.linenums > ' + $e.attr('rel'));
last.addClass('active');
window.location.href = '#' + $e.attr('id');
});
}
// Set anchor.
var headers = {};
$md.find('h1, h2, h3, h4, h5, h6').each(function () {
var node = $(this);
var val = encodeURIComponent(node.text().toLowerCase().replace(/[^\w\- ]/g, '').replace(/[ ]/g, '-'));
var name = val;
if (headers[val] > 0) {
name = val + '-' + headers[val];
}
if (headers[val] == undefined) {
headers[val] = 1;
} else {
headers[val] += 1;
}
node = node.wrap('<div id="' + name + '" class="anchor-wrap" ></div>');
node.append('<a class="anchor" href="#' + name + '"><span class="octicon octicon-link"></span></a>');
});
}
})(jQuery);
@ -98,6 +142,7 @@ function initCore() {
Gogits.initPopovers();
Gogits.initTabs();
Gogits.initModals();
Gogits.initDropDown();
Gogits.renderMarkdown();
}
@ -142,6 +187,60 @@ function initUserSetting() {
});
}
function initRepository() {
// clone group button script
(function () {
var $clone = $('.clone-group-btn');
if ($clone.length) {
var $url = $('.clone-group-url');
$clone.find('button[data-link]').on("click",function (e) {
var $this = $(this);
if (!$this.hasClass('btn-primary')) {
$clone.find('.btn-primary').removeClass('btn-primary').addClass("btn-default");
$(this).addClass('btn-primary').removeClass('btn-default');
$url.val($this.data("link"));
$clone.find('span.clone-url').text($this.data('link'));
}
}).eq(0).trigger("click");
// todo copy to clipboard
}
})();
// watching script
(function () {
var $watch = $('#gogs-repo-watching'),
watchLink = $watch.data("watch"),
unwatchLink = $watch.data("unwatch");
$watch.on('click', '.to-watch',function () {
if ($watch.hasClass("watching")) {
return false;
}
$.get(watchLink, function (json) {
if (json.ok) {
$watch.find('.text-primary').removeClass('text-primary');
$watch.find('.to-watch h4').addClass('text-primary');
$watch.find('.fa-eye-slash').removeClass('fa-eye-slash').addClass('fa-eye');
$watch.removeClass("no-watching").addClass("watching");
}
});
return false;
}).on('click', '.to-unwatch', function () {
if ($watch.hasClass("no-watching")) {
return false;
}
$.get(unwatchLink, function (json) {
if (json.ok) {
$watch.find('.text-primary').removeClass('text-primary');
$watch.find('.to-unwatch h4').addClass('text-primary');
$watch.find('.fa-eye').removeClass('fa-eye').addClass('fa-eye-slash');
$watch.removeClass("watching").addClass("no-watching");
}
});
return false;
});
})();
}
(function ($) {
$(function () {
initCore();
@ -152,5 +251,8 @@ function initUserSetting() {
if (body.data("page") == "user") {
initUserSetting();
}
if ($('.gogs-repo-nav').length) {
initRepository();
}
});
})(jQuery);

View File

@ -340,7 +340,7 @@ q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?
s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,
q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d=
c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i<c.length;++i)b(c[i]);d===(d|0)&&c[0].setAttribute("value",d);var r=j.createElement("ol");
r.className="linenums";for(var d=Math.max(0,d-1|0)||0,i=0,n=c.length;i<n;++i)k=c[i],k.className="L"+(i+d)%10,k.firstChild||k.appendChild(j.createTextNode("\u00a0")),r.appendChild(k);a.appendChild(r)}function p(a,d){for(var g=d.length;--g>=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*</.test(d)?"default-markup":"default-code";return F[a]}function K(a){var d=a.h;try{var g=T(a.c,a.i),b=g.a;
r.className="linenums";for(var d=Math.max(0,d-1|0)||0,i=0,n=c.length;i<n;++i)k=c[i],k.className="L"+(i+d+1),k.firstChild||k.appendChild(j.createTextNode("\u00a0")),r.appendChild(k);a.appendChild(r)}function p(a,d){for(var g=d.length;--g>=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*</.test(d)?"default-markup":"default-code";return F[a]}function K(a){var d=a.h;try{var g=T(a.c,a.i),b=g.a;
a.a=b;a.d=g.d;a.e=0;I(d,b)(a);var s=/\bMSIE\s(\d+)/.exec(navigator.userAgent),s=s&&+s[1]<=8,d=/\n/g,x=a.a,m=x.length,g=0,j=a.d,k=j.length,b=0,c=a.g,i=c.length,r=0;c[i]=m;var n,e;for(e=n=0;e<i;)c[e]!==c[e+2]?(c[n++]=c[e++],c[n++]=c[e++]):e+=2;i=n;for(e=n=0;e<i;){for(var p=c[e],w=c[e+1],t=e+2;t+2<=i&&c[t+1]===w;)t+=2;c[n++]=p;c[n++]=w;e=t}c.length=n;var f=a.c,h;if(f)h=f.style.display,f.style.display="none";try{for(;b<k;){var l=j[b+2]||m,B=c[r+2]||m,t=Math.min(l,B),A=j[b+1],G;if(A.nodeType!==1&&(G=x.substring(g,
t))){s&&(G=G.replace(d,"\r"));A.nodeValue=G;var L=A.ownerDocument,o=L.createElement("span");o.className=c[r+1];var v=A.parentNode;v.replaceChild(o,A);o.appendChild(A);g<l&&(j[b+1]=A=L.createTextNode(x.substring(t,l)),v.insertBefore(A,o.nextSibling))}g=t;g>=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],
@ -400,7 +400,7 @@ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n\u000
["kwd",/^-[_a-z]+/],["typ",/^[A-Z_]\w*/],["pun",/^[,.;]/]]),["erlang","erl"]);
// lang-go.js
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["pln",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])+(?:'|$)|`[^`]*(?:`|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\/\*[\S\s]*?\*\/)/],["pln",/^(?:[^"'/`]|\/(?![*/]))+/]]),["go"]);
// PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["pln",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])+(?:'|$)|`[^`]*(?:`|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\/\*[\S\s]*?\*\/)/],["pln",/^(?:[^"'/`]|\/(?![*/]))+/]]),["go"]);
// lang-hs.js
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n\u000b\u000c\r "],["str",/^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["str",/^'(?:[^\n\f\r'\\]|\\[^&])'?/,null,"'"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i,null,"0123456789"]],[["com",/^(?:--+[^\n\f\r]*|{-(?:[^-]|-+[^}-])*-})/],["kwd",/^(?:case|class|data|default|deriving|do|else|if|import|in|infix|infixl|infixr|instance|let|module|newtype|of|then|type|where|_)(?=[^\d'A-Za-z]|$)/,

77
routers/admin/admin.go Normal file
View File

@ -0,0 +1,77 @@
// 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 admin
import (
"strings"
"github.com/codegangsta/martini"
"github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/middleware"
)
func Dashboard(ctx *middleware.Context) {
ctx.Data["Title"] = "Admin Dashboard"
ctx.Data["PageIsDashboard"] = true
ctx.Data["Stats"] = models.GetStatistic()
ctx.HTML(200, "admin/dashboard")
}
func Users(ctx *middleware.Context) {
ctx.Data["Title"] = "User Management"
ctx.Data["PageIsUsers"] = true
var err error
ctx.Data["Users"], err = models.GetUsers(100, 0)
if err != nil {
ctx.Handle(200, "admin.Users", err)
return
}
ctx.HTML(200, "admin/users")
}
func Repositories(ctx *middleware.Context) {
ctx.Data["Title"] = "Repository Management"
ctx.Data["PageIsRepos"] = true
var err error
ctx.Data["Repos"], err = models.GetRepos(100, 0)
if err != nil {
ctx.Handle(200, "admin.Repositories", err)
return
}
ctx.HTML(200, "admin/repos")
}
func Config(ctx *middleware.Context) {
ctx.Data["Title"] = "Server Configuration"
ctx.Data["PageIsConfig"] = true
ctx.Data["AppUrl"] = base.AppUrl
ctx.Data["Domain"] = base.Domain
ctx.Data["RunUser"] = base.RunUser
ctx.Data["RunMode"] = strings.Title(martini.Env)
ctx.Data["RepoRootPath"] = base.RepoRootPath
ctx.Data["Service"] = base.Service
ctx.Data["DbCfg"] = models.DbCfg
ctx.Data["MailerEnabled"] = false
if base.MailService != nil {
ctx.Data["MailerEnabled"] = true
ctx.Data["Mailer"] = base.MailService
}
ctx.Data["CacheAdapter"] = base.CacheAdapter
ctx.Data["CacheConfig"] = base.CacheConfig
ctx.Data["LogMode"] = base.LogMode
ctx.Data["LogConfig"] = base.LogConfig
ctx.HTML(200, "admin/config")
}

109
routers/admin/user.go Normal file
View File

@ -0,0 +1,109 @@
// 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 admin
import (
"strings"
"github.com/codegangsta/martini"
"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"
)
func NewUser(ctx *middleware.Context, form auth.RegisterForm) {
ctx.Data["Title"] = "New Account"
ctx.Data["PageIsUsers"] = true
if ctx.Req.Method == "GET" {
ctx.HTML(200, "admin/users/new")
return
}
if form.Password != form.RetypePasswd {
ctx.Data["HasError"] = true
ctx.Data["Err_Password"] = true
ctx.Data["Err_RetypePasswd"] = true
ctx.Data["ErrorMsg"] = "Password and re-type password are not same"
auth.AssignForm(form, ctx.Data)
}
if ctx.HasError() {
ctx.HTML(200, "admin/users/new")
return
}
u := &models.User{
Name: form.UserName,
Email: form.Email,
Passwd: form.Password,
IsActive: true,
}
var err error
if u, err = models.RegisterUser(u); err != nil {
switch err {
case models.ErrUserAlreadyExist:
ctx.RenderWithErr("Username has been already taken", "admin/users/new", &form)
case models.ErrEmailAlreadyUsed:
ctx.RenderWithErr("E-mail address has been already used", "admin/users/new", &form)
case models.ErrUserNameIllegal:
ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "admin/users/new", &form)
default:
ctx.Handle(200, "admin.user.NewUser", err)
}
return
}
log.Trace("%s User created by admin(%s): %s", ctx.Req.RequestURI,
ctx.User.LowerName, strings.ToLower(form.UserName))
ctx.Redirect("/admin/users")
}
func EditUser(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(200, "admin.user.EditUser", err)
return
}
u, err := models.GetUserById(int64(uid))
if err != nil {
ctx.Handle(200, "admin.user.EditUser", err)
return
}
if ctx.Req.Method == "GET" {
ctx.Data["User"] = u
ctx.HTML(200, "admin/users/edit")
return
}
u.Email = form.Email
u.Website = form.Website
u.Location = form.Location
u.Avatar = base.EncodeMd5(form.Avatar)
u.AvatarEmail = form.Avatar
u.IsActive = form.Active == "on"
u.IsAdmin = form.Admin == "on"
if err := models.UpdateUser(u); err != nil {
ctx.Handle(200, "admin.user.EditUser", err)
return
}
ctx.Data["IsSuccess"] = true
ctx.Data["User"] = u
ctx.HTML(200, "admin/users/edit")
log.Trace("%s User profile updated by admin(%s): %s", ctx.Req.RequestURI,
ctx.User.LowerName, ctx.User.LowerName)
}

View File

@ -15,10 +15,10 @@ func Home(ctx *middleware.Context) {
return
}
ctx.Data["PageIsHome"] = true
ctx.HTML(200, "home", ctx.Data)
ctx.HTML(200, "home")
}
func Help(ctx *middleware.Context) {
ctx.Data["PageIsHelp"] = true
ctx.HTML(200, "help", ctx.Data)
ctx.HTML(200, "help")
}

View File

@ -21,5 +21,5 @@ func TemplatePreview(ctx *middleware.Context, params martini.Params) {
ctx.Data["Code"] = "2014031910370000009fff6782aadb2162b4a997acb69d4400888e0b9274657374"
ctx.Data["ActiveCodeLives"] = base.Service.ActiveCodeLives / 60
ctx.Data["ResetPwdCodeLives"] = base.Service.ResetPwdCodeLives / 60
ctx.HTML(200, params["_1"], ctx.Data)
ctx.HTML(200, params["_1"])
}

View File

@ -18,7 +18,7 @@ func Create(ctx *middleware.Context, form auth.CreateRepoForm) {
ctx.Data["Licenses"] = models.Licenses
if ctx.Req.Method == "GET" {
ctx.HTML(200, "repo/create", ctx.Data)
ctx.HTML(200, "repo/create")
return
}
@ -31,6 +31,9 @@ func Create(ctx *middleware.Context, form auth.CreateRepoForm) {
} else if err == models.ErrRepoAlreadyExist {
ctx.RenderWithErr("Repository name has already been used", "repo/create", &form)
return
} else if err == models.ErrRepoNameIllegal {
ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "repo/create", &form)
return
}
ctx.Handle(200, "repo.Create", err)
}
@ -45,7 +48,7 @@ func SettingPost(ctx *middleware.Context) {
case "delete":
if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") {
ctx.Data["ErrorMsg"] = "Please make sure you entered repository name is correct."
ctx.HTML(200, "repo/setting", ctx.Data)
ctx.HTML(200, "repo/setting")
return
}

View File

@ -40,7 +40,7 @@ func Branches(ctx *middleware.Context, params martini.Params) {
ctx.Data["Branches"] = brs
ctx.Data["IsRepoToolbarBranches"] = true
ctx.HTML(200, "repo/branches", ctx.Data)
ctx.HTML(200, "repo/branches")
}
func Single(ctx *middleware.Context, params martini.Params) {
@ -61,6 +61,8 @@ func Single(ctx *middleware.Context, params martini.Params) {
return
}
ctx.Data["IsRepoToolbarSource"] = true
// Branches.
brs, err := models.GetBranches(params["username"], params["reponame"])
if err != nil {
@ -69,7 +71,7 @@ func Single(ctx *middleware.Context, params martini.Params) {
return
} else if len(brs) == 0 {
ctx.Data["IsBareRepo"] = true
ctx.HTML(200, "repo/single", ctx.Data)
ctx.HTML(200, "repo/single")
return
}
@ -86,6 +88,11 @@ func Single(ctx *middleware.Context, params martini.Params) {
branchLink := "/" + ctx.Repo.Owner.LowerName + "/" + ctx.Repo.Repository.Name + "/src/" + params["branchname"]
if len(treename) != 0 && repoFile == nil {
ctx.Error(404)
return
}
if repoFile != nil && repoFile.IsFile() {
if repoFile.Size > 1024*1024 || repoFile.Filemode != git.FileModeBlob {
ctx.Data["FileIsLarge"] = true
@ -95,6 +102,11 @@ func Single(ctx *middleware.Context, params martini.Params) {
} else {
ctx.Data["IsFile"] = true
ctx.Data["FileName"] = repoFile.Name
ext := path.Ext(repoFile.Name)
if len(ext) > 0 {
ext = ext[1:]
}
ctx.Data["FileExt"] = ext
readmeExist := base.IsMarkdownFile(repoFile.Name) || base.IsReadmeFile(repoFile.Name)
ctx.Data["ReadmeExist"] = readmeExist
@ -139,10 +151,9 @@ func Single(ctx *middleware.Context, params martini.Params) {
return
} else {
// current repo branch link
urlPrefix := "http://" + base.Domain + branchLink
ctx.Data["FileName"] = readmeFile.Name
ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), urlPrefix))
ctx.Data["FileContent"] = string(base.RenderMarkdown(blob.Contents(), branchLink))
}
}
}
@ -178,9 +189,8 @@ func Single(ctx *middleware.Context, params martini.Params) {
ctx.Data["Paths"] = Paths
ctx.Data["Treenames"] = treenames
ctx.Data["IsRepoToolbarSource"] = true
ctx.Data["BranchLink"] = branchLink
ctx.HTML(200, "repo/single", ctx.Data)
ctx.HTML(200, "repo/single")
}
func Http(ctx *middleware.Context, params martini.Params) {
@ -212,6 +222,8 @@ func Setting(ctx *middleware.Context, params martini.Params) {
return
}
ctx.Data["IsRepoToolbarSetting"] = true
// Branches.
brs, err := models.GetBranches(params["username"], params["reponame"])
if err != nil {
@ -220,7 +232,7 @@ func Setting(ctx *middleware.Context, params martini.Params) {
return
} else if len(brs) == 0 {
ctx.Data["IsBareRepo"] = true
ctx.HTML(200, "repo/setting", ctx.Data)
ctx.HTML(200, "repo/setting")
return
}
@ -229,9 +241,13 @@ func Setting(ctx *middleware.Context, params martini.Params) {
title = t
}
if len(params["branchname"]) == 0 {
params["branchname"] = "master"
}
ctx.Data["Branchname"] = params["branchname"]
ctx.Data["Title"] = title + " - settings"
ctx.Data["IsRepoToolbarSetting"] = true
ctx.HTML(200, "repo/setting", ctx.Data)
ctx.HTML(200, "repo/setting")
}
func Commits(ctx *middleware.Context, params martini.Params) {
@ -255,17 +271,17 @@ func Commits(ctx *middleware.Context, params martini.Params) {
ctx.Data["Reponame"] = params["reponame"]
ctx.Data["CommitCount"] = commits.Len()
ctx.Data["Commits"] = commits
ctx.HTML(200, "repo/commits", ctx.Data)
ctx.HTML(200, "repo/commits")
}
func Issues(ctx *middleware.Context) {
ctx.Data["IsRepoToolbarIssues"] = true
ctx.HTML(200, "repo/issues", ctx.Data)
ctx.HTML(200, "repo/issues")
}
func Pulls(ctx *middleware.Context) {
ctx.Data["IsRepoToolbarPulls"] = true
ctx.HTML(200, "repo/pulls", ctx.Data)
ctx.HTML(200, "repo/pulls")
}
func Action(ctx *middleware.Context, params martini.Params) {

View File

@ -24,13 +24,13 @@ func Setting(ctx *middleware.Context, form auth.UpdateProfileForm) {
ctx.Data["Owner"] = user
if ctx.Req.Method == "GET" {
ctx.HTML(200, "user/setting", ctx.Data)
ctx.HTML(200, "user/setting")
return
}
// below is for POST requests
if hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) {
ctx.HTML(200, "user/setting", ctx.Data)
ctx.HTML(200, "user/setting")
return
}
@ -45,7 +45,8 @@ func Setting(ctx *middleware.Context, form auth.UpdateProfileForm) {
}
ctx.Data["IsSuccess"] = true
ctx.HTML(200, "user/setting", ctx.Data)
ctx.HTML(200, "user/setting")
log.Trace("%s User setting updated: %s", ctx.Req.RequestURI, ctx.User.LowerName)
}
@ -55,7 +56,7 @@ func SettingPassword(ctx *middleware.Context, form auth.UpdatePasswdForm) {
ctx.Data["IsUserPageSettingPasswd"] = true
if ctx.Req.Method == "GET" {
ctx.HTML(200, "user/password", ctx.Data)
ctx.HTML(200, "user/password")
return
}
@ -82,7 +83,7 @@ func SettingPassword(ctx *middleware.Context, form auth.UpdatePasswdForm) {
}
ctx.Data["Owner"] = user
ctx.HTML(200, "user/password", ctx.Data)
ctx.HTML(200, "user/password")
log.Trace("%s User password updated: %s", ctx.Req.RequestURI, ctx.User.LowerName)
}
@ -123,7 +124,7 @@ func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
// Add new SSH key.
if ctx.Req.Method == "POST" {
if hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) {
ctx.HTML(200, "user/publickey", ctx.Data)
ctx.HTML(200, "user/publickey")
return
}
@ -155,7 +156,7 @@ func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
ctx.Data["PageIsUserSetting"] = true
ctx.Data["IsUserPageSettingSSH"] = true
ctx.Data["Keys"] = keys
ctx.HTML(200, "user/publickey", ctx.Data)
ctx.HTML(200, "user/publickey")
}
func SettingNotification(ctx *middleware.Context) {
@ -163,7 +164,7 @@ func SettingNotification(ctx *middleware.Context) {
ctx.Data["Title"] = "Notification"
ctx.Data["PageIsUserSetting"] = true
ctx.Data["IsUserPageSettingNotify"] = true
ctx.HTML(200, "user/notification", ctx.Data)
ctx.HTML(200, "user/notification")
}
func SettingSecurity(ctx *middleware.Context) {
@ -171,5 +172,5 @@ func SettingSecurity(ctx *middleware.Context) {
ctx.Data["Title"] = "Security"
ctx.Data["PageIsUserSetting"] = true
ctx.Data["IsUserPageSettingSecurity"] = true
ctx.HTML(200, "user/security", ctx.Data)
ctx.HTML(200, "user/security")
}

View File

@ -34,7 +34,7 @@ func Dashboard(ctx *middleware.Context) {
return
}
ctx.Data["Feeds"] = feeds
ctx.HTML(200, "user/dashboard", ctx.Data)
ctx.HTML(200, "user/dashboard")
}
func Profile(ctx *middleware.Context, params martini.Params) {
@ -70,19 +70,19 @@ func Profile(ctx *middleware.Context, params martini.Params) {
}
ctx.Data["PageIsUserProfile"] = true
ctx.HTML(200, "user/profile", ctx.Data)
ctx.HTML(200, "user/profile")
}
func SignIn(ctx *middleware.Context, form auth.LogInForm) {
ctx.Data["Title"] = "Log In"
if ctx.Req.Method == "GET" {
ctx.HTML(200, "user/signin", ctx.Data)
ctx.HTML(200, "user/signin")
return
}
if hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) {
ctx.HTML(200, "user/signin", ctx.Data)
ctx.HTML(200, "user/signin")
return
}
@ -112,8 +112,14 @@ func SignUp(ctx *middleware.Context, form auth.RegisterForm) {
ctx.Data["Title"] = "Sign Up"
ctx.Data["PageIsSignUp"] = true
if base.Service.DisenableRegisteration {
ctx.Data["DisenableRegisteration"] = true
ctx.HTML(200, "user/signup")
return
}
if ctx.Req.Method == "GET" {
ctx.HTML(200, "user/signup", ctx.Data)
ctx.HTML(200, "user/signup")
return
}
@ -126,7 +132,7 @@ func SignUp(ctx *middleware.Context, form auth.RegisterForm) {
}
if ctx.HasError() {
ctx.HTML(200, "user/signup", ctx.Data)
ctx.HTML(200, "user/signup")
return
}
@ -139,11 +145,13 @@ func SignUp(ctx *middleware.Context, form auth.RegisterForm) {
var err error
if u, err = models.RegisterUser(u); err != nil {
switch err.Error() {
case models.ErrUserAlreadyExist.Error():
switch err {
case models.ErrUserAlreadyExist:
ctx.RenderWithErr("Username has been already taken", "user/signup", &form)
case models.ErrEmailAlreadyUsed.Error():
case models.ErrEmailAlreadyUsed:
ctx.RenderWithErr("E-mail address has been already used", "user/signup", &form)
case models.ErrUserNameIllegal:
ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), "user/signup", &form)
default:
ctx.Handle(200, "user.SignUp", err)
}
@ -153,12 +161,16 @@ func SignUp(ctx *middleware.Context, form auth.RegisterForm) {
log.Trace("%s User created: %s", ctx.Req.RequestURI, strings.ToLower(form.UserName))
// Send confirmation e-mail.
if base.Service.RegisterEmailConfirm {
if base.Service.RegisterEmailConfirm && u.Id > 1 {
mailer.SendRegisterMail(ctx.Render, u)
ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email
ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60
ctx.Render.HTML(200, "user/active", ctx.Data)
ctx.HTML(200, "user/active")
if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
return
}
ctx.Redirect("/user/login")
@ -170,7 +182,7 @@ func Delete(ctx *middleware.Context) {
ctx.Data["IsUserPageSettingDelete"] = true
if ctx.Req.Method == "GET" {
ctx.HTML(200, "user/delete", ctx.Data)
ctx.HTML(200, "user/delete")
return
}
@ -195,7 +207,7 @@ func Delete(ctx *middleware.Context) {
}
}
ctx.HTML(200, "user/delete", ctx.Data)
ctx.HTML(200, "user/delete")
}
const (
@ -218,15 +230,15 @@ func Feeds(ctx *middleware.Context, form auth.FeedsForm) {
}
func Issues(ctx *middleware.Context) {
ctx.HTML(200, "user/issues", ctx.Data)
ctx.HTML(200, "user/issues")
}
func Pulls(ctx *middleware.Context) {
ctx.HTML(200, "user/pulls", ctx.Data)
ctx.HTML(200, "user/pulls")
}
func Stars(ctx *middleware.Context) {
ctx.HTML(200, "user/stars", ctx.Data)
ctx.HTML(200, "user/stars")
}
func Activate(ctx *middleware.Context) {
@ -239,12 +251,16 @@ func Activate(ctx *middleware.Context) {
}
// Resend confirmation e-mail.
if base.Service.RegisterEmailConfirm {
ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60
mailer.SendActiveMail(ctx.Render, ctx.User)
if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
ctx.Data["ResendLimited"] = true
} else {
ctx.Data["Hours"] = base.Service.ActiveCodeLives / 60
mailer.SendActiveMail(ctx.Render, ctx.User)
}
} else {
ctx.Data["ServiceNotEnabled"] = true
}
ctx.Render.HTML(200, "user/active", ctx.Data)
ctx.HTML(200, "user/active")
return
}
@ -263,5 +279,5 @@ func Activate(ctx *middleware.Context) {
}
ctx.Data["IsActivateFailed"] = true
ctx.Render.HTML(200, "user/active", ctx.Data)
ctx.HTML(200, "user/active")
}

View File

@ -12,7 +12,9 @@ import (
"strings"
"github.com/codegangsta/cli"
"github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/base"
)
var (
@ -43,6 +45,10 @@ func In(b string, sl map[string]int) bool {
}
func runServ(*cli.Context) {
base.NewConfigContext()
models.LoadModelsConfig()
models.NewEngine()
keys := strings.Split(os.Args[2], "-")
if len(keys) != 2 {
fmt.Println("auth file format error")
@ -144,7 +150,7 @@ func runServ(*cli.Context) {
}
gitcmd := exec.Command(verb, rRepo)
gitcmd.Dir = models.RepoRootPath
gitcmd.Dir = base.RepoRootPath
gitcmd.Stdout = os.Stdout
gitcmd.Stdin = os.Stdin
gitcmd.Stderr = os.Stderr

View File

@ -0,0 +1,93 @@
{{template "base/head" .}}
{{template "base/navbar" .}}
<div id="gogs-body" class="container" data-page="admin">
{{template "admin/nav" .}}
<div id="gogs-admin-container" class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
Server Configuration
</div>
<div class="panel-body">
<div><b>Application Name:</b> {{AppName}}</div>
<div><b>Application Version:</b> {{AppVer}}</div>
<div><b>Application URL:</b> {{.AppUrl}}</div>
<div><b>Domain:</b> {{.Domain}}</div>
<hr/>
<div><b>Run User:</b> {{.RunUser}}</div>
<div><b>Run Mode:</b> {{.RunMode}}</div>
<hr/>
<div><b>Repository Root Path:</b> {{.RepoRootPath}}</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
Database Configuration
</div>
<div class="panel-body">
<div><b>Type:</b> {{.DbCfg.Type}}</div>
<div><b>Host:</b> {{.DbCfg.Host}}</div>
<div><b>Name:</b> {{.DbCfg.Name}}</div>
<div><b>User:</b> {{.DbCfg.User}}</div>
<div><b>SslMode:</b> {{.DbCfg.SslMode}} (for "postgres" only)</div>
<div><b>Path:</b> {{.DbCfg.Path}} (for "sqlite3" only)</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
Service Configuration
</div>
<div class="panel-body">
<div><b>Register Email Confirmation:</b> <i class="fa fa{{if .Service.RegisterEmailConfirm}}-check{{end}}-square-o"></i></div>
<div><b>Disenable Registeration:</b> <i class="fa fa{{if .Service.DisenableRegisteration}}-check{{end}}-square-o"></i></div>
<div><b>Require Sign In View:</b> <i class="fa fa{{if .Service.RequireSignInView}}-check{{end}}-square-o"></i></div>
<hr/>
<div><b>Active Code Lives:</b> {{.Service.ActiveCodeLives}} minutes</div>
<div><b>Reset Password Code Lives:</b> {{.Service.ResetPwdCodeLives}} minutes</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
Mailer Configuration
</div>
<div class="panel-body">
<div><b>Enabled:</b> <i class="fa fa{{if .MailerEnabled}}-check{{end}}-square-o"></i></div>
<div><b>Name:</b> {{.Mailer.Name}}</div>
<div><b>Host:</b> {{.Mailer.Host}}</div>
<div><b>User:</b> {{.Mailer.User}}</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
Cache Configuration
</div>
<div class="panel-body">
<div><b>Cache Adapter:</b> {{.CacheAdapter}}</div>
<div><b>Cache Config:</b></div>
<div style="padding-top: 5px;"><pre>{{.CacheConfig}}</pre></div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
Log Configuration
</div>
<div class="panel-body">
<div><b>Log Mode:</b> {{.LogMode}}</div>
<div><b>Log Config:</b></div>
<div style="padding-top: 5px;"><pre>{{.LogConfig}}</pre></div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@ -0,0 +1,26 @@
{{template "base/head" .}}
{{template "base/navbar" .}}
<div id="gogs-body" class="container" data-page="admin">
{{template "admin/nav" .}}
<div id="gogs-admin-container" class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
Statistic
</div>
<div class="panel-body">
Gogs database has <b>{{.Stats.Counter.User}}</b> users, <b>{{.Stats.Counter.PublicKey}}</b> SSH keys, <b>{{.Stats.Counter.Repo}}</b> repositories, <b>{{.Stats.Counter.Watch}}</b> watches, <b>{{.Stats.Counter.Action}}</b> actions, and <b>{{.Stats.Counter.Access}}</b> accesses.
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
System Status
</div>
<div class="panel-body">
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

8
templates/admin/nav.tmpl Normal file
View File

@ -0,0 +1,8 @@
<div id="gogs-user-setting-nav" class="col-md-3 gogs-admin-nav">
<ul class="list-group" data-init="tabs">
<li class="list-group-item{{if .PageIsDashboard}} active{{end}}"><a href="/admin"><i class="fa fa-tachometer fa-lg"></i> Dashboard</a></li>
<li class="list-group-item{{if .PageIsUsers}} active{{end}}"><a href="/admin/users"><i class="fa fa-users fa-lg"></i> Users</a></li>
<li class="list-group-item{{if .PageIsRepos}} active{{end}}"><a href="/admin/repos"><i class="fa fa-book fa-lg"></i> Repositories</a></li>
<li class="list-group-item{{if .PageIsConfig}} active{{end}}"><a href="/admin/config"><i class="fa fa-cogs fa-lg"></i> Configuration</a></li>
</ul>
</div>

View File

@ -0,0 +1,42 @@
{{template "base/head" .}}
{{template "base/navbar" .}}
<div id="gogs-body" class="container" data-page="admin">
{{template "admin/nav" .}}
<div id="gogs-admin-container" class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
Repository Management
</div>
<div class="panel-body">
<table class="table table-striped">
<thead>
<tr>
<th>Id</th>
<th>Owner</th>
<th>Name</th>
<th>Private</th>
<th>Watches</th>
<th>Forks</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{{range .Repos}}
<tr>
<td>{{.Id}}</td>
<th>{{.UserName}}</th>
<td><a href="/{{.UserName}}/{{.Name}}">{{.Name}}</a></td>
<td><i class="fa fa{{if .Private}}-check{{end}}-square-o"></i></td>
<td>{{.NumWatches}}</td>
<td>{{.NumForks}}</td>
<td>{{DateFormat .Created "M d, Y"}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@ -0,0 +1,45 @@
{{template "base/head" .}}
{{template "base/navbar" .}}
<div id="gogs-body" class="container" data-page="admin">
{{template "admin/nav" .}}
<div id="gogs-admin-container" class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
User Management
</div>
<div class="panel-body">
<a href="/admin/users/new" class="btn btn-primary">New Account</a>
<table class="table table-striped">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>E-mail</th>
<th>Actived</th>
<th>Admin</th>
<th>Repos</th>
<th>Join</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
{{range .Users}}
<tr>
<td>{{.Id}}</td>
<td><a href="/user/{{.Name}}">{{.Name}}</a></td>
<td>{{.Email}}</td>
<td><i class="fa fa{{if .IsActive}}-check{{end}}-square-o"></i></td>
<td><i class="fa fa{{if .IsAdmin}}-check{{end}}-square-o"></i></td>
<td>{{.NumRepos}}</td>
<td>{{DateFormat .Created "M d, Y"}}</td>
<td><a href="/admin/users/{{.Id}}"><i class="fa fa-pencil-square-o"></i></a></td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@ -0,0 +1,83 @@
{{template "base/head" .}}
{{template "base/navbar" .}}
<div id="gogs-body" class="container" data-page="admin">
{{template "admin/nav" .}}
<div id="gogs-admin-container" class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
Edit Account
</div>
<div class="panel-body">
<br/>
<form action="/admin/users/{{.User.Id}}" method="post" class="form-horizontal">
{{if .IsSuccess}}<p class="alert alert-success">Account profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}}
<input type="hidden" value="{{.User.Id}}" name="userId"/>
<div class="form-group">
<label class="col-md-3 control-label">Username: </label>
<label class="control-label">{{.User.Name}}</label>
</div>
<div class="form-group {{if .Err_Email}}has-error has-feedback{{end}}">
<label class="col-md-3 control-label">Email<strong class="text-danger">*</strong></label>
<div class="col-md-7">
<input name="email" class="form-control" placeholder="Type account's e-mail address" value="{{.User.Email}}" required="required">
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">Website</label>
<div class="col-md-7">
<input name="website" class="form-control" placeholder="Type account's website URL" value="{{.User.Website}}">
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">Location</label>
<div class="col-md-7">
<input name="location" class="form-control" placeholder="Type account's current location" value="{{.User.Location}}">
</div>
</div>
<div class="form-group {{if .Err_Avatar}}has-error has-feedback{{end}}">
<label class="col-md-3 control-label">Gravatar Email<strong class="text-danger">*</strong></label>
<div class="col-md-7">
<input name="avatar" class="form-control" placeholder="Type account's Gravatar e-mail address" required="required" value="{{.User.AvatarEmail}}">
</div>
</div>
<div class="form-group">
<div class="col-md-7 col-md-offset-3">
<div class="checkbox">
<label>
<input type="checkbox" name="active" {{if .User.IsActive}}checked{{end}}>
<strong>This account has activated</strong>
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-7 col-md-offset-3">
<div class="checkbox">
<label>
<input type="checkbox" name="admin" {{if .User.IsAdmin}}checked{{end}}>
<strong>This account has administor permisson</strong>
</label>
</div>
</div>
</div>
<hr/>
<div class="form-group">
<div class="col-md-offset-3 col-md-6">
<button type="submit" class="btn btn-lg btn-primary btn-block">Update account profile</button>
<!-- <a type="button" href="/admin/users/{{.User.Id}}/delete" class="btn btn-lg btn-danger btn-block">Delete this account</a> -->
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@ -0,0 +1,54 @@
{{template "base/head" .}}
{{template "base/navbar" .}}
<div id="gogs-body" class="container" data-page="admin">
{{template "admin/nav" .}}
<div id="gogs-admin-container" class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
New Account
</div>
<div class="panel-body">
<br/>
<form action="/admin/users/new" method="post" class="form-horizontal">
<div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div>
<div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}">
<label class="col-md-3 control-label">Username: </label>
<div class="col-md-7">
<input name="username" class="form-control" placeholder="Type account's username" value="{{.username}}" required="required">
</div>
</div>
<div class="form-group {{if .Err_Email}}has-error has-feedback{{end}}">
<label class="col-md-3 control-label">Email: </label>
<div class="col-md-7">
<input name="email" class="form-control" placeholder="Type account's e-mail address" value="{{.email}}" required="required" title="Email is not valid">
</div>
</div>
<div class="form-group {{if .Err_Password}}has-error has-feedback{{end}}">
<label class="col-md-3 control-label">Password: </label>
<div class="col-md-7">
<input name="passwd" type="password" class="form-control" placeholder="Type account's password" required="required" title="Password must contain at least 6 characters">
</div>
</div>
<div class="form-group {{if .Err_RetypePasswd}}has-error has-feedback{{end}}">
<label class="col-md-3 control-label">Re-type: </label>
<div class="col-md-7">
<input name="retypepasswd" type="password" class="form-control" placeholder="Re-type account's password" required="required" title="Re-type Password must be same to Password">
</div>
</div>
<hr/>
<div class="form-group">
<div class="col-md-offset-3 col-md-7">
<button type="submit" class="btn btn-lg btn-primary">Create new account</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@ -2,12 +2,19 @@
</div>
<footer id="footer">
<div class="container footer-wrap">
<p>© 2014 Gogs · ver {{AppVer}} ·
All: {{LoadTimes .PageStartTime}} ·
Tmpl: {{call .TmplLoadTimes}} ·
<i class="fa fa-github"></i><a target="_blank" href="https://github.com/gogits/gogs">GitHub</a> ·
</p>
<p class="desc"></p>
<div class="row">
<div class="col-md-6">
<p>© 2014 GoGits · Version: {{AppVer}} ·
Page: <b>{{LoadTimes .PageStartTime}}</b> ·
Template: <b>{{call .TmplLoadTimes}}</b>
</p>
</div>
<div class="col-md-1" style="margin: -5px;">
<a target="_blank" href="https://github.com/gogits/gogs"><i class="fa fa-github fa-2x"></i></a>
</div>
<p class="desc"></p>
</div>
</div>
</footer>
</body>

View File

@ -10,7 +10,9 @@
</a>
<a class="navbar-right gogs-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 gogs-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>
{{else}}<a id="gogs-nav-signin" class="gogs-nav-item navbar-right navbar-btn btn btn-danger" href="/user/login/">Sign in</a>{{end}}
{{if .IsAdmin}}<a class="navbar-right gogs-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}}
{{else}}<a id="gogs-nav-signin" class="gogs-nav-item navbar-right navbar-btn btn btn-danger" href="/user/login/">Sign In</a>
<a id="gogs-nav-signup" class="gogs-nav-item navbar-right navbar-btn btn btn-info" href="/user/sign_up/">Sign Up</a>{{end}}
</nav>
</div>
</div>

View File

@ -5,40 +5,62 @@
<h3 class="name"><i class="fa fa-book fa-lg"></i><a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / {{.Repository.Name}}</h3>
<p class="desc">{{.Repository.Description}}{{if .Repository.Website}}<a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}</p>
</div>
{{if not .IsBareRepo}}
<div class="col-md-6 actions text-right">
<div class="btn-group" id="gogs-repo-clone">
<div class="col-md-6 actions text-right clone-group-btn">
{{if not .IsBareRepo}}
<!--<div class="btn-group" id="gogs-repo-clone">
<button type="button" class="btn btn-default"><i class="fa fa-download fa-lg fa-m"></i></button>
<button type="button" class="btn btn-default dropdown-toggle" data-container="body" data-toggle="popover" data-placement="bottom" data-content="<label>SSH:</label><div class='input-group'><input type='text' class='form-control' value='{{.CloneLink.SSH}}'></div>" data-html="1">
<span class="caret"></span>
</button>
</div>
<div class="btn-group" id="gogs-repo-watching">
<button type="button" class="btn btn-default"><i class="fa fa-eye fa-lg fa-m"></i></button>
</div>-->
<div class="btn-group" id="gogs-repo-clone">
<button type="button" class="btn btn-default"><i class="fa fa-download fa-lg fa-m"></i></button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu" role="menu">
<div class="dropdown-item text-left" data-val="not-watching">
<h4 role="presentation" class="dropdown-header">Not Watching</h4>
<div class="dropdown-menu clone-group-btn dropdown-menu-right">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default" data-link="{{.CloneLink.SSH}}" type="button">SSH</button>
<button class="btn btn-default" data-link="{{.CloneLink.HTTPS}}" type="button">HTTPS</button>
</span>
<input type="text" class="form-control clone-group-url" value="" readonly/>
<span class="input-group-btn">
<button class="btn btn-default" type="button"><i class="fa fa-copy" data-toggle="tooltip" title="copy to clipboard" data-placement="top"></i></button>
</span>
</div>
<p class="help-block text-center">Need help cloning? Visit <a href="#">Help</a>!</p>
</div>
</div>
<div class="btn-group {{if .IsRepositoryWatching}}watching{{else}}no-watching{{end}}" id="gogs-repo-watching" data-watch="/{{.SignedUser.Name}}/{{.Repository.Name}}/action/watch" data-unwatch="/{{.SignedUser.Name}}/{{.Repository.Name}}/action/unwatch">
{{if .IsRepositoryWatching}}
<button type="button" class="btn btn-default"><i class="fa fa-eye fa-lg fa-m"></i></button>
{{else}}
<button type="button" class="btn btn-default"><i class="fa fa-eye-slash fa-lg fa-m"></i></button>
{{end}}
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<div class="dropdown-menu">
<div class="dropdown-item text-left to-unwatch">
<h4 role="presentation" class="dropdown-header {{if not .IsRepositoryWatching}}text-primary{{end}}">Not Watching</h4>
<p class="description">You only receive notifications for conversations in which you participate or are @mentioned.</p>
<p class="divider"></p>
</div>
<div class="dropdown-item text-left" data-val="watching">
<h4 role="presentation" class="dropdown-header">Watching</h4>
<div class="dropdown-item text-left to-watch">
<h4 role="presentation" class="dropdown-header {{if .IsRepositoryWatching}}text-primary{{end}}">Watching</h4>
<p class="description">You receive notifications for all conversations in this repository.</p>
</div>
</div>
</div>
<div class="btn-group">
<button type="button" class="btn btn-default"><i class="fa fa-star"></i>Star&nbsp;&nbsp;{{.Repository.NumStars}}</button>
<button type="button" class="btn btn-default" data-toggle="tooltip" data-placement="top" title="Star"><i class="fa fa-star"></i>&nbsp;{{.Repository.NumStars}}</button>
</div>
{{end}}
<div class="btn-group">
<button type="button" class="btn btn-default"><i class="fa fa-code-fork"></i>Fork&nbsp;&nbsp;{{.Repository.NumForks}}</button>
<a type="button" {{if not .IsRepositoryOwner}}href="/{{.Username}}/{{.Reponame}}/fork"{{end}} class="btn btn-default" data-toggle="tooltip" data-placement="top" title="Fork"><i class="fa fa-code-fork fa-lg"></i>&nbsp;{{.Repository.NumForks}}</a>
</div>
</div>
{{end}}
</div>
</div>
</div>

View File

@ -10,20 +10,24 @@
<li class="list-group-item"><a href="#">Notifications</a></li>-->
</ul>
</div>
<div id="gogs-repo-setting-container" class="col-md-9">
{{if .ErrorMsg}}<p class="alert alert-danger">{{.ErrorMsg}}</p>{{end}}
<div class="panel panel-default">
<div class="panel-heading">
Repository Options
</div>
<div class="panel-body">
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
Danger Zone
</div>
<div class="panel-body">
<button type="button" class="btn btn-default pull-right" href="#delete-repository-modal" data-toggle="modal">
Delete this repository

View File

@ -5,7 +5,7 @@
<div id="gogs-body" class="container">
<div id="gogs-source">
{{if .IsBareRepo}}
Need to fill in some guide.
{{template "repo/single_bare" .}}
{{else}}
<div class="source-toolbar">
{{ $n := len .Treenames}}
@ -15,7 +15,7 @@
<b class="caret"></b></a>
<ul class="dropdown-menu">
{{range .Branches}}
<li><a {{if eq . $.Branchname}}class="current" {{end}}href="{{$.BranchLink}}">{{.}}</a></li>
<li><a {{if eq . $.Branchname}}class="current" {{end}}href="/{{$.Username}}/{{$.Reponame}}/src/{{.}}">{{.}}</a></li>
{{end}}
</ul>
</div>

View File

@ -0,0 +1,31 @@
<div class="panel panel-default guide-box clone-group-btn">
<div class="panel-heading guide-head">
<h4>Quick Guide</h4>
</div>
<div class="panel-body guide-content text-center">
<h3>Clone this repository</h3>
<div class="input-group col-md-8 col-md-offset-2 guide-buttons">
<span class="input-group-btn">
<button class="btn btn-default" data-link="{{.CloneLink.SSH}}" type="button">SSH</button>
<button class="btn btn-default" data-link="{{.CloneLink.HTTPS}}" type="button">HTTPS</button>
</span>
<input type="text" class="form-control clone-group-url" id="guide-clone-url" value="" readonly/>
<span class="input-group-btn">
<button class="btn btn-default" type="button"><i class="fa fa-copy" data-toggle="tooltip" title="copy to clipboard" data-placement="top"></i></button>
</span>
</div>
<p>We recommend every repository include a <strong>README</strong>, <strong>LICENSE</strong>, and <strong>.gitignore</strong>.</p>
<hr/>
<h3>Create a new repository on the command line</h3>
<pre class="text-left"><code>touch README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin <span class="clone-url"></span>
git push -u origin master</code></pre>
<hr/>
<h3>Push an existing repository from the command line</h3>
<pre class="text-left"><code>git remote add origin <span class="clone-url"></span>
git push -u origin master</code></pre>
</div>
</div>

View File

@ -1,6 +1,10 @@
<div class="panel panel-default file-content">
<div class="panel-heading file-head">
<i class="icon fa fa-book"></i> {{.FileName}}
{{if .ReadmeExist}}
<i class="icon fa fa-book"></i>
{{else}}
<i class="icon fa fa-file-text-o"></i>
{{end}}{{.FileName}}
</div>
{{if .FileIsLarge}}
<div class="panel-footer">
@ -12,8 +16,15 @@
{{.FileContent|str2html}}
</div>
{{else}}
<div class="panel-body file-body markdown">
<pre><code>{{.FileContent}}</code></pre>
<div class="panel-body file-body file-code">
<table>
<tbody>
<tr>
<td class="lines-num"></td>
<td class="lines-code markdown"><pre class="linenums lang-{{.FileExt}}"><code>{{.FileContent}}</code></pre></td>
</tr>
</tbody>
</table>
</div>
{{end}}
{{end}}

View File

@ -15,9 +15,8 @@
<li><a href="/{{.RepositoryLink}}/release">Release</a></li>
<li><a href="//{{.RepositoryLink}}/wiki">Wiki</a></li>
</ul>
</li>
</li>{{end}}
</ul>
{{end}}
<ul class="nav navbar-nav navbar-right">
{{if not .IsBareRepo}}
<li class="dropdown">

View File

@ -6,6 +6,8 @@
{{if .IsActivatePage}}
{{if .ServiceNotEnabled}}
<p>Sorry, Register Mail Confirmation has been disabled.</p>
{{else if .ResendLimited}}
<p>Sorry, you are sending activation e-mail too frequently, please wait 3 minutes.</p>
{{else}}
<p>New confirmation e-mail has been sent to <b>{{.SignedUser.Email}}</b>, please check your inbox within {{.Hours}} hours to complete your registeration.</p>
<hr/>

View File

@ -33,8 +33,9 @@
</div>
<div class="panel-body">
<ul class="list-group">{{range .MyRepos}}
<li class="list-group-item"><i class="fa fa-book"></i><a href="/{{$.SignedUserName}}/{{.Name}}">{{.Name}}</a>
<li class="list-group-item"><a href="/{{$.SignedUserName}}/{{.Name}}">
<span class="stars pull-right"><i class="fa fa-star"></i>{{.NumStars}}</span>
<i class="fa fa-book"></i>{{.Name}}</a>
</li>{{end}}
</ul>
</div>

View File

@ -5,8 +5,8 @@
<div id="gogs-user-setting-container" class="col-md-9">
<div id="gogs-setting-pwd">
<h4>Account Profile</h4>
<form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting">{{if .IsSuccess}}
<p class="alert alert-success">Your profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}}
<form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting">
{{if .IsSuccess}}<p class="alert alert-success">Your profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}}
<p>Your Email will be public and used for Account related notifications and any web based operations made via the web.</p>
<div class="form-group">
<label class="col-md-2 control-label">Email</label>
@ -29,7 +29,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group {{if .Err_Avatar}}has-error has-feedback{{end}}">
<label class="col-md-2 control-label">Gravatar Email<strong class="text-danger">*</strong></label>
<div class="col-md-8">
<input type="text" name="avatar" class="form-control" placeholder="Type your Gravatar e-mail address" required="required" value="{{.Owner.AvatarEmail}}">

View File

@ -32,7 +32,7 @@
</div>
<div class="form-group text-center" id="gogs-social-login">
<a class="btn btn-default btn-lg">Social Login</a>
<a class="btn btn-danger btn-lg">Register new account</a>
</div>
</form>
</div>

View File

@ -2,6 +2,9 @@
{{template "base/navbar" .}}
<div class="container" id="gogs-body" data-page="user-signup">
<form action="/user/sign_up" method="post" class="form-horizontal gogs-card" id="gogs-login-card">
{{if .DisenableRegisteration}}
Sorry, registeration has been disenabled, you can only get account from administrator.
{{else}}
<h3>Sign Up</h3>
<div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div>
<div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}">
@ -43,6 +46,7 @@
<a href="/user/login">Already have an account? Sign in now!</a>
</div>
</div>
{{end}}
</form>
</div>
{{template "base/footer" .}}

25
web.go
View File

@ -16,11 +16,14 @@ import (
"github.com/gogits/binding"
"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/mailer"
"github.com/gogits/gogs/modules/middleware"
"github.com/gogits/gogs/routers"
"github.com/gogits/gogs/routers/admin"
"github.com/gogits/gogs/routers/dev"
"github.com/gogits/gogs/routers/repo"
"github.com/gogits/gogs/routers/user"
@ -35,6 +38,16 @@ gogs web`,
Flags: []cli.Flag{},
}
// globalInit is for global configuration reload-able.
func globalInit() {
base.NewConfigContext()
mailer.NewMailerContext()
models.LoadModelsConfig()
models.LoadRepoConfig()
models.NewRepoContext()
models.NewEngine()
}
// Check run mode(Default of martini is Dev).
func checkRunMode() {
switch base.Cfg.MustValue("", "RUN_MODE") {
@ -58,6 +71,7 @@ func newMartini() *martini.ClassicMartini {
}
func runWeb(*cli.Context) {
globalInit()
base.NewServices()
checkRunMode()
log.Info("%s %s", base.AppName, base.AppVer)
@ -73,7 +87,8 @@ func runWeb(*cli.Context) {
m.Use(middleware.InitContext())
reqSignIn, ignSignIn := middleware.SignInRequire(true), middleware.SignInRequire(false)
reqSignIn := middleware.SignInRequire(true)
ignSignIn := middleware.SignInRequire(base.Service.RequireSignInView)
reqSignOut := middleware.SignOutRequire()
// Routers.
m.Get("/", ignSignIn, routers.Home)
@ -99,6 +114,14 @@ func runWeb(*cli.Context) {
m.Get("/help", routers.Help)
adminReq := middleware.AdminRequire()
m.Get("/admin", reqSignIn, adminReq, admin.Dashboard)
m.Get("/admin/users", reqSignIn, adminReq, admin.Users)
m.Any("/admin/users/new", reqSignIn, adminReq, binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser)
m.Any("/admin/users/:userid", reqSignIn, adminReq, binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser)
m.Get("/admin/repos", reqSignIn, adminReq, admin.Repositories)
m.Get("/admin/config", reqSignIn, adminReq, admin.Config)
m.Post("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.SettingPost)
m.Get("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.Setting)