mirror of https://github.com/gogits/gogs.git
Jesse Zhu
9 years ago
1168 changed files with 9986 additions and 8648 deletions
@ -1,20 +1,21 @@
|
||||
.git |
||||
.git/ |
||||
.git/* |
||||
.git/** |
||||
conf |
||||
conf/ |
||||
conf/* |
||||
conf/** |
||||
packager |
||||
packager/ |
||||
packager/* |
||||
packager/** |
||||
scripts |
||||
scripts/ |
||||
scripts/* |
||||
scripts/** |
||||
.github/ |
||||
.github/** |
||||
config.codekit |
||||
LICENSE |
||||
Makefile |
||||
.dockerignore |
||||
*.yml |
||||
*.md |
||||
.bra.toml |
||||
.editorconfig |
||||
.gitignore |
||||
.gopmfile |
||||
config.codekit |
||||
LICENSE |
||||
Dockerfile* |
||||
|
@ -0,0 +1,22 @@
|
||||
We DO NOT take questions or config/deploy problems on GitHub, please use our forum: https://discuss.gogs.io |
||||
|
||||
Please take a moment to search that an issue doesn't already exist. |
||||
|
||||
For bug reports, please give the relevant info: |
||||
|
||||
- Gogs version (or commit ref): |
||||
- Git version: |
||||
- Operating system: |
||||
- Database: |
||||
- [ ] PostgreSQL |
||||
- [ ] MySQL |
||||
- [ ] SQLite |
||||
- Can you reproduce the bug at http://try.gogs.io: |
||||
- [ ] Yes (provide example URL) |
||||
- [ ] No |
||||
- [ ] Not relevant |
||||
- Log gist: |
||||
|
||||
## Description |
||||
|
||||
... |
@ -0,0 +1,4 @@
|
||||
Please, make sure you are targeting the `develop` branch! |
||||
|
||||
More instructions about contributing with Gogs code can be found here: |
||||
https://github.com/gogits/gogs/wiki/Contributing-Code |
@ -1,7 +1,3 @@
|
||||
Execute following command in ROOT directory when anything is changed: |
||||
|
||||
$ go-bindata -o=modules/bindata/bindata.go -ignore="\\.DS_Store|README.md" -pkg=bindata conf/... |
||||
|
||||
Add -debug flag to make life easier in development(somehow isn't working): |
||||
|
||||
$ go-bindata -debug -o=modules/bindata/bindata.go -ignore="\\.DS_Store|README.md" -pkg=bindata conf/... |
||||
$ make bindata |
@ -0,0 +1,9 @@
|
||||
#!/bin/sh |
||||
# Crontabs are located by default in /var/spool/cron/crontabs/ |
||||
# The default configuration is also calling all the scripts in /etc/periodic/${period} |
||||
|
||||
if test -f ./setup; then |
||||
source ./setup |
||||
fi |
||||
|
||||
exec gosu root /usr/sbin/crond -fS |
@ -0,0 +1,144 @@
|
||||
hash: f2fa73b9a379e1fa12f2b48fb0b9942a545b4518a2d71cbd956ee81093347773 |
||||
updated: 2016-03-19T14:44:26.835671547-04:00 |
||||
imports: |
||||
- name: github.com/bradfitz/gomemcache |
||||
version: fb1f79c6b65acda83063cbc69f6bba1522558bfc |
||||
subpackages: |
||||
- memcache |
||||
- name: github.com/codegangsta/cli |
||||
version: aca5b047ed14d17224157c3434ea93bf6cdaadee |
||||
- name: github.com/go-macaron/binding |
||||
version: a68f34212fe257219981e43adfe4c96ab48f42cd |
||||
- name: github.com/go-macaron/cache |
||||
version: 56173531277692bc2925924d51fda1cd0a6b8178 |
||||
subpackages: |
||||
- memcache |
||||
- redis |
||||
- name: github.com/go-macaron/captcha |
||||
version: 8aa5919789ab301e865595eb4b1114d6b9847deb |
||||
- name: github.com/go-macaron/csrf |
||||
version: 6a9a7df172cc1fcd81e4585f44b09200b6087cc0 |
||||
- name: github.com/go-macaron/gzip |
||||
version: cad1c6580a07c56f5f6bc52d66002a05985c5854 |
||||
- name: github.com/go-macaron/i18n |
||||
version: d2d3329f13b52314f3292c4cecb601fad13f02c8 |
||||
- name: github.com/go-macaron/inject |
||||
version: c5ab7bf3a307593cd44cb272d1a5beea473dd072 |
||||
- name: github.com/go-macaron/session |
||||
version: 66031fcb37a0fff002a1f028eb0b3a815c78306b |
||||
subpackages: |
||||
- redis |
||||
- name: github.com/go-macaron/toolbox |
||||
version: 82b511550b0aefc36b3a28062ad3a52e812bee38 |
||||
- name: github.com/go-sql-driver/mysql |
||||
version: 66312f7fe2678aa0f5ec770f96702f4c4ec5aa8e |
||||
- name: github.com/go-xorm/core |
||||
version: 502158401cde814951eae62f064d9e5ff39e13ce |
||||
- name: github.com/go-xorm/xorm |
||||
version: 769f6b3ae663248e8f1b1d8fecbe1eb26ac77ac7 |
||||
- name: github.com/gogits/chardet |
||||
version: 2404f777256163ea3eadb273dada5dcb037993c0 |
||||
- name: github.com/gogits/cron |
||||
version: 3abc0f88f2062336bcc41b43a4febbd847a390ce |
||||
- name: github.com/gogits/git-module |
||||
version: 76e8cce6c7ef3ba1cf75752261c721ebf14cd129 |
||||
- name: github.com/gogits/go-gogs-client |
||||
version: 788ec59749df076b98e208909b44fdef02779deb |
||||
- name: github.com/issue9/identicon |
||||
version: f8c0d2ce04db79c663b1da33d3a9f62be753ee88 |
||||
- name: github.com/kardianos/minwinsvc |
||||
version: cad6b2b879b0970e4245a20ebf1a81a756e2bb70 |
||||
- name: github.com/klauspost/compress |
||||
version: 006acde2c5d283d2f8b8aa03d8f0cd2891c680cf |
||||
subpackages: |
||||
- gzip |
||||
- flate |
||||
- name: github.com/klauspost/cpuid |
||||
version: 09cded8978dc9e80714c4d85b0322337b0a1e5e0 |
||||
- name: github.com/klauspost/crc32 |
||||
version: 19b0b332c9e4516a6370a0456e6182c3b5036720 |
||||
- name: github.com/lib/pq |
||||
version: 165a3529e799da61ab10faed1fabff3662d6193f |
||||
subpackages: |
||||
- oid |
||||
- name: github.com/mattn/go-sqlite3 |
||||
version: 76e335f60bbcee20755df9864f0153af1a80ad2d |
||||
- name: github.com/mcuadros/go-version |
||||
version: d52711f8d6bea8dc01efafdb68ad95a4e2606630 |
||||
- name: github.com/microcosm-cc/bluemonday |
||||
version: 4ac6f27528d0a3f2a59e0b0a6f6b3ff0bb89fe20 |
||||
- name: github.com/msteinert/pam |
||||
version: 02ccfbfaf0cc627aa3aec8ef7ed5cfeec5b43f63 |
||||
- name: github.com/nfnt/resize |
||||
version: 4d93a29130b1b6aba503e2aa8b50f516213ea80e |
||||
- name: github.com/russross/blackfriday |
||||
version: b43df972fb5fdf3af8d2e90f38a69d374fe26dd0 |
||||
- name: github.com/satori/go.uuid |
||||
version: e673fdd4dea8a7334adbbe7f57b7e4b00bdc5502 |
||||
- name: github.com/sergi/go-diff |
||||
version: ec7fdbb58eb3e300c8595ad5ac74a5aa50019cc7 |
||||
subpackages: |
||||
- diffmatchpatch |
||||
- name: github.com/shurcooL/sanitized_anchor_name |
||||
version: 10ef21a441db47d8b13ebcc5fd2310f636973c77 |
||||
- name: github.com/Unknwon/cae |
||||
version: 7f5e046bc8a6c3cde743c233b96ee4fd84ee6ecd |
||||
subpackages: |
||||
- zip |
||||
- name: github.com/Unknwon/com |
||||
version: 28b053d5a2923b87ce8c5a08f3af779894a72758 |
||||
- name: github.com/Unknwon/i18n |
||||
version: 3b48b6662051bed72d36efa3c1e897bdf96b2e37 |
||||
- name: github.com/Unknwon/paginater |
||||
version: 7748a72e01415173a27d79866b984328e7b0c12b |
||||
- name: golang.org/x/crypto |
||||
version: c197bcf24cde29d3f73c7b4ac6fd41f4384e8af6 |
||||
subpackages: |
||||
- ssh |
||||
- curve25519 |
||||
- name: golang.org/x/net |
||||
version: 35b06af0720201bc2f326773a80767387544f8c4 |
||||
subpackages: |
||||
- html |
||||
- html/charset |
||||
- html/atom |
||||
- name: golang.org/x/sys |
||||
version: 9d4e42a20653790449273b3c85e67d6d8bae6e2e |
||||
subpackages: |
||||
- windows/svc |
||||
- windows |
||||
- name: golang.org/x/text |
||||
version: 1b466db55e0ba5d56ef5315c728216b42f796491 |
||||
subpackages: |
||||
- transform |
||||
- encoding |
||||
- encoding/charmap |
||||
- encoding/htmlindex |
||||
- encoding/internal/identifier |
||||
- encoding/internal |
||||
- encoding/japanese |
||||
- encoding/korean |
||||
- encoding/simplifiedchinese |
||||
- encoding/traditionalchinese |
||||
- encoding/unicode |
||||
- language |
||||
- internal/utf8internal |
||||
- runes |
||||
- internal/tag |
||||
- name: gopkg.in/alexcesaro/quotedprintable.v3 |
||||
version: 2caba252f4dc53eaf6b553000885530023f54623 |
||||
- name: gopkg.in/asn1-ber.v1 |
||||
version: 4e86f4367175e39f69d9358a5f17b4dda270378d |
||||
- name: gopkg.in/bufio.v1 |
||||
version: 567b2bfa514e796916c4747494d6ff5132a1dfce |
||||
- name: gopkg.in/gomail.v2 |
||||
version: 060a5f4e98dbf37408cf0c745681e4001d877827 |
||||
- name: gopkg.in/ini.v1 |
||||
version: 776aa739ce9373377cd16f526cdf06cb4c89b40f |
||||
- name: gopkg.in/ldap.v2 |
||||
version: 07a7330929b9ee80495c88a4439657d89c7dbd87 |
||||
- name: gopkg.in/macaron.v1 |
||||
version: 94a5ef7105036242f79e5e07a8eb8651d06c8533 |
||||
- name: gopkg.in/redis.v2 |
||||
version: e6179049628164864e6e84e973cfb56335748dea |
||||
devImports: [] |
@ -0,0 +1,56 @@
|
||||
package: github.com/gogits/gogs |
||||
import: |
||||
- package: github.com/Unknwon/cae |
||||
subpackages: |
||||
- zip |
||||
- package: github.com/Unknwon/com |
||||
- package: github.com/Unknwon/i18n |
||||
- package: github.com/Unknwon/paginater |
||||
- package: github.com/codegangsta/cli |
||||
- package: github.com/go-macaron/binding |
||||
- package: github.com/go-macaron/cache |
||||
subpackages: |
||||
- memcache |
||||
- redis |
||||
- package: github.com/go-macaron/captcha |
||||
- package: github.com/go-macaron/csrf |
||||
- package: github.com/go-macaron/gzip |
||||
- package: github.com/go-macaron/i18n |
||||
- package: github.com/go-macaron/session |
||||
subpackages: |
||||
- redis |
||||
- package: github.com/go-macaron/toolbox |
||||
- package: github.com/go-sql-driver/mysql |
||||
- package: github.com/go-xorm/core |
||||
- package: github.com/go-xorm/xorm |
||||
- package: github.com/gogits/chardet |
||||
- package: github.com/gogits/cron |
||||
- package: github.com/gogits/git-module |
||||
- package: github.com/gogits/go-gogs-client |
||||
- package: github.com/issue9/identicon |
||||
- package: github.com/kardianos/minwinsvc |
||||
- package: github.com/lib/pq |
||||
- package: github.com/mattn/go-sqlite3 |
||||
- package: github.com/mcuadros/go-version |
||||
- package: github.com/microcosm-cc/bluemonday |
||||
- package: github.com/msteinert/pam |
||||
- package: github.com/nfnt/resize |
||||
- package: github.com/russross/blackfriday |
||||
- package: github.com/satori/go.uuid |
||||
- package: github.com/sergi/go-diff |
||||
subpackages: |
||||
- diffmatchpatch |
||||
- package: golang.org/x/crypto |
||||
subpackages: |
||||
- ssh |
||||
- package: golang.org/x/net |
||||
subpackages: |
||||
- html |
||||
- html/charset |
||||
- package: golang.org/x/text |
||||
subpackages: |
||||
- transform |
||||
- package: gopkg.in/gomail.v2 |
||||
- package: gopkg.in/ini.v1 |
||||
- package: gopkg.in/ldap.v2 |
||||
- package: gopkg.in/macaron.v1 |
@ -1,59 +0,0 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cron |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
"github.com/gogits/gogs/models" |
||||
"github.com/gogits/gogs/modules/cron" |
||||
"github.com/gogits/gogs/modules/log" |
||||
"github.com/gogits/gogs/modules/setting" |
||||
) |
||||
|
||||
var c = cron.New() |
||||
|
||||
func NewContext() { |
||||
var ( |
||||
entry *cron.Entry |
||||
err error |
||||
) |
||||
if setting.Cron.UpdateMirror.Enabled { |
||||
entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, models.MirrorUpdate) |
||||
if err != nil { |
||||
log.Fatal(4, "Cron[Update mirrors]: %v", err) |
||||
} |
||||
if setting.Cron.UpdateMirror.RunAtStart { |
||||
entry.Prev = time.Now() |
||||
go models.MirrorUpdate() |
||||
} |
||||
} |
||||
if setting.Cron.RepoHealthCheck.Enabled { |
||||
entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, models.GitFsck) |
||||
if err != nil { |
||||
log.Fatal(4, "Cron[Repository health check]: %v", err) |
||||
} |
||||
if setting.Cron.RepoHealthCheck.RunAtStart { |
||||
entry.Prev = time.Now() |
||||
go models.GitFsck() |
||||
} |
||||
} |
||||
if setting.Cron.CheckRepoStats.Enabled { |
||||
entry, err = c.AddFunc("Check repository statistics", setting.Cron.CheckRepoStats.Schedule, models.CheckRepoStats) |
||||
if err != nil { |
||||
log.Fatal(4, "Cron[Check repository statistics]: %v", err) |
||||
} |
||||
if setting.Cron.CheckRepoStats.RunAtStart { |
||||
entry.Prev = time.Now() |
||||
go models.CheckRepoStats() |
||||
} |
||||
} |
||||
c.Start() |
||||
} |
||||
|
||||
// ListTasks returns all running cron tasks.
|
||||
func ListTasks() []*cron.Entry { |
||||
return c.Entries() |
||||
} |
@ -0,0 +1,320 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/Unknwon/com" |
||||
"github.com/go-xorm/xorm" |
||||
|
||||
"github.com/gogits/gogs/modules/log" |
||||
) |
||||
|
||||
// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
|
||||
type CommentType int |
||||
|
||||
const ( |
||||
// Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
|
||||
COMMENT_TYPE_COMMENT CommentType = iota |
||||
COMMENT_TYPE_REOPEN |
||||
COMMENT_TYPE_CLOSE |
||||
|
||||
// References.
|
||||
COMMENT_TYPE_ISSUE_REF |
||||
// Reference from a commit (not part of a pull request)
|
||||
COMMENT_TYPE_COMMIT_REF |
||||
// Reference from a comment
|
||||
COMMENT_TYPE_COMMENT_REF |
||||
// Reference from a pull request
|
||||
COMMENT_TYPE_PULL_REF |
||||
) |
||||
|
||||
type CommentTag int |
||||
|
||||
const ( |
||||
COMMENT_TAG_NONE CommentTag = iota |
||||
COMMENT_TAG_POSTER |
||||
COMMENT_TAG_WRITER |
||||
COMMENT_TAG_OWNER |
||||
) |
||||
|
||||
// Comment represents a comment in commit and issue page.
|
||||
type Comment struct { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
Type CommentType |
||||
PosterID int64 |
||||
Poster *User `xorm:"-"` |
||||
IssueID int64 `xorm:"INDEX"` |
||||
CommitID int64 |
||||
Line int64 |
||||
Content string `xorm:"TEXT"` |
||||
RenderedContent string `xorm:"-"` |
||||
|
||||
Created time.Time `xorm:"-"` |
||||
CreatedUnix int64 |
||||
|
||||
// Reference issue in commit message
|
||||
CommitSHA string `xorm:"VARCHAR(40)"` |
||||
|
||||
Attachments []*Attachment `xorm:"-"` |
||||
|
||||
// For view issue page.
|
||||
ShowTag CommentTag `xorm:"-"` |
||||
} |
||||
|
||||
func (c *Comment) BeforeInsert() { |
||||
c.CreatedUnix = time.Now().UTC().Unix() |
||||
} |
||||
|
||||
func (c *Comment) AfterSet(colName string, _ xorm.Cell) { |
||||
var err error |
||||
switch colName { |
||||
case "id": |
||||
c.Attachments, err = GetAttachmentsByCommentID(c.ID) |
||||
if err != nil { |
||||
log.Error(3, "GetAttachmentsByCommentID[%d]: %v", c.ID, err) |
||||
} |
||||
|
||||
case "poster_id": |
||||
c.Poster, err = GetUserByID(c.PosterID) |
||||
if err != nil { |
||||
if IsErrUserNotExist(err) { |
||||
c.PosterID = -1 |
||||
c.Poster = NewFakeUser() |
||||
} else { |
||||
log.Error(3, "GetUserByID[%d]: %v", c.ID, err) |
||||
} |
||||
} |
||||
case "created_unix": |
||||
c.Created = time.Unix(c.CreatedUnix, 0).Local() |
||||
} |
||||
} |
||||
|
||||
func (c *Comment) AfterDelete() { |
||||
_, err := DeleteAttachmentsByComment(c.ID, true) |
||||
|
||||
if err != nil { |
||||
log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) |
||||
} |
||||
} |
||||
|
||||
// HashTag returns unique hash tag for comment.
|
||||
func (c *Comment) HashTag() string { |
||||
return "issuecomment-" + com.ToStr(c.ID) |
||||
} |
||||
|
||||
// EventTag returns unique event hash tag for comment.
|
||||
func (c *Comment) EventTag() string { |
||||
return "event-" + com.ToStr(c.ID) |
||||
} |
||||
|
||||
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) { |
||||
comment := &Comment{ |
||||
Type: opts.Type, |
||||
PosterID: opts.Doer.Id, |
||||
IssueID: opts.Issue.ID, |
||||
CommitID: opts.CommitID, |
||||
CommitSHA: opts.CommitSHA, |
||||
Line: opts.LineNum, |
||||
Content: opts.Content, |
||||
} |
||||
if _, err = e.Insert(comment); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// Compose comment action, could be plain comment, close or reopen issue/pull request.
|
||||
// This object will be used to notify watchers in the end of function.
|
||||
act := &Action{ |
||||
ActUserID: opts.Doer.Id, |
||||
ActUserName: opts.Doer.Name, |
||||
ActEmail: opts.Doer.Email, |
||||
Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]), |
||||
RepoID: opts.Repo.ID, |
||||
RepoUserName: opts.Repo.Owner.Name, |
||||
RepoName: opts.Repo.Name, |
||||
IsPrivate: opts.Repo.IsPrivate, |
||||
} |
||||
|
||||
// Check comment type.
|
||||
switch opts.Type { |
||||
case COMMENT_TYPE_COMMENT: |
||||
act.OpType = ACTION_COMMENT_ISSUE |
||||
|
||||
if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// Check attachments
|
||||
attachments := make([]*Attachment, 0, len(opts.Attachments)) |
||||
for _, uuid := range opts.Attachments { |
||||
attach, err := getAttachmentByUUID(e, uuid) |
||||
if err != nil { |
||||
if IsErrAttachmentNotExist(err) { |
||||
continue |
||||
} |
||||
return nil, fmt.Errorf("getAttachmentByUUID[%s]: %v", uuid, err) |
||||
} |
||||
attachments = append(attachments, attach) |
||||
} |
||||
|
||||
for i := range attachments { |
||||
attachments[i].IssueID = opts.Issue.ID |
||||
attachments[i].CommentID = comment.ID |
||||
// No assign value could be 0, so ignore AllCols().
|
||||
if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil { |
||||
return nil, fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err) |
||||
} |
||||
} |
||||
|
||||
case COMMENT_TYPE_REOPEN: |
||||
act.OpType = ACTION_REOPEN_ISSUE |
||||
if opts.Issue.IsPull { |
||||
act.OpType = ACTION_REOPEN_PULL_REQUEST |
||||
} |
||||
|
||||
if opts.Issue.IsPull { |
||||
_, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls-1 WHERE id=?", opts.Repo.ID) |
||||
} else { |
||||
_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID) |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
case COMMENT_TYPE_CLOSE: |
||||
act.OpType = ACTION_CLOSE_ISSUE |
||||
if opts.Issue.IsPull { |
||||
act.OpType = ACTION_CLOSE_PULL_REQUEST |
||||
} |
||||
|
||||
if opts.Issue.IsPull { |
||||
_, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls+1 WHERE id=?", opts.Repo.ID) |
||||
} else { |
||||
_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID) |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
// Notify watchers for whatever action comes in, ignore if no action type
|
||||
if act.OpType > 0 { |
||||
if err = notifyWatchers(e, act); err != nil { |
||||
return nil, fmt.Errorf("notifyWatchers: %v", err) |
||||
} |
||||
} |
||||
|
||||
return comment, nil |
||||
} |
||||
|
||||
func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) { |
||||
cmtType := COMMENT_TYPE_CLOSE |
||||
if !issue.IsClosed { |
||||
cmtType = COMMENT_TYPE_REOPEN |
||||
} |
||||
return createComment(e, &CreateCommentOptions{ |
||||
Type: cmtType, |
||||
Doer: doer, |
||||
Repo: repo, |
||||
Issue: issue, |
||||
}) |
||||
} |
||||
|
||||
type CreateCommentOptions struct { |
||||
Type CommentType |
||||
Doer *User |
||||
Repo *Repository |
||||
Issue *Issue |
||||
|
||||
CommitID int64 |
||||
CommitSHA string |
||||
LineNum int64 |
||||
Content string |
||||
Attachments []string // UUIDs of attachments
|
||||
} |
||||
|
||||
// CreateComment creates comment of issue or commit.
|
||||
func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) { |
||||
sess := x.NewSession() |
||||
defer sessionRelease(sess) |
||||
if err = sess.Begin(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
comment, err = createComment(sess, opts) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return comment, sess.Commit() |
||||
} |
||||
|
||||
// CreateIssueComment creates a plain issue comment.
|
||||
func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) { |
||||
return CreateComment(&CreateCommentOptions{ |
||||
Type: COMMENT_TYPE_COMMENT, |
||||
Doer: doer, |
||||
Repo: repo, |
||||
Issue: issue, |
||||
Content: content, |
||||
Attachments: attachments, |
||||
}) |
||||
} |
||||
|
||||
// CreateRefComment creates a commit reference comment to issue.
|
||||
func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error { |
||||
if len(commitSHA) == 0 { |
||||
return fmt.Errorf("cannot create reference with empty commit SHA") |
||||
} |
||||
|
||||
// Check if same reference from same commit has already existed.
|
||||
has, err := x.Get(&Comment{ |
||||
Type: COMMENT_TYPE_COMMIT_REF, |
||||
IssueID: issue.ID, |
||||
CommitSHA: commitSHA, |
||||
}) |
||||
if err != nil { |
||||
return fmt.Errorf("check reference comment: %v", err) |
||||
} else if has { |
||||
return nil |
||||
} |
||||
|
||||
_, err = CreateComment(&CreateCommentOptions{ |
||||
Type: COMMENT_TYPE_COMMIT_REF, |
||||
Doer: doer, |
||||
Repo: repo, |
||||
Issue: issue, |
||||
CommitSHA: commitSHA, |
||||
Content: content, |
||||
}) |
||||
return err |
||||
} |
||||
|
||||
// GetCommentByID returns the comment by given ID.
|
||||
func GetCommentByID(id int64) (*Comment, error) { |
||||
c := new(Comment) |
||||
has, err := x.Id(id).Get(c) |
||||
if err != nil { |
||||
return nil, err |
||||
} else if !has { |
||||
return nil, ErrCommentNotExist{id} |
||||
} |
||||
return c, nil |
||||
} |
||||
|
||||
// GetCommentsByIssueID returns all comments of issue by given ID.
|
||||
func GetCommentsByIssueID(issueID int64) ([]*Comment, error) { |
||||
comments := make([]*Comment, 0, 10) |
||||
return comments, x.Where("issue_id=?", issueID).Asc("created_unix").Find(&comments) |
||||
} |
||||
|
||||
// UpdateComment updates information of comment.
|
||||
func UpdateComment(c *Comment) error { |
||||
_, err := x.Id(c.ID).AllCols().Update(c) |
||||
return err |
||||
} |
@ -0,0 +1,234 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models |
||||
|
||||
import ( |
||||
"fmt" |
||||
"html/template" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/go-xorm/xorm" |
||||
) |
||||
|
||||
// Label represents a label of repository for issues.
|
||||
type Label struct { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
RepoID int64 `xorm:"INDEX"` |
||||
Name string |
||||
Color string `xorm:"VARCHAR(7)"` |
||||
NumIssues int |
||||
NumClosedIssues int |
||||
NumOpenIssues int `xorm:"-"` |
||||
IsChecked bool `xorm:"-"` |
||||
} |
||||
|
||||
// CalOpenIssues calculates the open issues of label.
|
||||
func (m *Label) CalOpenIssues() { |
||||
m.NumOpenIssues = m.NumIssues - m.NumClosedIssues |
||||
} |
||||
|
||||
// ForegroundColor calculates the text color for labels based
|
||||
// on their background color.
|
||||
func (l *Label) ForegroundColor() template.CSS { |
||||
if strings.HasPrefix(l.Color, "#") { |
||||
if color, err := strconv.ParseUint(l.Color[1:], 16, 64); err == nil { |
||||
r := float32(0xFF & (color >> 16)) |
||||
g := float32(0xFF & (color >> 8)) |
||||
b := float32(0xFF & color) |
||||
luminance := (0.2126*r + 0.7152*g + 0.0722*b) / 255 |
||||
|
||||
if luminance < 0.5 { |
||||
return template.CSS("#fff") |
||||
} |
||||
} |
||||
} |
||||
|
||||
// default to black
|
||||
return template.CSS("#000") |
||||
} |
||||
|
||||
// NewLabel creates new label of repository.
|
||||
func NewLabel(l *Label) error { |
||||
_, err := x.Insert(l) |
||||
return err |
||||
} |
||||
|
||||
func getLabelByID(e Engine, id int64) (*Label, error) { |
||||
if id <= 0 { |
||||
return nil, ErrLabelNotExist{id} |
||||
} |
||||
|
||||
l := &Label{ID: id} |
||||
has, err := x.Get(l) |
||||
if err != nil { |
||||
return nil, err |
||||
} else if !has { |
||||
return nil, ErrLabelNotExist{l.ID} |
||||
} |
||||
return l, nil |
||||
} |
||||
|
||||
// GetLabelByID returns a label by given ID.
|
||||
func GetLabelByID(id int64) (*Label, error) { |
||||
return getLabelByID(x, id) |
||||
} |
||||
|
||||
// GetLabelsByRepoID returns all labels that belong to given repository by ID.
|
||||
func GetLabelsByRepoID(repoID int64) ([]*Label, error) { |
||||
labels := make([]*Label, 0, 10) |
||||
return labels, x.Where("repo_id=?", repoID).Find(&labels) |
||||
} |
||||
|
||||
func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) { |
||||
issueLabels, err := getIssueLabels(e, issueID) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("getIssueLabels: %v", err) |
||||
} |
||||
|
||||
var label *Label |
||||
labels := make([]*Label, 0, len(issueLabels)) |
||||
for idx := range issueLabels { |
||||
label, err = getLabelByID(e, issueLabels[idx].LabelID) |
||||
if err != nil && !IsErrLabelNotExist(err) { |
||||
return nil, fmt.Errorf("getLabelByID: %v", err) |
||||
} |
||||
labels = append(labels, label) |
||||
} |
||||
return labels, nil |
||||
} |
||||
|
||||
// GetLabelsByIssueID returns all labels that belong to given issue by ID.
|
||||
func GetLabelsByIssueID(issueID int64) ([]*Label, error) { |
||||
return getLabelsByIssueID(x, issueID) |
||||
} |
||||
|
||||
func updateLabel(e Engine, l *Label) error { |
||||
_, err := e.Id(l.ID).AllCols().Update(l) |
||||
return err |
||||
} |
||||
|
||||
// UpdateLabel updates label information.
|
||||
func UpdateLabel(l *Label) error { |
||||
return updateLabel(x, l) |
||||
} |
||||
|
||||
// DeleteLabel delete a label of given repository.
|
||||
func DeleteLabel(repoID, labelID int64) error { |
||||
l, err := GetLabelByID(labelID) |
||||
if err != nil { |
||||
if IsErrLabelNotExist(err) { |
||||
return nil |
||||
} |
||||
return err |
||||
} |
||||
|
||||
sess := x.NewSession() |
||||
defer sessionRelease(sess) |
||||
if err = sess.Begin(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err = x.Where("label_id=?", labelID).Delete(new(IssueLabel)); err != nil { |
||||
return err |
||||
} else if _, err = sess.Delete(l); err != nil { |
||||
return err |
||||
} |
||||
return sess.Commit() |
||||
} |
||||
|
||||
// .___ .____ ___. .__
|
||||
// | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
|
||||
// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
|
||||
// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
|
||||
// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
|
||||
// \/ \/ \/ \/ \/ \/ \/
|
||||
|
||||
// IssueLabel represetns an issue-lable relation.
|
||||
type IssueLabel struct { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
IssueID int64 `xorm:"UNIQUE(s)"` |
||||
LabelID int64 `xorm:"UNIQUE(s)"` |
||||
} |
||||
|
||||
func hasIssueLabel(e Engine, issueID, labelID int64) bool { |
||||
has, _ := e.Where("issue_id=? AND label_id=?", issueID, labelID).Get(new(IssueLabel)) |
||||
return has |
||||
} |
||||
|
||||
// HasIssueLabel returns true if issue has been labeled.
|
||||
func HasIssueLabel(issueID, labelID int64) bool { |
||||
return hasIssueLabel(x, issueID, labelID) |
||||
} |
||||
|
||||
func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) { |
||||
if _, err = e.Insert(&IssueLabel{ |
||||
IssueID: issue.ID, |
||||
LabelID: label.ID, |
||||
}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
label.NumIssues++ |
||||
if issue.IsClosed { |
||||
label.NumClosedIssues++ |
||||
} |
||||
return updateLabel(e, label) |
||||
} |
||||
|
||||
// NewIssueLabel creates a new issue-label relation.
|
||||
func NewIssueLabel(issue *Issue, label *Label) (err error) { |
||||
sess := x.NewSession() |
||||
defer sessionRelease(sess) |
||||
if err = sess.Begin(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = newIssueLabel(sess, issue, label); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return sess.Commit() |
||||
} |
||||
|
||||
func getIssueLabels(e Engine, issueID int64) ([]*IssueLabel, error) { |
||||
issueLabels := make([]*IssueLabel, 0, 10) |
||||
return issueLabels, e.Where("issue_id=?", issueID).Asc("label_id").Find(&issueLabels) |
||||
} |
||||
|
||||
// GetIssueLabels returns all issue-label relations of given issue by ID.
|
||||
func GetIssueLabels(issueID int64) ([]*IssueLabel, error) { |
||||
return getIssueLabels(x, issueID) |
||||
} |
||||
|
||||
func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) { |
||||
if _, err = e.Delete(&IssueLabel{ |
||||
IssueID: issue.ID, |
||||
LabelID: label.ID, |
||||
}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
label.NumIssues-- |
||||
if issue.IsClosed { |
||||
label.NumClosedIssues-- |
||||
} |
||||
return updateLabel(e, label) |
||||
} |
||||
|
||||
// DeleteIssueLabel deletes issue-label relation.
|
||||
func DeleteIssueLabel(issue *Issue, label *Label) (err error) { |
||||
sess := x.NewSession() |
||||
defer sessionRelease(sess) |
||||
if err = sess.Begin(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = deleteIssueLabel(sess, issue, label); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return sess.Commit() |
||||
} |
@ -0,0 +1,57 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models |
||||
|
||||
import ( |
||||
"github.com/gogits/git-module" |
||||
) |
||||
|
||||
type Branch struct { |
||||
Path string |
||||
Name string |
||||
} |
||||
|
||||
func GetBranchesByPath(path string) ([]*Branch, error) { |
||||
gitRepo, err := git.OpenRepository(path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
brs, err := gitRepo.GetBranches() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
branches := make([]*Branch, len(brs)) |
||||
for i := range brs { |
||||
branches[i] = &Branch{ |
||||
Path: path, |
||||
Name: brs[i], |
||||
} |
||||
} |
||||
return branches, nil |
||||
} |
||||
|
||||
func (repo *Repository) GetBranch(br string) (*Branch, error) { |
||||
if !git.IsBranchExist(repo.RepoPath(), br) { |
||||
return nil, &ErrBranchNotExist{br} |
||||
} |
||||
return &Branch{ |
||||
Path: repo.RepoPath(), |
||||
Name: br, |
||||
}, nil |
||||
} |
||||
|
||||
func (repo *Repository) GetBranches() ([]*Branch, error) { |
||||
return GetBranchesByPath(repo.RepoPath()) |
||||
} |
||||
|
||||
func (br *Branch) GetCommit() (*git.Commit, error) { |
||||
gitRepo, err := git.OpenRepository(br.Path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return gitRepo.GetBranchCommit(br.Name) |
||||
} |
@ -0,0 +1,159 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
// Collaboration represent the relation between an individual and a repository.
|
||||
type Collaboration struct { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` |
||||
UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` |
||||
Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"` |
||||
} |
||||
|
||||
func (c *Collaboration) ModeName() string { |
||||
switch c.Mode { |
||||
case ACCESS_MODE_READ: |
||||
return "Read" |
||||
case ACCESS_MODE_WRITE: |
||||
return "Write" |
||||
case ACCESS_MODE_ADMIN: |
||||
return "Admin" |
||||
} |
||||
return "Undefined" |
||||
} |
||||
|
||||
// AddCollaborator adds new collaboration relation between an individual and a repository.
|
||||
func (repo *Repository) AddCollaborator(u *User) error { |
||||
collaboration := &Collaboration{ |
||||
RepoID: repo.ID, |
||||
UserID: u.Id, |
||||
} |
||||
|
||||
has, err := x.Get(collaboration) |
||||
if err != nil { |
||||
return err |
||||
} else if has { |
||||
return nil |
||||
} |
||||
collaboration.Mode = ACCESS_MODE_WRITE |
||||
|
||||
sess := x.NewSession() |
||||
defer sessionRelease(sess) |
||||
if err = sess.Begin(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err = sess.InsertOne(collaboration); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if repo.Owner.IsOrganization() { |
||||
err = repo.recalculateTeamAccesses(sess, 0) |
||||
} else { |
||||
err = repo.recalculateAccesses(sess) |
||||
} |
||||
if err != nil { |
||||
return fmt.Errorf("recalculateAccesses 'team=%v': %v", repo.Owner.IsOrganization(), err) |
||||
} |
||||
|
||||
return sess.Commit() |
||||
} |
||||
|
||||
func (repo *Repository) getCollaborations(e Engine) ([]*Collaboration, error) { |
||||
collaborations := make([]*Collaboration, 0) |
||||
return collaborations, e.Find(&collaborations, &Collaboration{RepoID: repo.ID}) |
||||
} |
||||
|
||||
// Collaborator represents a user with collaboration details.
|
||||
type Collaborator struct { |
||||
*User |
||||
Collaboration *Collaboration |
||||
} |
||||
|
||||
func (repo *Repository) getCollaborators(e Engine) ([]*Collaborator, error) { |
||||
collaborations, err := repo.getCollaborations(e) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("getCollaborations: %v", err) |
||||
} |
||||
|
||||
collaborators := make([]*Collaborator, len(collaborations)) |
||||
for i, c := range collaborations { |
||||
user, err := getUserByID(e, c.UserID) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
collaborators[i] = &Collaborator{ |
||||
User: user, |
||||
Collaboration: c, |
||||
} |
||||
} |
||||
return collaborators, nil |
||||
} |
||||
|
||||
// GetCollaborators returns the collaborators for a repository
|
||||
func (repo *Repository) GetCollaborators() ([]*Collaborator, error) { |
||||
return repo.getCollaborators(x) |
||||
} |
||||
|
||||
// ChangeCollaborationAccessMode sets new access mode for the collaboration.
|
||||
func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode) error { |
||||
// Discard invalid input
|
||||
if mode <= ACCESS_MODE_NONE || mode > ACCESS_MODE_OWNER { |
||||
return nil |
||||
} |
||||
|
||||
collaboration := &Collaboration{ |
||||
RepoID: repo.ID, |
||||
UserID: uid, |
||||
} |
||||
has, err := x.Get(collaboration) |
||||
if err != nil { |
||||
return fmt.Errorf("get collaboration: %v", err) |
||||
} else if !has { |
||||
return nil |
||||
} |
||||
|
||||
collaboration.Mode = mode |
||||
|
||||
sess := x.NewSession() |
||||
defer sessionRelease(sess) |
||||
if err = sess.Begin(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err = sess.Id(collaboration.ID).AllCols().Update(collaboration); err != nil { |
||||
return fmt.Errorf("update collaboration: %v", err) |
||||
} else if _, err = sess.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil { |
||||
return fmt.Errorf("update access table: %v", err) |
||||
} |
||||
|
||||
return sess.Commit() |
||||
} |
||||
|
||||
// DeleteCollaboration removes collaboration relation between the user and repository.
|
||||
func (repo *Repository) DeleteCollaboration(uid int64) (err error) { |
||||
collaboration := &Collaboration{ |
||||
RepoID: repo.ID, |
||||
UserID: uid, |
||||
} |
||||
|
||||
sess := x.NewSession() |
||||
defer sessionRelease(sess) |
||||
if err = sess.Begin(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if has, err := sess.Delete(collaboration); err != nil || has == 0 { |
||||
return err |
||||
} else if err = repo.recalculateAccesses(sess); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return sess.Commit() |
||||
} |
@ -0,0 +1,45 @@
|
||||
package models |
||||
|
||||
import ( |
||||
"fmt" |
||||
"testing" |
||||
|
||||
. "github.com/smartystreets/goconvey/convey" |
||||
|
||||
"github.com/gogits/gogs/modules/setting" |
||||
) |
||||
|
||||
func init() { |
||||
setting.NewContext() |
||||
} |
||||
|
||||
func Test_SSHParsePublicKey(t *testing.T) { |
||||
testKeys := map[string]struct { |
||||
typeName string |
||||
length int |
||||
content string |
||||
}{ |
||||
"dsa-1024": {"dsa", 1024, "ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment"}, |
||||
"rsa-1024": {"rsa", 1024, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n"}, |
||||
"rsa-2048": {"rsa", 2048, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMZXh+1OBUwSH9D45wTaxErQIN9IoC9xl7MKJkqvTvv6O5RR9YW/IK9FbfjXgXsppYGhsCZo1hFOOsXHMnfOORqu/xMDx4yPuyvKpw4LePEcg4TDipaDFuxbWOqc/BUZRZcXu41QAWfDLrInwsltWZHSeG7hjhpacl4FrVv9V1pS6Oc5Q1NxxEzTzuNLS/8diZrTm/YAQQ/+B+mzWI3zEtF4miZjjAljWd1LTBPvU23d29DcBmmFahcZ441XZsTeAwGxG/Q6j8NgNXj9WxMeWwxXV2jeAX/EBSpZrCVlCQ1yJswT6xCp8TuBnTiGWYMBNTbOZvPC4e0WI2/yZW/s5F nocomment"}, |
||||
"ecdsa-256": {"ecdsa", 256, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFQacN3PrOll7PXmN5B/ZNVahiUIqI05nbBlZk1KXsO3d06ktAWqbNflv2vEmA38bTFTfJ2sbn2B5ksT52cDDbA= nocomment"}, |
||||
"ecdsa-384": {"ecdsa", 384, "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBINmioV+XRX1Fm9Qk2ehHXJ2tfVxW30ypUWZw670Zyq5GQfBAH6xjygRsJ5wWsHXBsGYgFUXIHvMKVAG1tpw7s6ax9oA+dJOJ7tj+vhn8joFqT+sg3LYHgZkHrfqryRasQ== nocomment"}, |
||||
"ecdsa-521": {"ecdsa", 521, "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACGt3UG3EzRwNOI17QR84l6PgiAcvCE7v6aXPj/SC6UWKg4EL8vW9ZBcdYL9wzs4FZXh4MOV8jAzu3KRWNTwb4k2wFNUpGOt7l28MztFFEtH5BDDrtAJSPENPy8pvPLMfnPg5NhvWycqIBzNcHipem5wSJFN5PdpNOC2xMrPWKNqj+ZjQ== nocomment"}, |
||||
} |
||||
|
||||
Convey("Parse public keys in both native and ssh-keygen", t, func() { |
||||
for name, key := range testKeys { |
||||
fmt.Println("\nTesting key:", name) |
||||
|
||||
keyTypeN, lengthN, errN := SSHNativeParsePublicKey(key.content) |
||||
So(errN, ShouldBeNil) |
||||
So(keyTypeN, ShouldEqual, key.typeName) |
||||
So(lengthN, ShouldEqual, key.length) |
||||
|
||||
keyTypeK, lengthK, errK := SSHKeyGenParsePublicKey(key.content) |
||||
So(errK, ShouldBeNil) |
||||
So(keyTypeK, ShouldEqual, key.typeName) |
||||
So(lengthK, ShouldEqual, key.length) |
||||
} |
||||
}) |
||||
} |
@ -1,61 +1,23 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2016 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 avatar_test |
||||
|
||||
package avatar |
||||
|
||||
import ( |
||||
"errors" |
||||
"os" |
||||
"strconv" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/gogits/gogs/modules/avatar" |
||||
"github.com/gogits/gogs/modules/log" |
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
const TMPDIR = "test-avatar" |
||||
|
||||
func TestFetch(t *testing.T) { |
||||
os.Mkdir(TMPDIR, 0755) |
||||
defer os.RemoveAll(TMPDIR) |
||||
|
||||
hash := avatar.HashEmail("ssx205@gmail.com") |
||||
a := avatar.New(hash, TMPDIR) |
||||
a.UpdateTimeout(time.Millisecond * 200) |
||||
} |
||||
|
||||
func TestFetchMany(t *testing.T) { |
||||
os.Mkdir(TMPDIR, 0755) |
||||
defer os.RemoveAll(TMPDIR) |
||||
|
||||
t.Log("start") |
||||
var n = 5 |
||||
ch := make(chan bool, n) |
||||
for i := 0; i < n; i++ { |
||||
go func(i int) { |
||||
hash := avatar.HashEmail(strconv.Itoa(i) + "ssx205@gmail.com") |
||||
a := avatar.New(hash, TMPDIR) |
||||
a.Update() |
||||
t.Log("finish", hash) |
||||
ch <- true |
||||
}(i) |
||||
} |
||||
for i := 0; i < n; i++ { |
||||
<-ch |
||||
} |
||||
t.Log("end") |
||||
} |
||||
|
||||
// cat
|
||||
// wget http://www.artsjournal.com/artfulmanager/wp/wp-content/uploads/2013/12/200x200xmirror_cat.jpg.pagespeed.ic.GOZSv6v1_H.jpg -O default.jpg
|
||||
/* |
||||
func TestHttp(t *testing.T) { |
||||
http.Handle("/", avatar.CacheServer("./", "default.jpg")) |
||||
http.ListenAndServe(":8001", nil) |
||||
} |
||||
*/ |
||||
func Test_RandomImage(t *testing.T) { |
||||
Convey("Generate a random avatar from email", t, func() { |
||||
_, err := RandomImage([]byte("gogs@local")) |
||||
So(err, ShouldBeNil) |
||||
|
||||
func TestLogTrace(t *testing.T) { |
||||
log.Trace("%v", errors.New("console log test")) |
||||
Convey("Try to generate an image with size zero", func() { |
||||
_, err := RandomImageSize(0, []byte("gogs@local")) |
||||
So(err, ShouldNotBeNil) |
||||
}) |
||||
}) |
||||
} |
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,71 @@
|
||||
// Copyright 2016 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 context |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"github.com/Unknwon/paginater" |
||||
"gopkg.in/macaron.v1" |
||||
|
||||
"github.com/gogits/gogs/modules/base" |
||||
"github.com/gogits/gogs/modules/log" |
||||
"github.com/gogits/gogs/modules/setting" |
||||
) |
||||
|
||||
type APIContext struct { |
||||
*Context |
||||
} |
||||
|
||||
// Error responses error message to client with given message.
|
||||
// If status is 500, also it prints error to log.
|
||||
func (ctx *APIContext) Error(status int, title string, obj interface{}) { |
||||
var message string |
||||
if err, ok := obj.(error); ok { |
||||
message = err.Error() |
||||
} else { |
||||
message = obj.(string) |
||||
} |
||||
|
||||
if status == 500 { |
||||
log.Error(4, "%s: %s", title, message) |
||||
} |
||||
|
||||
ctx.JSON(status, map[string]string{ |
||||
"message": message, |
||||
"url": base.DOC_URL, |
||||
}) |
||||
} |
||||
|
||||
func (ctx *APIContext) SetLinkHeader(total, pageSize int) { |
||||
page := paginater.New(total, pageSize, ctx.QueryInt("page"), 0) |
||||
links := make([]string, 0, 4) |
||||
if page.HasNext() { |
||||
links = append(links, fmt.Sprintf("<%s%s?page=%d>; rel=\"next\"", setting.AppUrl, ctx.Req.URL.Path[1:], page.Next())) |
||||
} |
||||
if !page.IsLast() { |
||||
links = append(links, fmt.Sprintf("<%s%s?page=%d>; rel=\"last\"", setting.AppUrl, ctx.Req.URL.Path[1:], page.TotalPages())) |
||||
} |
||||
if !page.IsFirst() { |
||||
links = append(links, fmt.Sprintf("<%s%s?page=1>; rel=\"first\"", setting.AppUrl, ctx.Req.URL.Path[1:])) |
||||
} |
||||
if page.HasPrevious() { |
||||
links = append(links, fmt.Sprintf("<%s%s?page=%d>; rel=\"prev\"", setting.AppUrl, ctx.Req.URL.Path[1:], page.Previous())) |
||||
} |
||||
|
||||
if len(links) > 0 { |
||||
ctx.Header().Set("Link", strings.Join(links, ",")) |
||||
} |
||||
} |
||||
|
||||
func APIContexter() macaron.Handler { |
||||
return func(c *Context) { |
||||
ctx := &APIContext{ |
||||
Context: c, |
||||
} |
||||
c.Map(ctx) |
||||
} |
||||
} |
@ -0,0 +1,87 @@
|
||||
// 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 context |
||||
|
||||
import ( |
||||
"net/url" |
||||
|
||||
"github.com/go-macaron/csrf" |
||||
"gopkg.in/macaron.v1" |
||||
|
||||
"github.com/gogits/gogs/modules/auth" |
||||
"github.com/gogits/gogs/modules/setting" |
||||
) |
||||
|
||||
type ToggleOptions struct { |
||||
SignInRequired bool |
||||
SignOutRequired bool |
||||
AdminRequired bool |
||||
DisableCSRF bool |
||||
} |
||||
|
||||
func Toggle(options *ToggleOptions) macaron.Handler { |
||||
return func(ctx *Context) { |
||||
// Cannot view any page before installation.
|
||||
if !setting.InstallLock { |
||||
ctx.Redirect(setting.AppSubUrl + "/install") |
||||
return |
||||
} |
||||
|
||||
// Checking non-logged users landing page.
|
||||
if !ctx.IsSigned && ctx.Req.RequestURI == "/" && setting.LandingPageUrl != setting.LANDING_PAGE_HOME { |
||||
ctx.Redirect(setting.AppSubUrl + string(setting.LandingPageUrl)) |
||||
return |
||||
} |
||||
|
||||
// Redirect to dashboard if user tries to visit any non-login page.
|
||||
if options.SignOutRequired && ctx.IsSigned && ctx.Req.RequestURI != "/" { |
||||
ctx.Redirect(setting.AppSubUrl + "/") |
||||
return |
||||
} |
||||
|
||||
if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" && !auth.IsAPIPath(ctx.Req.URL.Path) { |
||||
csrf.Validate(ctx.Context, ctx.csrf) |
||||
if ctx.Written() { |
||||
return |
||||
} |
||||
} |
||||
|
||||
if options.SignInRequired { |
||||
if !ctx.IsSigned { |
||||
// Restrict API calls with error message.
|
||||
if auth.IsAPIPath(ctx.Req.URL.Path) { |
||||
ctx.JSON(403, map[string]string{ |
||||
"message": "Only signed in user is allowed to call APIs.", |
||||
}) |
||||
return |
||||
} |
||||
|
||||
ctx.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+ctx.Req.RequestURI), 0, setting.AppSubUrl) |
||||
ctx.Redirect(setting.AppSubUrl + "/user/login") |
||||
return |
||||
} else if !ctx.User.IsActive && setting.Service.RegisterEmailConfirm { |
||||
ctx.Data["Title"] = ctx.Tr("auth.active_your_account") |
||||
ctx.HTML(200, "user/auth/activate") |
||||
return |
||||
} |
||||
} |
||||
|
||||
// Redirect to log in page if auto-signin info is provided and has not signed in.
|
||||
if !options.SignOutRequired && !ctx.IsSigned && !auth.IsAPIPath(ctx.Req.URL.Path) && |
||||
len(ctx.GetCookie(setting.CookieUserName)) > 0 { |
||||
ctx.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+ctx.Req.RequestURI), 0, setting.AppSubUrl) |
||||
ctx.Redirect(setting.AppSubUrl + "/user/login") |
||||
return |
||||
} |
||||
|
||||
if options.AdminRequired { |
||||
if !ctx.User.IsAdmin { |
||||
ctx.Error(403) |
||||
return |
||||
} |
||||
ctx.Data["PageIsAdmin"] = true |
||||
} |
||||
} |
||||
} |
@ -1,27 +0,0 @@
|
||||
package cron |
||||
|
||||
import "time" |
||||
|
||||
// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
|
||||
// It does not support jobs more frequent than once a second.
|
||||
type ConstantDelaySchedule struct { |
||||
Delay time.Duration |
||||
} |
||||
|
||||
// Every returns a crontab Schedule that activates once every duration.
|
||||
// Delays of less than a second are not supported (will round up to 1 second).
|
||||
// Any fields less than a Second are truncated.
|
||||
func Every(duration time.Duration) ConstantDelaySchedule { |
||||
if duration < time.Second { |
||||
duration = time.Second |
||||
} |
||||
return ConstantDelaySchedule{ |
||||
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, |
||||
} |
||||
} |
||||
|
||||
// Next returns the next time this should be run.
|
||||
// This rounds so that the next activation time will be on the second.
|
||||
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { |
||||
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) |
||||
} |
@ -1,54 +0,0 @@
|
||||
package cron |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestConstantDelayNext(t *testing.T) { |
||||
tests := []struct { |
||||
time string |
||||
delay time.Duration |
||||
expected string |
||||
}{ |
||||
// Simple cases
|
||||
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, |
||||
{"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"}, |
||||
{"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"}, |
||||
|
||||
// Wrap around hours
|
||||
{"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"}, |
||||
|
||||
// Wrap around days
|
||||
{"Mon Jul 9 23:46 2012", 14 * time.Minute, "Tue Jul 10 00:00 2012"}, |
||||
{"Mon Jul 9 23:45 2012", 35 * time.Minute, "Tue Jul 10 00:20 2012"}, |
||||
{"Mon Jul 9 23:35:51 2012", 44*time.Minute + 24*time.Second, "Tue Jul 10 00:20:15 2012"}, |
||||
{"Mon Jul 9 23:35:51 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Thu Jul 11 01:20:15 2012"}, |
||||
|
||||
// Wrap around months
|
||||
{"Mon Jul 9 23:35 2012", 91*24*time.Hour + 25*time.Minute, "Thu Oct 9 00:00 2012"}, |
||||
|
||||
// Wrap around minute, hour, day, month, and year
|
||||
{"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"}, |
||||
|
||||
// Round to nearest second on the delay
|
||||
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, |
||||
|
||||
// Round up to 1 second if the duration is less.
|
||||
{"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:01 2012"}, |
||||
|
||||
// Round to nearest second when calculating the next time.
|
||||
{"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"}, |
||||
|
||||
// Round to nearest second for both.
|
||||
{"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, |
||||
} |
||||
|
||||
for _, c := range tests { |
||||
actual := Every(c.delay).Next(getTime(c.time)) |
||||
expected := getTime(c.expected) |
||||
if actual != expected { |
||||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual) |
||||
} |
||||
} |
||||
} |
@ -1,255 +0,0 @@
|
||||
package cron |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
// Many tests schedule a job for every second, and then wait at most a second
|
||||
// for it to run. This amount is just slightly larger than 1 second to
|
||||
// compensate for a few milliseconds of runtime.
|
||||
const ONE_SECOND = 1*time.Second + 10*time.Millisecond |
||||
|
||||
// Start and stop cron with no entries.
|
||||
func TestNoEntries(t *testing.T) { |
||||
cron := New() |
||||
cron.Start() |
||||
|
||||
select { |
||||
case <-time.After(ONE_SECOND): |
||||
t.FailNow() |
||||
case <-stop(cron): |
||||
} |
||||
} |
||||
|
||||
// Start, stop, then add an entry. Verify entry doesn't run.
|
||||
func TestStopCausesJobsToNotRun(t *testing.T) { |
||||
wg := &sync.WaitGroup{} |
||||
wg.Add(1) |
||||
|
||||
cron := New() |
||||
cron.Start() |
||||
cron.Stop() |
||||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) |
||||
|
||||
select { |
||||
case <-time.After(ONE_SECOND): |
||||
// No job ran!
|
||||
case <-wait(wg): |
||||
t.FailNow() |
||||
} |
||||
} |
||||
|
||||
// Add a job, start cron, expect it runs.
|
||||
func TestAddBeforeRunning(t *testing.T) { |
||||
wg := &sync.WaitGroup{} |
||||
wg.Add(1) |
||||
|
||||
cron := New() |
||||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) |
||||
cron.Start() |
||||
defer cron.Stop() |
||||
|
||||
// Give cron 2 seconds to run our job (which is always activated).
|
||||
select { |
||||
case <-time.After(ONE_SECOND): |
||||
t.FailNow() |
||||
case <-wait(wg): |
||||
} |
||||
} |
||||
|
||||
// Start cron, add a job, expect it runs.
|
||||
func TestAddWhileRunning(t *testing.T) { |
||||
wg := &sync.WaitGroup{} |
||||
wg.Add(1) |
||||
|
||||
cron := New() |
||||
cron.Start() |
||||
defer cron.Stop() |
||||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) |
||||
|
||||
select { |
||||
case <-time.After(ONE_SECOND): |
||||
t.FailNow() |
||||
case <-wait(wg): |
||||
} |
||||
} |
||||
|
||||
// Test timing with Entries.
|
||||
func TestSnapshotEntries(t *testing.T) { |
||||
wg := &sync.WaitGroup{} |
||||
wg.Add(1) |
||||
|
||||
cron := New() |
||||
cron.AddFunc("", "@every 2s", func() { wg.Done() }) |
||||
cron.Start() |
||||
defer cron.Stop() |
||||
|
||||
// Cron should fire in 2 seconds. After 1 second, call Entries.
|
||||
select { |
||||
case <-time.After(ONE_SECOND): |
||||
cron.Entries() |
||||
} |
||||
|
||||
// Even though Entries was called, the cron should fire at the 2 second mark.
|
||||
select { |
||||
case <-time.After(ONE_SECOND): |
||||
t.FailNow() |
||||
case <-wait(wg): |
||||
} |
||||
|
||||
} |
||||
|
||||
// Test that the entries are correctly sorted.
|
||||
// Add a bunch of long-in-the-future entries, and an immediate entry, and ensure
|
||||
// that the immediate entry runs immediately.
|
||||
// Also: Test that multiple jobs run in the same instant.
|
||||
func TestMultipleEntries(t *testing.T) { |
||||
wg := &sync.WaitGroup{} |
||||
wg.Add(2) |
||||
|
||||
cron := New() |
||||
cron.AddFunc("", "0 0 0 1 1 ?", func() {}) |
||||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) |
||||
cron.AddFunc("", "0 0 0 31 12 ?", func() {}) |
||||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) |
||||
|
||||
cron.Start() |
||||
defer cron.Stop() |
||||
|
||||
select { |
||||
case <-time.After(ONE_SECOND): |
||||
t.FailNow() |
||||
case <-wait(wg): |
||||
} |
||||
} |
||||
|
||||
// Test running the same job twice.
|
||||
func TestRunningJobTwice(t *testing.T) { |
||||
wg := &sync.WaitGroup{} |
||||
wg.Add(2) |
||||
|
||||
cron := New() |
||||
cron.AddFunc("", "0 0 0 1 1 ?", func() {}) |
||||
cron.AddFunc("", "0 0 0 31 12 ?", func() {}) |
||||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) |
||||
|
||||
cron.Start() |
||||
defer cron.Stop() |
||||
|
||||
select { |
||||
case <-time.After(2 * ONE_SECOND): |
||||
t.FailNow() |
||||
case <-wait(wg): |
||||
} |
||||
} |
||||
|
||||
func TestRunningMultipleSchedules(t *testing.T) { |
||||
wg := &sync.WaitGroup{} |
||||
wg.Add(2) |
||||
|
||||
cron := New() |
||||
cron.AddFunc("", "0 0 0 1 1 ?", func() {}) |
||||
cron.AddFunc("", "0 0 0 31 12 ?", func() {}) |
||||
cron.AddFunc("", "* * * * * ?", func() { wg.Done() }) |
||||
cron.Schedule("", "", Every(time.Minute), FuncJob(func() {})) |
||||
cron.Schedule("", "", Every(time.Second), FuncJob(func() { wg.Done() })) |
||||
cron.Schedule("", "", Every(time.Hour), FuncJob(func() {})) |
||||
|
||||
cron.Start() |
||||
defer cron.Stop() |
||||
|
||||
select { |
||||
case <-time.After(2 * ONE_SECOND): |
||||
t.FailNow() |
||||
case <-wait(wg): |
||||
} |
||||
} |
||||
|
||||
// Test that the cron is run in the local time zone (as opposed to UTC).
|
||||
func TestLocalTimezone(t *testing.T) { |
||||
wg := &sync.WaitGroup{} |
||||
wg.Add(1) |
||||
|
||||
now := time.Now().Local() |
||||
spec := fmt.Sprintf("%d %d %d %d %d ?", |
||||
now.Second()+1, now.Minute(), now.Hour(), now.Day(), now.Month()) |
||||
|
||||
cron := New() |
||||
cron.AddFunc("", spec, func() { wg.Done() }) |
||||
cron.Start() |
||||
defer cron.Stop() |
||||
|
||||
select { |
||||
case <-time.After(ONE_SECOND): |
||||
t.FailNow() |
||||
case <-wait(wg): |
||||
} |
||||
} |
||||
|
||||
type testJob struct { |
||||
wg *sync.WaitGroup |
||||
name string |
||||
} |
||||
|
||||
func (t testJob) Run() { |
||||
t.wg.Done() |
||||
} |
||||
|
||||
// Simple test using Runnables.
|
||||
func TestJob(t *testing.T) { |
||||
wg := &sync.WaitGroup{} |
||||
wg.Add(1) |
||||
|
||||
cron := New() |
||||
cron.AddJob("", "0 0 0 30 Feb ?", testJob{wg, "job0"}) |
||||
cron.AddJob("", "0 0 0 1 1 ?", testJob{wg, "job1"}) |
||||
cron.AddJob("", "* * * * * ?", testJob{wg, "job2"}) |
||||
cron.AddJob("", "1 0 0 1 1 ?", testJob{wg, "job3"}) |
||||
cron.Schedule("", "", Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"}) |
||||
cron.Schedule("", "", Every(5*time.Minute), testJob{wg, "job5"}) |
||||
|
||||
cron.Start() |
||||
defer cron.Stop() |
||||
|
||||
select { |
||||
case <-time.After(ONE_SECOND): |
||||
t.FailNow() |
||||
case <-wait(wg): |
||||
} |
||||
|
||||
// Ensure the entries are in the right order.
|
||||
expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"} |
||||
|
||||
var actuals []string |
||||
for _, entry := range cron.Entries() { |
||||
actuals = append(actuals, entry.Job.(testJob).name) |
||||
} |
||||
|
||||
for i, expected := range expecteds { |
||||
if actuals[i] != expected { |
||||
t.Errorf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals) |
||||
t.FailNow() |
||||
} |
||||
} |
||||
} |
||||
|
||||
func wait(wg *sync.WaitGroup) chan bool { |
||||
ch := make(chan bool) |
||||
go func() { |
||||
wg.Wait() |
||||
ch <- true |
||||
}() |
||||
return ch |
||||
} |
||||
|
||||
func stop(cron *Cron) chan bool { |
||||
ch := make(chan bool) |
||||
go func() { |
||||
cron.Stop() |
||||
ch <- true |
||||
}() |
||||
return ch |
||||
} |
@ -1,129 +0,0 @@
|
||||
/* |
||||
Package cron implements a cron spec parser and job runner. |
||||
|
||||
Usage |
||||
|
||||
Callers may register Funcs to be invoked on a given schedule. Cron will run |
||||
them in their own goroutines. |
||||
|
||||
c := cron.New() |
||||
c.AddFunc("Every hour on the half hour","0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) |
||||
c.AddFunc("Every hour","@hourly", func() { fmt.Println("Every hour") }) |
||||
c.AddFunc("Every hour and a half","@every 1h30m", func() { fmt.Println("Every hour thirty") }) |
||||
c.Start() |
||||
.. |
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
... |
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") }) |
||||
.. |
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries()) |
||||
.. |
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
CRON Expression Format |
||||
|
||||
A cron expression represents a set of times, using 6 space-separated fields. |
||||
|
||||
Field name | Mandatory? | Allowed values | Allowed special characters |
||||
---------- | ---------- | -------------- | -------------------------- |
||||
Seconds | Yes | 0-59 | * / , - |
||||
Minutes | Yes | 0-59 | * / , - |
||||
Hours | Yes | 0-23 | * / , - |
||||
Day of month | Yes | 1-31 | * / , - ? |
||||
Month | Yes | 1-12 or JAN-DEC | * / , - |
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? |
||||
|
||||
Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun", |
||||
and "sun" are equally accepted. |
||||
|
||||
Special Characters |
||||
|
||||
Asterisk ( * ) |
||||
|
||||
The asterisk indicates that the cron expression will match for all values of the |
||||
field; e.g., using an asterisk in the 5th field (month) would indicate every |
||||
month. |
||||
|
||||
Slash ( / ) |
||||
|
||||
Slashes are used to describe increments of ranges. For example 3-59/15 in the |
||||
1st field (minutes) would indicate the 3rd minute of the hour and every 15 |
||||
minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", |
||||
that is, an increment over the largest possible range of the field. The form |
||||
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the |
||||
increment until the end of that specific range. It does not wrap around. |
||||
|
||||
Comma ( , ) |
||||
|
||||
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in |
||||
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays. |
||||
|
||||
Hyphen ( - ) |
||||
|
||||
Hyphens are used to define ranges. For example, 9-17 would indicate every |
||||
hour between 9am and 5pm inclusive. |
||||
|
||||
Question mark ( ? ) |
||||
|
||||
Question mark may be used instead of '*' for leaving either day-of-month or |
||||
day-of-week blank. |
||||
|
||||
Predefined schedules |
||||
|
||||
You may use one of several pre-defined schedules in place of a cron expression. |
||||
|
||||
Entry | Description | Equivalent To |
||||
----- | ----------- | ------------- |
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * |
||||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * * |
||||
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 |
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * |
||||
@hourly | Run once an hour, beginning of hour | 0 0 * * * * |
||||
|
||||
Intervals |
||||
|
||||
You may also schedule a job to execute at fixed intervals. This is supported by |
||||
formatting the cron spec like this: |
||||
|
||||
@every <duration> |
||||
|
||||
where "duration" is a string accepted by time.ParseDuration |
||||
(http://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
For example, "@every 1h30m10s" would indicate a schedule that activates every |
||||
1 hour, 30 minutes, 10 seconds. |
||||
|
||||
Note: The interval does not take the job runtime into account. For example, |
||||
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, |
||||
it will have only 2 minutes of idle time between each run. |
||||
|
||||
Time zones |
||||
|
||||
All interpretation and scheduling is done in the machine's local time zone (as |
||||
provided by the Go time package (http://www.golang.org/pkg/time).
|
||||
|
||||
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will |
||||
not be run! |
||||
|
||||
Thread safety |
||||
|
||||
Since the Cron service runs concurrently with the calling code, some amount of |
||||
care must be taken to ensure proper synchronization. |
||||
|
||||
All cron methods are designed to be correctly synchronized as long as the caller |
||||
ensures that invocations have a clear happens-before ordering between them. |
||||
|
||||
Implementation |
||||
|
||||
Cron entries are stored in an array, sorted by their next activation time. Cron |
||||
sleeps until the next job is due to be run. |
||||
|
||||
Upon waking: |
||||
- it runs each entry that is active on that second |
||||
- it calculates the next run times for the jobs that were run |
||||
- it re-sorts the array of entries by next activation time. |
||||
- it goes to sleep until the soonest job. |
||||
*/ |
||||
package cron |
@ -1,231 +0,0 @@
|
||||
package cron |
||||
|
||||
import ( |
||||
"fmt" |
||||
"log" |
||||
"math" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// Parse returns a new crontab schedule representing the given spec.
|
||||
// It returns a descriptive error if the spec is not valid.
|
||||
//
|
||||
// It accepts
|
||||
// - Full crontab specs, e.g. "* * * * * ?"
|
||||
// - Descriptors, e.g. "@midnight", "@every 1h30m"
|
||||
func Parse(spec string) (_ Schedule, err error) { |
||||
// Convert panics into errors
|
||||
defer func() { |
||||
if recovered := recover(); recovered != nil { |
||||
err = fmt.Errorf("%v", recovered) |
||||
} |
||||
}() |
||||
|
||||
if spec[0] == '@' { |
||||
return parseDescriptor(spec), nil |
||||
} |
||||
|
||||
// Split on whitespace. We require 5 or 6 fields.
|
||||
// (second) (minute) (hour) (day of month) (month) (day of week, optional)
|
||||
fields := strings.Fields(spec) |
||||
if len(fields) != 5 && len(fields) != 6 { |
||||
log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec) |
||||
} |
||||
|
||||
// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
|
||||
if len(fields) == 5 { |
||||
fields = append(fields, "*") |
||||
} |
||||
|
||||
schedule := &SpecSchedule{ |
||||
Second: getField(fields[0], seconds), |
||||
Minute: getField(fields[1], minutes), |
||||
Hour: getField(fields[2], hours), |
||||
Dom: getField(fields[3], dom), |
||||
Month: getField(fields[4], months), |
||||
Dow: getField(fields[5], dow), |
||||
} |
||||
|
||||
return schedule, nil |
||||
} |
||||
|
||||
// getField returns an Int with the bits set representing all of the times that
|
||||
// the field represents. A "field" is a comma-separated list of "ranges".
|
||||
func getField(field string, r bounds) uint64 { |
||||
// list = range {"," range}
|
||||
var bits uint64 |
||||
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) |
||||
for _, expr := range ranges { |
||||
bits |= getRange(expr, r) |
||||
} |
||||
return bits |
||||
} |
||||
|
||||
// getRange returns the bits indicated by the given expression:
|
||||
// number | number "-" number [ "/" number ]
|
||||
func getRange(expr string, r bounds) uint64 { |
||||
|
||||
var ( |
||||
start, end, step uint |
||||
rangeAndStep = strings.Split(expr, "/") |
||||
lowAndHigh = strings.Split(rangeAndStep[0], "-") |
||||
singleDigit = len(lowAndHigh) == 1 |
||||
) |
||||
|
||||
var extra_star uint64 |
||||
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { |
||||
start = r.min |
||||
end = r.max |
||||
extra_star = starBit |
||||
} else { |
||||
start = parseIntOrName(lowAndHigh[0], r.names) |
||||
switch len(lowAndHigh) { |
||||
case 1: |
||||
end = start |
||||
case 2: |
||||
end = parseIntOrName(lowAndHigh[1], r.names) |
||||
default: |
||||
log.Panicf("Too many hyphens: %s", expr) |
||||
} |
||||
} |
||||
|
||||
switch len(rangeAndStep) { |
||||
case 1: |
||||
step = 1 |
||||
case 2: |
||||
step = mustParseInt(rangeAndStep[1]) |
||||
|
||||
// Special handling: "N/step" means "N-max/step".
|
||||
if singleDigit { |
||||
end = r.max |
||||
} |
||||
default: |
||||
log.Panicf("Too many slashes: %s", expr) |
||||
} |
||||
|
||||
if start < r.min { |
||||
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr) |
||||
} |
||||
if end > r.max { |
||||
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr) |
||||
} |
||||
if start > end { |
||||
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr) |
||||
} |
||||
|
||||
return getBits(start, end, step) | extra_star |
||||
} |
||||
|
||||
// parseIntOrName returns the (possibly-named) integer contained in expr.
|
||||
func parseIntOrName(expr string, names map[string]uint) uint { |
||||
if names != nil { |
||||
if namedInt, ok := names[strings.ToLower(expr)]; ok { |
||||
return namedInt |
||||
} |
||||
} |
||||
return mustParseInt(expr) |
||||
} |
||||
|
||||
// mustParseInt parses the given expression as an int or panics.
|
||||
func mustParseInt(expr string) uint { |
||||
num, err := strconv.Atoi(expr) |
||||
if err != nil { |
||||
log.Panicf("Failed to parse int from %s: %s", expr, err) |
||||
} |
||||
if num < 0 { |
||||
log.Panicf("Negative number (%d) not allowed: %s", num, expr) |
||||
} |
||||
|
||||
return uint(num) |
||||
} |
||||
|
||||
// getBits sets all bits in the range [min, max], modulo the given step size.
|
||||
func getBits(min, max, step uint) uint64 { |
||||
var bits uint64 |
||||
|
||||
// If step is 1, use shifts.
|
||||
if step == 1 { |
||||
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) |
||||
} |
||||
|
||||
// Else, use a simple loop.
|
||||
for i := min; i <= max; i += step { |
||||
bits |= 1 << i |
||||
} |
||||
return bits |
||||
} |
||||
|
||||
// all returns all bits within the given bounds. (plus the star bit)
|
||||
func all(r bounds) uint64 { |
||||
return getBits(r.min, r.max, 1) | starBit |
||||
} |
||||
|
||||
// parseDescriptor returns a pre-defined schedule for the expression, or panics
|
||||
// if none matches.
|
||||
func parseDescriptor(spec string) Schedule { |
||||
switch spec { |
||||
case "@yearly", "@annually": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: 1 << hours.min, |
||||
Dom: 1 << dom.min, |
||||
Month: 1 << months.min, |
||||
Dow: all(dow), |
||||
} |
||||
|
||||
case "@monthly": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: 1 << hours.min, |
||||
Dom: 1 << dom.min, |
||||
Month: all(months), |
||||
Dow: all(dow), |
||||
} |
||||
|
||||
case "@weekly": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: 1 << hours.min, |
||||
Dom: all(dom), |
||||
Month: all(months), |
||||
Dow: 1 << dow.min, |
||||
} |
||||
|
||||
case "@daily", "@midnight": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: 1 << hours.min, |
||||
Dom: all(dom), |
||||
Month: all(months), |
||||
Dow: all(dow), |
||||
} |
||||
|
||||
case "@hourly": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: all(hours), |
||||
Dom: all(dom), |
||||
Month: all(months), |
||||
Dow: all(dow), |
||||
} |
||||
} |
||||
|
||||
const every = "@every " |
||||
if strings.HasPrefix(spec, every) { |
||||
duration, err := time.ParseDuration(spec[len(every):]) |
||||
if err != nil { |
||||
log.Panicf("Failed to parse duration %s: %s", spec, err) |
||||
} |
||||
return Every(duration) |
||||
} |
||||
|
||||
log.Panicf("Unrecognized descriptor: %s", spec) |
||||
return nil |
||||
} |
@ -1,117 +0,0 @@
|
||||
package cron |
||||
|
||||
import ( |
||||
"reflect" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestRange(t *testing.T) { |
||||
ranges := []struct { |
||||
expr string |
||||
min, max uint |
||||
expected uint64 |
||||
}{ |
||||
{"5", 0, 7, 1 << 5}, |
||||
{"0", 0, 7, 1 << 0}, |
||||
{"7", 0, 7, 1 << 7}, |
||||
|
||||
{"5-5", 0, 7, 1 << 5}, |
||||
{"5-6", 0, 7, 1<<5 | 1<<6}, |
||||
{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7}, |
||||
|
||||
{"5-6/2", 0, 7, 1 << 5}, |
||||
{"5-7/2", 0, 7, 1<<5 | 1<<7}, |
||||
{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7}, |
||||
|
||||
{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit}, |
||||
{"*/2", 1, 3, 1<<1 | 1<<3 | starBit}, |
||||
} |
||||
|
||||
for _, c := range ranges { |
||||
actual := getRange(c.expr, bounds{c.min, c.max, nil}) |
||||
if actual != c.expected { |
||||
t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestField(t *testing.T) { |
||||
fields := []struct { |
||||
expr string |
||||
min, max uint |
||||
expected uint64 |
||||
}{ |
||||
{"5", 1, 7, 1 << 5}, |
||||
{"5,6", 1, 7, 1<<5 | 1<<6}, |
||||
{"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7}, |
||||
{"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3}, |
||||
} |
||||
|
||||
for _, c := range fields { |
||||
actual := getField(c.expr, bounds{c.min, c.max, nil}) |
||||
if actual != c.expected { |
||||
t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestBits(t *testing.T) { |
||||
allBits := []struct { |
||||
r bounds |
||||
expected uint64 |
||||
}{ |
||||
{minutes, 0xfffffffffffffff}, // 0-59: 60 ones
|
||||
{hours, 0xffffff}, // 0-23: 24 ones
|
||||
{dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero
|
||||
{months, 0x1ffe}, // 1-12: 12 ones, 1 zero
|
||||
{dow, 0x7f}, // 0-6: 7 ones
|
||||
} |
||||
|
||||
for _, c := range allBits { |
||||
actual := all(c.r) // all() adds the starBit, so compensate for that..
|
||||
if c.expected|starBit != actual { |
||||
t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", |
||||
c.r.min, c.r.max, 1, c.expected|starBit, actual) |
||||
} |
||||
} |
||||
|
||||
bits := []struct { |
||||
min, max, step uint |
||||
expected uint64 |
||||
}{ |
||||
|
||||
{0, 0, 1, 0x1}, |
||||
{1, 1, 1, 0x2}, |
||||
{1, 5, 2, 0x2a}, // 101010
|
||||
{1, 4, 2, 0xa}, // 1010
|
||||
} |
||||
|
||||
for _, c := range bits { |
||||
actual := getBits(c.min, c.max, c.step) |
||||
if c.expected != actual { |
||||
t.Errorf("%d-%d/%d => (expected) %b != %b (actual)", |
||||
c.min, c.max, c.step, c.expected, actual) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestSpecSchedule(t *testing.T) { |
||||
entries := []struct { |
||||
expr string |
||||
expected Schedule |
||||
}{ |
||||
{"* 5 * * * *", &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}}, |
||||
{"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}}, |
||||
} |
||||
|
||||
for _, c := range entries { |
||||
actual, err := Parse(c.expr) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
if !reflect.DeepEqual(actual, c.expected) { |
||||
t.Errorf("%s => (expected) %v != %v (actual)", c.expr, c.expected, actual) |
||||
} |
||||
} |
||||
} |
@ -1,161 +0,0 @@
|
||||
package cron |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
// SpecSchedule specifies a duty cycle (to the second granularity), based on a
|
||||
// traditional crontab specification. It is computed initially and stored as bit sets.
|
||||
type SpecSchedule struct { |
||||
Second, Minute, Hour, Dom, Month, Dow uint64 |
||||
} |
||||
|
||||
// bounds provides a range of acceptable values (plus a map of name to value).
|
||||
type bounds struct { |
||||
min, max uint |
||||
names map[string]uint |
||||
} |
||||
|
||||
// The bounds for each field.
|
||||
var ( |
||||
seconds = bounds{0, 59, nil} |
||||
minutes = bounds{0, 59, nil} |
||||
hours = bounds{0, 23, nil} |
||||
dom = bounds{1, 31, nil} |
||||
months = bounds{1, 12, map[string]uint{ |
||||
"jan": 1, |
||||
"feb": 2, |
||||
"mar": 3, |
||||
"apr": 4, |
||||
"may": 5, |
||||
"jun": 6, |
||||
"jul": 7, |
||||
"aug": 8, |
||||
"sep": 9, |
||||
"oct": 10, |
||||
"nov": 11, |
||||
"dec": 12, |
||||
}} |
||||
dow = bounds{0, 6, map[string]uint{ |
||||
"sun": 0, |
||||
"mon": 1, |
||||
"tue": 2, |
||||
"wed": 3, |
||||
"thu": 4, |
||||
"fri": 5, |
||||
"sat": 6, |
||||
}} |
||||
) |
||||
|
||||
const ( |
||||
// Set the top bit if a star was included in the expression.
|
||||
starBit = 1 << 63 |
||||
) |
||||
|
||||
// Next returns the next time this schedule is activated, greater than the given
|
||||
// time. If no time can be found to satisfy the schedule, return the zero time.
|
||||
func (s *SpecSchedule) Next(t time.Time) time.Time { |
||||
// General approach:
|
||||
// For Month, Day, Hour, Minute, Second:
|
||||
// Check if the time value matches. If yes, continue to the next field.
|
||||
// If the field doesn't match the schedule, then increment the field until it matches.
|
||||
// While incrementing the field, a wrap-around brings it back to the beginning
|
||||
// of the field list (since it is necessary to re-verify previous field
|
||||
// values)
|
||||
|
||||
// Start at the earliest possible time (the upcoming second).
|
||||
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) |
||||
|
||||
// This flag indicates whether a field has been incremented.
|
||||
added := false |
||||
|
||||
// If no time is found within five years, return zero.
|
||||
yearLimit := t.Year() + 5 |
||||
|
||||
WRAP: |
||||
if t.Year() > yearLimit { |
||||
return time.Time{} |
||||
} |
||||
|
||||
// Find the first applicable month.
|
||||
// If it's this month, then do nothing.
|
||||
for 1<<uint(t.Month())&s.Month == 0 { |
||||
// If we have to add a month, reset the other parts to 0.
|
||||
if !added { |
||||
added = true |
||||
// Otherwise, set the date at the beginning (since the current time is irrelevant).
|
||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) |
||||
} |
||||
t = t.AddDate(0, 1, 0) |
||||
|
||||
// Wrapped around.
|
||||
if t.Month() == time.January { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
// Now get a day in that month.
|
||||
for !dayMatches(s, t) { |
||||
if !added { |
||||
added = true |
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) |
||||
} |
||||
t = t.AddDate(0, 0, 1) |
||||
|
||||
if t.Day() == 1 { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
for 1<<uint(t.Hour())&s.Hour == 0 { |
||||
if !added { |
||||
added = true |
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()) |
||||
} |
||||
t = t.Add(1 * time.Hour) |
||||
|
||||
if t.Hour() == 0 { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
for 1<<uint(t.Minute())&s.Minute == 0 { |
||||
if !added { |
||||
added = true |
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location()) |
||||
} |
||||
t = t.Add(1 * time.Minute) |
||||
|
||||
if t.Minute() == 0 { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
for 1<<uint(t.Second())&s.Second == 0 { |
||||
if !added { |
||||
added = true |
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()) |
||||
} |
||||
t = t.Add(1 * time.Second) |
||||
|
||||
if t.Second() == 0 { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
return t |
||||
} |
||||
|
||||
// dayMatches returns true if the schedule's day-of-week and day-of-month
|
||||
// restrictions are satisfied by the given time.
|
||||
func dayMatches(s *SpecSchedule, t time.Time) bool { |
||||
var ( |
||||
domMatch bool = 1<<uint(t.Day())&s.Dom > 0 |
||||
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0 |
||||
) |
||||
|
||||
if s.Dom&starBit > 0 || s.Dow&starBit > 0 { |
||||
return domMatch && dowMatch |
||||
} |
||||
return domMatch || dowMatch |
||||
} |
@ -1,173 +0,0 @@
|
||||
package cron |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestActivation(t *testing.T) { |
||||
tests := []struct { |
||||
time, spec string |
||||
expected bool |
||||
}{ |
||||
// Every fifteen minutes.
|
||||
{"Mon Jul 9 15:00 2012", "0 0/15 * * *", true}, |
||||
{"Mon Jul 9 15:45 2012", "0 0/15 * * *", true}, |
||||
{"Mon Jul 9 15:40 2012", "0 0/15 * * *", false}, |
||||
|
||||
// Every fifteen minutes, starting at 5 minutes.
|
||||
{"Mon Jul 9 15:05 2012", "0 5/15 * * *", true}, |
||||
{"Mon Jul 9 15:20 2012", "0 5/15 * * *", true}, |
||||
{"Mon Jul 9 15:50 2012", "0 5/15 * * *", true}, |
||||
|
||||
// Named months
|
||||
{"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true}, |
||||
{"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false}, |
||||
|
||||
// Everything set.
|
||||
{"Sun Jul 15 08:30 2012", "0 30 08 ? Jul Sun", true}, |
||||
{"Sun Jul 15 08:30 2012", "0 30 08 15 Jul ?", true}, |
||||
{"Mon Jul 16 08:30 2012", "0 30 08 ? Jul Sun", false}, |
||||
{"Mon Jul 16 08:30 2012", "0 30 08 15 Jul ?", false}, |
||||
|
||||
// Predefined schedules
|
||||
{"Mon Jul 9 15:00 2012", "@hourly", true}, |
||||
{"Mon Jul 9 15:04 2012", "@hourly", false}, |
||||
{"Mon Jul 9 15:00 2012", "@daily", false}, |
||||
{"Mon Jul 9 00:00 2012", "@daily", true}, |
||||
{"Mon Jul 9 00:00 2012", "@weekly", false}, |
||||
{"Sun Jul 8 00:00 2012", "@weekly", true}, |
||||
{"Sun Jul 8 01:00 2012", "@weekly", false}, |
||||
{"Sun Jul 8 00:00 2012", "@monthly", false}, |
||||
{"Sun Jul 1 00:00 2012", "@monthly", true}, |
||||
|
||||
// Test interaction of DOW and DOM.
|
||||
// If both are specified, then only one needs to match.
|
||||
{"Sun Jul 15 00:00 2012", "0 * * 1,15 * Sun", true}, |
||||
{"Fri Jun 15 00:00 2012", "0 * * 1,15 * Sun", true}, |
||||
{"Wed Aug 1 00:00 2012", "0 * * 1,15 * Sun", true}, |
||||
|
||||
// However, if one has a star, then both need to match.
|
||||
{"Sun Jul 15 00:00 2012", "0 * * * * Mon", false}, |
||||
{"Sun Jul 15 00:00 2012", "0 * * */10 * Sun", false}, |
||||
{"Mon Jul 9 00:00 2012", "0 * * 1,15 * *", false}, |
||||
{"Sun Jul 15 00:00 2012", "0 * * 1,15 * *", true}, |
||||
{"Sun Jul 15 00:00 2012", "0 * * */2 * Sun", true}, |
||||
} |
||||
|
||||
for _, test := range tests { |
||||
sched, err := Parse(test.spec) |
||||
if err != nil { |
||||
t.Error(err) |
||||
continue |
||||
} |
||||
actual := sched.Next(getTime(test.time).Add(-1 * time.Second)) |
||||
expected := getTime(test.time) |
||||
if test.expected && expected != actual || !test.expected && expected == actual { |
||||
t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)", |
||||
test.spec, test.time, expected, actual) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestNext(t *testing.T) { |
||||
runs := []struct { |
||||
time, spec string |
||||
expected string |
||||
}{ |
||||
// Simple cases
|
||||
{"Mon Jul 9 14:45 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, |
||||
{"Mon Jul 9 14:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, |
||||
{"Mon Jul 9 14:59:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"}, |
||||
|
||||
// Wrap around hours
|
||||
{"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"}, |
||||
|
||||
// Wrap around days
|
||||
{"Mon Jul 9 23:46 2012", "0 */15 * * *", "Tue Jul 10 00:00 2012"}, |
||||
{"Mon Jul 9 23:45 2012", "0 20-35/15 * * *", "Tue Jul 10 00:20 2012"}, |
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * *", "Tue Jul 10 00:20:15 2012"}, |
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * *", "Tue Jul 10 01:20:15 2012"}, |
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * *", "Tue Jul 10 10:20:15 2012"}, |
||||
|
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"}, |
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"}, |
||||
{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"}, |
||||
|
||||
// Wrap around months
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"}, |
||||
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Aug 6 00:00 2012"}, |
||||
{"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"}, |
||||
|
||||
// Wrap around years
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"}, |
||||
{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"}, |
||||
|
||||
// Wrap around minute, hour, day, month, and year
|
||||
{"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"}, |
||||
|
||||
// Leap year
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"}, |
||||
|
||||
// Daylight savings time EST -> EDT
|
||||
{"2012-03-11T00:00:00-0500", "0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"}, |
||||
|
||||
// Daylight savings time EDT -> EST
|
||||
{"2012-11-04T00:00:00-0400", "0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"}, |
||||
{"2012-11-04T01:45:00-0400", "0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"}, |
||||
|
||||
// Unsatisfiable
|
||||
{"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""}, |
||||
{"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""}, |
||||
} |
||||
|
||||
for _, c := range runs { |
||||
sched, err := Parse(c.spec) |
||||
if err != nil { |
||||
t.Error(err) |
||||
continue |
||||
} |
||||
actual := sched.Next(getTime(c.time)) |
||||
expected := getTime(c.expected) |
||||
if !actual.Equal(expected) { |
||||
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestErrors(t *testing.T) { |
||||
invalidSpecs := []string{ |
||||
"xyz", |
||||
"60 0 * * *", |
||||
"0 60 * * *", |
||||
"0 0 * * XYZ", |
||||
} |
||||
for _, spec := range invalidSpecs { |
||||
_, err := Parse(spec) |
||||
if err == nil { |
||||
t.Error("expected an error parsing: ", spec) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func getTime(value string) time.Time { |
||||
if value == "" { |
||||
return time.Time{} |
||||
} |
||||
t, err := time.Parse("Mon Jan 2 15:04 2006", value) |
||||
if err != nil { |
||||
t, err = time.Parse("Mon Jan 2 15:04:05 2006", value) |
||||
if err != nil { |
||||
t, err = time.Parse("2006-01-02T15:04:05-0700", value) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
// Daylight savings time tests require location
|
||||
if ny, err := time.LoadLocation("America/New_York"); err == nil { |
||||
t = t.In(ny) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return t |
||||
} |
@ -1,206 +0,0 @@
|
||||
// Copyright 2013 The Beego Authors. All rights reserved.
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package httplib |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"os" |
||||
"strings" |
||||
"testing" |
||||
) |
||||
|
||||
func TestResponse(t *testing.T) { |
||||
req := Get("http://httpbin.org/get") |
||||
resp, err := req.Response() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(resp) |
||||
} |
||||
|
||||
func TestGet(t *testing.T) { |
||||
req := Get("http://httpbin.org/get") |
||||
b, err := req.Bytes() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(b) |
||||
|
||||
s, err := req.String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(s) |
||||
|
||||
if string(b) != s { |
||||
t.Fatal("request data not match") |
||||
} |
||||
} |
||||
|
||||
func TestSimplePost(t *testing.T) { |
||||
v := "smallfish" |
||||
req := Post("http://httpbin.org/post") |
||||
req.Param("username", v) |
||||
|
||||
str, err := req.String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(str) |
||||
|
||||
n := strings.Index(str, v) |
||||
if n == -1 { |
||||
t.Fatal(v + " not found in post") |
||||
} |
||||
} |
||||
|
||||
// func TestPostFile(t *testing.T) {
|
||||
// v := "smallfish"
|
||||
// req := Post("http://httpbin.org/post")
|
||||
// req.Param("username", v)
|
||||
// req.PostFile("uploadfile", "httplib_test.go")
|
||||
|
||||
// str, err := req.String()
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// t.Log(str)
|
||||
|
||||
// n := strings.Index(str, v)
|
||||
// if n == -1 {
|
||||
// t.Fatal(v + " not found in post")
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestSimplePut(t *testing.T) { |
||||
str, err := Put("http://httpbin.org/put").String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(str) |
||||
} |
||||
|
||||
func TestSimpleDelete(t *testing.T) { |
||||
str, err := Delete("http://httpbin.org/delete").String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(str) |
||||
} |
||||
|
||||
func TestWithCookie(t *testing.T) { |
||||
v := "smallfish" |
||||
str, err := Get("http://httpbin.org/cookies/set?k1=" + v).SetEnableCookie(true).String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(str) |
||||
|
||||
str, err = Get("http://httpbin.org/cookies").SetEnableCookie(true).String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(str) |
||||
|
||||
n := strings.Index(str, v) |
||||
if n == -1 { |
||||
t.Fatal(v + " not found in cookie") |
||||
} |
||||
} |
||||
|
||||
func TestWithBasicAuth(t *testing.T) { |
||||
str, err := Get("http://httpbin.org/basic-auth/user/passwd").SetBasicAuth("user", "passwd").String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(str) |
||||
n := strings.Index(str, "authenticated") |
||||
if n == -1 { |
||||
t.Fatal("authenticated not found in response") |
||||
} |
||||
} |
||||
|
||||
func TestWithUserAgent(t *testing.T) { |
||||
v := "beego" |
||||
str, err := Get("http://httpbin.org/headers").SetUserAgent(v).String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(str) |
||||
|
||||
n := strings.Index(str, v) |
||||
if n == -1 { |
||||
t.Fatal(v + " not found in user-agent") |
||||
} |
||||
} |
||||
|
||||
func TestWithSetting(t *testing.T) { |
||||
v := "beego" |
||||
var setting BeegoHttpSettings |
||||
setting.EnableCookie = true |
||||
setting.UserAgent = v |
||||
setting.Transport = nil |
||||
SetDefaultSetting(setting) |
||||
|
||||
str, err := Get("http://httpbin.org/get").String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(str) |
||||
|
||||
n := strings.Index(str, v) |
||||
if n == -1 { |
||||
t.Fatal(v + " not found in user-agent") |
||||
} |
||||
} |
||||
|
||||
func TestToJson(t *testing.T) { |
||||
req := Get("http://httpbin.org/ip") |
||||
resp, err := req.Response() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(resp) |
||||
|
||||
// httpbin will return http remote addr
|
||||
type Ip struct { |
||||
Origin string `json:"origin"` |
||||
} |
||||
var ip Ip |
||||
err = req.ToJson(&ip) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(ip.Origin) |
||||
|
||||
if n := strings.Count(ip.Origin, "."); n != 3 { |
||||
t.Fatal("response is not valid ip") |
||||
} |
||||
} |
||||
|
||||
func TestToFile(t *testing.T) { |
||||
f := "beego_testfile" |
||||
req := Get("http://httpbin.org/ip") |
||||
err := req.ToFile(f) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
defer os.Remove(f) |
||||
b, err := ioutil.ReadFile(f) |
||||
if n := strings.Index(string(b), "origin"); n == -1 { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func TestHeader(t *testing.T) { |
||||
req := Get("http://httpbin.org/headers") |
||||
req.Header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36") |
||||
str, err := req.String() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
t.Log(str) |
||||
} |
@ -1,68 +0,0 @@
|
||||
// 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 log |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
|
||||
"github.com/go-xorm/xorm" |
||||
) |
||||
|
||||
type Log struct { |
||||
Id int64 |
||||
Level int |
||||
Msg string `xorm:"TEXT"` |
||||
} |
||||
|
||||
// DatabaseWriter implements LoggerInterface and is used to log into database.
|
||||
type DatabaseWriter struct { |
||||
Driver string `json:"driver"` |
||||
Conn string `json:"conn"` |
||||
Level int `json:"level"` |
||||
x *xorm.Engine |
||||
} |
||||
|
||||
func NewDatabase() LoggerInterface { |
||||
return &DatabaseWriter{Level: TRACE} |
||||
} |
||||
|
||||
// init database writer with json config.
|
||||
// config like:
|
||||
// {
|
||||
// "driver": "mysql"
|
||||
// "conn":"root:root@tcp(127.0.0.1:3306)/gogs?charset=utf8",
|
||||
// "level": 0
|
||||
// }
|
||||
// connection string is based on xorm.
|
||||
func (d *DatabaseWriter) Init(jsonconfig string) (err error) { |
||||
if err = json.Unmarshal([]byte(jsonconfig), d); err != nil { |
||||
return err |
||||
} |
||||
d.x, err = xorm.NewEngine(d.Driver, d.Conn) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return d.x.Sync(new(Log)) |
||||
} |
||||
|
||||
// write message in database writer.
|
||||
func (d *DatabaseWriter) WriteMsg(msg string, skip, level int) error { |
||||
if level < d.Level { |
||||
return nil |
||||
} |
||||
|
||||
_, err := d.x.Insert(&Log{Level: level, Msg: msg}) |
||||
return err |
||||
} |
||||
|
||||
func (_ *DatabaseWriter) Flush() { |
||||
} |
||||
|
||||
func (_ *DatabaseWriter) Destroy() { |
||||
} |
||||
|
||||
func init() { |
||||
Register("database", NewDatabase) |
||||
} |
@ -1,133 +0,0 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package middleware |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/url" |
||||
|
||||
"github.com/go-macaron/csrf" |
||||
"gopkg.in/macaron.v1" |
||||
|
||||
"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/setting" |
||||
) |
||||
|
||||
type ToggleOptions struct { |
||||
SignInRequire bool |
||||
SignOutRequire bool |
||||
AdminRequire bool |
||||
DisableCsrf bool |
||||
} |
||||
|
||||
// AutoSignIn reads cookie and try to auto-login.
|
||||
func AutoSignIn(ctx *Context) (bool, error) { |
||||
if !models.HasEngine { |
||||
return false, nil |
||||
} |
||||
|
||||
uname := ctx.GetCookie(setting.CookieUserName) |
||||
if len(uname) == 0 { |
||||
return false, nil |
||||
} |
||||
|
||||
isSucceed := false |
||||
defer func() { |
||||
if !isSucceed { |
||||
log.Trace("auto-login cookie cleared: %s", uname) |
||||
ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl) |
||||
ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl) |
||||
} |
||||
}() |
||||
|
||||
u, err := models.GetUserByName(uname) |
||||
if err != nil { |
||||
if !models.IsErrUserNotExist(err) { |
||||
return false, fmt.Errorf("GetUserByName: %v", err) |
||||
} |
||||
return false, nil |
||||
} |
||||
|
||||
if val, _ := ctx.GetSuperSecureCookie( |
||||
base.EncodeMD5(u.Rands+u.Passwd), setting.CookieRememberName); val != u.Name { |
||||
return false, nil |
||||
} |
||||
|
||||
isSucceed = true |
||||
ctx.Session.Set("uid", u.Id) |
||||
ctx.Session.Set("uname", u.Name) |
||||
return true, nil |
||||
} |
||||
|
||||
func Toggle(options *ToggleOptions) macaron.Handler { |
||||
return func(ctx *Context) { |
||||
// Cannot view any page before installation.
|
||||
if !setting.InstallLock { |
||||
ctx.Redirect(setting.AppSubUrl + "/install") |
||||
return |
||||
} |
||||
|
||||
// Checking non-logged users landing page.
|
||||
if !ctx.IsSigned && ctx.Req.RequestURI == "/" && setting.LandingPageUrl != setting.LANDING_PAGE_HOME { |
||||
ctx.Redirect(setting.AppSubUrl + string(setting.LandingPageUrl)) |
||||
return |
||||
} |
||||
|
||||
// Redirect to dashboard if user tries to visit any non-login page.
|
||||
if options.SignOutRequire && ctx.IsSigned && ctx.Req.RequestURI != "/" { |
||||
ctx.Redirect(setting.AppSubUrl + "/") |
||||
return |
||||
} |
||||
|
||||
if !options.SignOutRequire && !options.DisableCsrf && ctx.Req.Method == "POST" && !auth.IsAPIPath(ctx.Req.URL.Path) { |
||||
csrf.Validate(ctx.Context, ctx.csrf) |
||||
if ctx.Written() { |
||||
return |
||||
} |
||||
} |
||||
|
||||
if options.SignInRequire { |
||||
if !ctx.IsSigned { |
||||
// Restrict API calls with error message.
|
||||
if auth.IsAPIPath(ctx.Req.URL.Path) { |
||||
ctx.APIError(403, "", "Only signed in user is allowed to call APIs.") |
||||
return |
||||
} |
||||
|
||||
ctx.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+ctx.Req.RequestURI), 0, setting.AppSubUrl) |
||||
ctx.Redirect(setting.AppSubUrl + "/user/login") |
||||
return |
||||
} else if !ctx.User.IsActive && setting.Service.RegisterEmailConfirm { |
||||
ctx.Data["Title"] = ctx.Tr("auth.active_your_account") |
||||
ctx.HTML(200, "user/auth/activate") |
||||
return |
||||
} |
||||
} |
||||
|
||||
// Try auto-signin when not signed in.
|
||||
if !options.SignOutRequire && !ctx.IsSigned && !auth.IsAPIPath(ctx.Req.URL.Path) { |
||||
succeed, err := AutoSignIn(ctx) |
||||
if err != nil { |
||||
ctx.Handle(500, "AutoSignIn", err) |
||||
return |
||||
} else if succeed { |
||||
log.Trace("Auto-login succeed: %s", ctx.Session.Get("uname")) |
||||
ctx.Redirect(setting.AppSubUrl + ctx.Req.RequestURI) |
||||
return |
||||
} |
||||
} |
||||
|
||||
if options.AdminRequire { |
||||
if !ctx.User.IsAdmin { |
||||
ctx.Error(403) |
||||
return |
||||
} |
||||
ctx.Data["PageIsAdmin"] = true |
||||
} |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue