mirror of https://github.com/gogits/gogs.git
Lunny Xiao
11 years ago
113 changed files with 5679 additions and 1839 deletions
@ -1,6 +0,0 @@ |
|||||||
package models |
|
||||||
|
|
||||||
func Fix() error { |
|
||||||
_, err := orm.Exec("alter table repository drop column num_releases") |
|
||||||
return err |
|
||||||
} |
|
@ -0,0 +1,236 @@ |
|||||||
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package models |
||||||
|
|
||||||
|
import ( |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/gogits/gogs/modules/base" |
||||||
|
) |
||||||
|
|
||||||
|
// GetOwnerTeam returns owner team of organization.
|
||||||
|
func (org *User) GetOwnerTeam() (*Team, error) { |
||||||
|
t := &Team{ |
||||||
|
OrgId: org.Id, |
||||||
|
Name: OWNER_TEAM, |
||||||
|
} |
||||||
|
_, err := x.Get(t) |
||||||
|
return t, err |
||||||
|
} |
||||||
|
|
||||||
|
// CreateOrganization creates record of a new organization.
|
||||||
|
func CreateOrganization(org, owner *User) (*User, error) { |
||||||
|
if !IsLegalName(org.Name) { |
||||||
|
return nil, ErrUserNameIllegal |
||||||
|
} |
||||||
|
|
||||||
|
isExist, err := IsUserExist(org.Name) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} else if isExist { |
||||||
|
return nil, ErrUserAlreadyExist |
||||||
|
} |
||||||
|
|
||||||
|
isExist, err = IsEmailUsed(org.Email) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} else if isExist { |
||||||
|
return nil, ErrEmailAlreadyUsed |
||||||
|
} |
||||||
|
|
||||||
|
org.LowerName = strings.ToLower(org.Name) |
||||||
|
org.FullName = org.Name |
||||||
|
org.Avatar = base.EncodeMd5(org.Email) |
||||||
|
org.AvatarEmail = org.Email |
||||||
|
// No password for organization.
|
||||||
|
org.NumTeams = 1 |
||||||
|
org.NumMembers = 1 |
||||||
|
|
||||||
|
sess := x.NewSession() |
||||||
|
defer sess.Close() |
||||||
|
if err = sess.Begin(); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if _, err = sess.Insert(org); err != nil { |
||||||
|
sess.Rollback() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Create default owner team.
|
||||||
|
t := &Team{ |
||||||
|
OrgId: org.Id, |
||||||
|
Name: OWNER_TEAM, |
||||||
|
Authorize: ORG_ADMIN, |
||||||
|
NumMembers: 1, |
||||||
|
} |
||||||
|
if _, err = sess.Insert(t); err != nil { |
||||||
|
sess.Rollback() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Add initial creator to organization and owner team.
|
||||||
|
ou := &OrgUser{ |
||||||
|
Uid: owner.Id, |
||||||
|
OrgId: org.Id, |
||||||
|
IsOwner: true, |
||||||
|
NumTeam: 1, |
||||||
|
} |
||||||
|
if _, err = sess.Insert(ou); err != nil { |
||||||
|
sess.Rollback() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
tu := &TeamUser{ |
||||||
|
Uid: owner.Id, |
||||||
|
OrgId: org.Id, |
||||||
|
TeamId: t.Id, |
||||||
|
} |
||||||
|
if _, err = sess.Insert(tu); err != nil { |
||||||
|
sess.Rollback() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return org, sess.Commit() |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: need some kind of mechanism to record failure.
|
||||||
|
// DeleteOrganization completely and permanently deletes everything of organization.
|
||||||
|
func DeleteOrganization(org *User) (err error) { |
||||||
|
if err := DeleteUser(org); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
sess := x.NewSession() |
||||||
|
defer sess.Close() |
||||||
|
if err = sess.Begin(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if _, err = sess.Delete(&Team{OrgId: org.Id}); err != nil { |
||||||
|
sess.Rollback() |
||||||
|
return err |
||||||
|
} |
||||||
|
if _, err = sess.Delete(&OrgUser{OrgId: org.Id}); err != nil { |
||||||
|
sess.Rollback() |
||||||
|
return err |
||||||
|
} |
||||||
|
if _, err = sess.Delete(&TeamUser{OrgId: org.Id}); err != nil { |
||||||
|
sess.Rollback() |
||||||
|
return err |
||||||
|
} |
||||||
|
return sess.Commit() |
||||||
|
} |
||||||
|
|
||||||
|
type AuthorizeType int |
||||||
|
|
||||||
|
const ( |
||||||
|
ORG_READABLE AuthorizeType = iota + 1 |
||||||
|
ORG_WRITABLE |
||||||
|
ORG_ADMIN |
||||||
|
) |
||||||
|
|
||||||
|
const OWNER_TEAM = "Owner" |
||||||
|
|
||||||
|
// Team represents a organization team.
|
||||||
|
type Team struct { |
||||||
|
Id int64 |
||||||
|
OrgId int64 `xorm:"INDEX"` |
||||||
|
Name string |
||||||
|
Description string |
||||||
|
Authorize AuthorizeType |
||||||
|
RepoIds string `xorm:"TEXT"` |
||||||
|
NumMembers int |
||||||
|
NumRepos int |
||||||
|
} |
||||||
|
|
||||||
|
// NewTeam creates a record of new team.
|
||||||
|
func NewTeam(t *Team) error { |
||||||
|
_, err := x.Insert(t) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func UpdateTeam(t *Team) error { |
||||||
|
if len(t.Description) > 255 { |
||||||
|
t.Description = t.Description[:255] |
||||||
|
} |
||||||
|
|
||||||
|
_, err := x.Id(t.Id).AllCols().Update(t) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// ________ ____ ___
|
||||||
|
// \_____ \_______ ____ | | \______ ___________
|
||||||
|
// / | \_ __ \/ ___\| | / ___// __ \_ __ \
|
||||||
|
// / | \ | \/ /_/ > | /\___ \\ ___/| | \/
|
||||||
|
// \_______ /__| \___ /|______//____ >\___ >__|
|
||||||
|
// \/ /_____/ \/ \/
|
||||||
|
|
||||||
|
// OrgUser represents an organization-user relation.
|
||||||
|
type OrgUser struct { |
||||||
|
Id int64 |
||||||
|
Uid int64 `xorm:"INDEX"` |
||||||
|
OrgId int64 `xorm:"INDEX"` |
||||||
|
IsPublic bool |
||||||
|
IsOwner bool |
||||||
|
NumTeam int |
||||||
|
} |
||||||
|
|
||||||
|
// GetOrgUsersByUserId returns all organization-user relations by user ID.
|
||||||
|
func GetOrgUsersByUserId(uid int64) ([]*OrgUser, error) { |
||||||
|
ous := make([]*OrgUser, 0, 10) |
||||||
|
err := x.Where("uid=?", uid).Find(&ous) |
||||||
|
return ous, err |
||||||
|
} |
||||||
|
|
||||||
|
// GetOrgUsersByOrgId returns all organization-user relations by organization ID.
|
||||||
|
func GetOrgUsersByOrgId(orgId int64) ([]*OrgUser, error) { |
||||||
|
ous := make([]*OrgUser, 0, 10) |
||||||
|
err := x.Where("org_id=?", orgId).Find(&ous) |
||||||
|
return ous, err |
||||||
|
} |
||||||
|
|
||||||
|
func GetOrganizationCount(u *User) (int64, error) { |
||||||
|
return x.Where("uid=?", u.Id).Count(new(OrgUser)) |
||||||
|
} |
||||||
|
|
||||||
|
// IsOrganizationOwner returns true if given user ID is in the owner team.
|
||||||
|
func IsOrganizationOwner(orgId, uid int64) bool { |
||||||
|
has, _ := x.Where("is_owner=?", true).Get(&OrgUser{Uid: uid, OrgId: orgId}) |
||||||
|
return has |
||||||
|
} |
||||||
|
|
||||||
|
// ___________ ____ ___
|
||||||
|
// \__ ___/___ _____ _____ | | \______ ___________
|
||||||
|
// | |_/ __ \\__ \ / \| | / ___// __ \_ __ \
|
||||||
|
// | |\ ___/ / __ \| Y Y \ | /\___ \\ ___/| | \/
|
||||||
|
// |____| \___ >____ /__|_| /______//____ >\___ >__|
|
||||||
|
// \/ \/ \/ \/ \/
|
||||||
|
|
||||||
|
// TeamUser represents an team-user relation.
|
||||||
|
type TeamUser struct { |
||||||
|
Id int64 |
||||||
|
Uid int64 |
||||||
|
OrgId int64 `xorm:"INDEX"` |
||||||
|
TeamId int64 |
||||||
|
} |
||||||
|
|
||||||
|
// GetTeamMembers returns all members in given team of organization.
|
||||||
|
func GetTeamMembers(orgId, teamId int64) ([]*User, error) { |
||||||
|
tus := make([]*TeamUser, 0, 10) |
||||||
|
err := x.Where("org_id=?", orgId).And("team_id=?", teamId).Find(&tus) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
us := make([]*User, len(tus)) |
||||||
|
for i, tu := range tus { |
||||||
|
us[i], err = GetUserById(tu.Uid) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
return us, nil |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package auth |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"reflect" |
||||||
|
|
||||||
|
"github.com/go-martini/martini" |
||||||
|
|
||||||
|
"github.com/gogits/gogs/modules/base" |
||||||
|
"github.com/gogits/gogs/modules/middleware/binding" |
||||||
|
) |
||||||
|
|
||||||
|
type CreateOrgForm struct { |
||||||
|
OrgName string `form:"orgname" binding:"Required;AlphaDashDot;MaxSize(30)"` |
||||||
|
Email string `form:"email" binding:"Required;Email;MaxSize(50)"` |
||||||
|
} |
||||||
|
|
||||||
|
func (f *CreateOrgForm) Name(field string) string { |
||||||
|
names := map[string]string{ |
||||||
|
"OrgName": "Organization name", |
||||||
|
"Email": "E-mail address", |
||||||
|
} |
||||||
|
return names[field] |
||||||
|
} |
||||||
|
|
||||||
|
func (f *CreateOrgForm) Validate(errs *binding.Errors, req *http.Request, ctx martini.Context) { |
||||||
|
data := ctx.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) |
||||||
|
validate(errs, data, f) |
||||||
|
} |
||||||
|
|
||||||
|
type OrgSettingForm struct { |
||||||
|
DisplayName string `form:"display_name" binding:"Required;MaxSize(100)"` |
||||||
|
Email string `form:"email" binding:"Required;Email;MaxSize(50)"` |
||||||
|
Description string `form:"desc" binding:"MaxSize(255)"` |
||||||
|
Website string `form:"site" binding:"Url;MaxSize(100)"` |
||||||
|
Location string `form:"location" binding:"MaxSize(50)"` |
||||||
|
} |
||||||
|
|
||||||
|
func (f *OrgSettingForm) Name(field string) string { |
||||||
|
names := map[string]string{ |
||||||
|
"DisplayName": "Display name", |
||||||
|
"Email": "E-mail address", |
||||||
|
"Description": "Description", |
||||||
|
"Website": "Website address", |
||||||
|
"Location": "Location", |
||||||
|
} |
||||||
|
return names[field] |
||||||
|
} |
||||||
|
|
||||||
|
func (f *OrgSettingForm) Validate(errors *binding.Errors, req *http.Request, context martini.Context) { |
||||||
|
data := context.Get(reflect.TypeOf(base.TmplData{})).Interface().(base.TmplData) |
||||||
|
validate(errors, data, f) |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
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) |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,255 @@ |
|||||||
|
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 |
||||||
|
} |
@ -0,0 +1,129 @@ |
|||||||
|
/* |
||||||
|
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("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) |
||||||
|
c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) |
||||||
|
c.AddFunc("@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 |
@ -0,0 +1,24 @@ |
|||||||
|
// 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 ( |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/gogits/gogs/models" |
||||||
|
"github.com/gogits/gogs/modules/setting" |
||||||
|
) |
||||||
|
|
||||||
|
var c = New() |
||||||
|
|
||||||
|
func NewCronContext() { |
||||||
|
c.AddFunc("Update mirrors", "@every 1h", models.MirrorUpdate) |
||||||
|
c.AddFunc("Deliver hooks", fmt.Sprintf("@every %dm", setting.WebhookTaskInterval), models.DeliverHooks) |
||||||
|
c.Start() |
||||||
|
} |
||||||
|
|
||||||
|
func ListEntries() []*Entry { |
||||||
|
return c.Entries() |
||||||
|
} |
@ -0,0 +1,231 @@ |
|||||||
|
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 |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
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) %b != %b (actual)", c.expr, c.expected, actual) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,161 @@ |
|||||||
|
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 |
||||||
|
} |
@ -0,0 +1,173 @@ |
|||||||
|
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,95 +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 hooks |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/gogits/gogs/modules/httplib" |
|
||||||
"github.com/gogits/gogs/modules/log" |
|
||||||
) |
|
||||||
|
|
||||||
// Hook task types.
|
|
||||||
const ( |
|
||||||
HTT_WEBHOOK = iota + 1 |
|
||||||
HTT_SERVICE |
|
||||||
) |
|
||||||
|
|
||||||
type PayloadAuthor struct { |
|
||||||
Name string `json:"name"` |
|
||||||
Email string `json:"email"` |
|
||||||
} |
|
||||||
|
|
||||||
type PayloadCommit struct { |
|
||||||
Id string `json:"id"` |
|
||||||
Message string `json:"message"` |
|
||||||
Url string `json:"url"` |
|
||||||
Author *PayloadAuthor `json:"author"` |
|
||||||
} |
|
||||||
|
|
||||||
type PayloadRepo struct { |
|
||||||
Id int64 `json:"id"` |
|
||||||
Name string `json:"name"` |
|
||||||
Url string `json:"url"` |
|
||||||
Description string `json:"description"` |
|
||||||
Website string `json:"website"` |
|
||||||
Watchers int `json:"watchers"` |
|
||||||
Owner *PayloadAuthor `json:"author"` |
|
||||||
Private bool `json:"private"` |
|
||||||
} |
|
||||||
|
|
||||||
// Payload represents payload information of hook.
|
|
||||||
type Payload struct { |
|
||||||
Secret string `json:"secret"` |
|
||||||
Ref string `json:"ref"` |
|
||||||
Commits []*PayloadCommit `json:"commits"` |
|
||||||
Repo *PayloadRepo `json:"repository"` |
|
||||||
Pusher *PayloadAuthor `json:"pusher"` |
|
||||||
} |
|
||||||
|
|
||||||
// HookTask represents hook task.
|
|
||||||
type HookTask struct { |
|
||||||
Type int |
|
||||||
Url string |
|
||||||
*Payload |
|
||||||
ContentType int |
|
||||||
IsSsl bool |
|
||||||
} |
|
||||||
|
|
||||||
var ( |
|
||||||
taskQueue = make(chan *HookTask, 1000) |
|
||||||
) |
|
||||||
|
|
||||||
// AddHookTask adds new hook task to task queue.
|
|
||||||
func AddHookTask(t *HookTask) { |
|
||||||
taskQueue <- t |
|
||||||
} |
|
||||||
|
|
||||||
func init() { |
|
||||||
go handleQueue() |
|
||||||
} |
|
||||||
|
|
||||||
func handleQueue() { |
|
||||||
for { |
|
||||||
select { |
|
||||||
case t := <-taskQueue: |
|
||||||
// Only support JSON now.
|
|
||||||
data, err := json.MarshalIndent(t.Payload, "", "\t") |
|
||||||
if err != nil { |
|
||||||
log.Error("hooks.handleQueue(json): %v", err) |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
_, err = httplib.Post(t.Url).SetTimeout(5*time.Second, 5*time.Second). |
|
||||||
Body(data).Response() |
|
||||||
if err != nil { |
|
||||||
log.Error("hooks.handleQueue: Fail to deliver hook: %v", err) |
|
||||||
continue |
|
||||||
} |
|
||||||
log.Info("Hook delivered: %s", string(data)) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,89 @@ |
|||||||
|
// 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 process |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"os/exec" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/gogits/gogs/modules/log" |
||||||
|
) |
||||||
|
|
||||||
|
// Process represents a working process inherit from Gogs.
|
||||||
|
type Process struct { |
||||||
|
Pid int64 // Process ID, not system one.
|
||||||
|
Description string |
||||||
|
Start time.Time |
||||||
|
Cmd *exec.Cmd |
||||||
|
} |
||||||
|
|
||||||
|
// List of existing processes.
|
||||||
|
var ( |
||||||
|
curPid int64 = 1 |
||||||
|
Processes []*Process |
||||||
|
) |
||||||
|
|
||||||
|
// Add adds a existing process and returns its PID.
|
||||||
|
func Add(desc string, cmd *exec.Cmd) int64 { |
||||||
|
pid := curPid |
||||||
|
Processes = append(Processes, &Process{ |
||||||
|
Pid: pid, |
||||||
|
Description: desc, |
||||||
|
Start: time.Now(), |
||||||
|
Cmd: cmd, |
||||||
|
}) |
||||||
|
curPid++ |
||||||
|
return pid |
||||||
|
} |
||||||
|
|
||||||
|
func ExecDir(dir, desc, cmdName string, args ...string) (string, string, error) { |
||||||
|
bufOut := new(bytes.Buffer) |
||||||
|
bufErr := new(bytes.Buffer) |
||||||
|
|
||||||
|
cmd := exec.Command(cmdName, args...) |
||||||
|
cmd.Dir = dir |
||||||
|
cmd.Stdout = bufOut |
||||||
|
cmd.Stderr = bufErr |
||||||
|
|
||||||
|
pid := Add(desc, cmd) |
||||||
|
err := cmd.Run() |
||||||
|
if errKill := Kill(pid); errKill != nil { |
||||||
|
log.Error("Exec: %v", pid, desc, errKill) |
||||||
|
} |
||||||
|
return bufOut.String(), bufErr.String(), err |
||||||
|
} |
||||||
|
|
||||||
|
// Exec starts executing a command and record its process.
|
||||||
|
func Exec(desc, cmdName string, args ...string) (string, string, error) { |
||||||
|
return ExecDir("", desc, cmdName, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// Remove removes a process from list.
|
||||||
|
func Remove(pid int64) { |
||||||
|
for i, proc := range Processes { |
||||||
|
if proc.Pid == pid { |
||||||
|
Processes = append(Processes[:i], Processes[i+1:]...) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Kill kills and removes a process from list.
|
||||||
|
func Kill(pid int64) error { |
||||||
|
for i, proc := range Processes { |
||||||
|
if proc.Pid == pid { |
||||||
|
if proc.Cmd.Process != nil && proc.Cmd.ProcessState != nil && !proc.Cmd.ProcessState.Exited() { |
||||||
|
if err := proc.Cmd.Process.Kill(); err != nil { |
||||||
|
return fmt.Errorf("fail to kill process(%d/%s): %v", proc.Pid, proc.Description, err) |
||||||
|
} |
||||||
|
} |
||||||
|
Processes = append(Processes[:i], Processes[i+1:]...) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 248 KiB |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,205 @@ |
|||||||
|
// 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 org |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/go-martini/martini" |
||||||
|
|
||||||
|
"github.com/gogits/gogs/models" |
||||||
|
"github.com/gogits/gogs/modules/auth" |
||||||
|
"github.com/gogits/gogs/modules/base" |
||||||
|
"github.com/gogits/gogs/modules/log" |
||||||
|
"github.com/gogits/gogs/modules/middleware" |
||||||
|
"github.com/gogits/gogs/routers/user" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
NEW base.TplName = "org/new" |
||||||
|
SETTINGS base.TplName = "org/settings" |
||||||
|
) |
||||||
|
|
||||||
|
func Organization(ctx *middleware.Context, params martini.Params) { |
||||||
|
ctx.Data["Title"] = "Organization " + params["org"] |
||||||
|
ctx.HTML(200, "org/org") |
||||||
|
} |
||||||
|
|
||||||
|
func Members(ctx *middleware.Context, params martini.Params) { |
||||||
|
ctx.Data["Title"] = "Organization " + params["org"] + " Members" |
||||||
|
ctx.HTML(200, "org/members") |
||||||
|
} |
||||||
|
|
||||||
|
func New(ctx *middleware.Context) { |
||||||
|
ctx.Data["Title"] = "Create An Organization" |
||||||
|
ctx.HTML(200, NEW) |
||||||
|
} |
||||||
|
|
||||||
|
func NewPost(ctx *middleware.Context, form auth.CreateOrgForm) { |
||||||
|
ctx.Data["Title"] = "Create An Organization" |
||||||
|
|
||||||
|
if ctx.HasError() { |
||||||
|
ctx.HTML(200, NEW) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
org := &models.User{ |
||||||
|
Name: form.OrgName, |
||||||
|
Email: form.Email, |
||||||
|
IsActive: true, // NOTE: may need to set false when require e-mail confirmation.
|
||||||
|
Type: models.ORGANIZATION, |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
if org, err = models.CreateOrganization(org, ctx.User); err != nil { |
||||||
|
switch err { |
||||||
|
case models.ErrUserAlreadyExist: |
||||||
|
ctx.Data["Err_OrgName"] = true |
||||||
|
ctx.RenderWithErr("Organization name has been already taken", NEW, &form) |
||||||
|
case models.ErrEmailAlreadyUsed: |
||||||
|
ctx.Data["Err_Email"] = true |
||||||
|
ctx.RenderWithErr("E-mail address has been already used", NEW, &form) |
||||||
|
case models.ErrUserNameIllegal: |
||||||
|
ctx.Data["Err_OrgName"] = true |
||||||
|
ctx.RenderWithErr(models.ErrRepoNameIllegal.Error(), NEW, &form) |
||||||
|
default: |
||||||
|
ctx.Handle(500, "user.NewPost(CreateUser)", err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
log.Trace("%s Organization created: %s", ctx.Req.RequestURI, org.Name) |
||||||
|
|
||||||
|
ctx.Redirect("/org/" + form.OrgName + "/dashboard") |
||||||
|
} |
||||||
|
|
||||||
|
func Dashboard(ctx *middleware.Context, params martini.Params) { |
||||||
|
ctx.Data["Title"] = "Dashboard" |
||||||
|
ctx.Data["PageIsUserDashboard"] = true |
||||||
|
ctx.Data["PageIsOrgDashboard"] = true |
||||||
|
|
||||||
|
org, err := models.GetUserByName(params["org"]) |
||||||
|
if err != nil { |
||||||
|
if err == models.ErrUserNotExist { |
||||||
|
ctx.Handle(404, "org.Dashboard(GetUserByName)", err) |
||||||
|
} else { |
||||||
|
ctx.Handle(500, "org.Dashboard(GetUserByName)", err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if err := ctx.User.GetOrganizations(); err != nil { |
||||||
|
ctx.Handle(500, "home.Dashboard(GetOrganizations)", err) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.Data["Orgs"] = ctx.User.Orgs |
||||||
|
ctx.Data["ContextUser"] = org |
||||||
|
|
||||||
|
ctx.Data["MyRepos"], err = models.GetRepositories(org.Id, true) |
||||||
|
if err != nil { |
||||||
|
ctx.Handle(500, "org.Dashboard(GetRepositories)", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
actions, err := models.GetFeeds(org.Id, 0, false) |
||||||
|
if err != nil { |
||||||
|
ctx.Handle(500, "org.Dashboard(GetFeeds)", err) |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.Data["Feeds"] = actions |
||||||
|
|
||||||
|
ctx.HTML(200, user.DASHBOARD) |
||||||
|
} |
||||||
|
|
||||||
|
func Settings(ctx *middleware.Context, params martini.Params) { |
||||||
|
ctx.Data["Title"] = "Settings" |
||||||
|
|
||||||
|
org, err := models.GetUserByName(params["org"]) |
||||||
|
if err != nil { |
||||||
|
if err == models.ErrUserNotExist { |
||||||
|
ctx.Handle(404, "org.Settings(GetUserByName)", err) |
||||||
|
} else { |
||||||
|
ctx.Handle(500, "org.Settings(GetUserByName)", err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.Data["Org"] = org |
||||||
|
|
||||||
|
ctx.HTML(200, SETTINGS) |
||||||
|
} |
||||||
|
|
||||||
|
func SettingsPost(ctx *middleware.Context, params martini.Params, form auth.OrgSettingForm) { |
||||||
|
ctx.Data["Title"] = "Settings" |
||||||
|
|
||||||
|
org, err := models.GetUserByName(params["org"]) |
||||||
|
if err != nil { |
||||||
|
if err == models.ErrUserNotExist { |
||||||
|
ctx.Handle(404, "org.SettingsPost(GetUserByName)", err) |
||||||
|
} else { |
||||||
|
ctx.Handle(500, "org.SettingsPost(GetUserByName)", err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.Data["Org"] = org |
||||||
|
|
||||||
|
if ctx.HasError() { |
||||||
|
ctx.HTML(200, SETTINGS) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
org.FullName = form.DisplayName |
||||||
|
org.Email = form.Email |
||||||
|
org.Description = form.Description |
||||||
|
org.Website = form.Website |
||||||
|
org.Location = form.Location |
||||||
|
if err = models.UpdateUser(org); err != nil { |
||||||
|
ctx.Handle(500, "org.SettingsPost(UpdateUser)", err) |
||||||
|
return |
||||||
|
} |
||||||
|
log.Trace("%s Organization setting updated: %s", ctx.Req.RequestURI, org.LowerName) |
||||||
|
ctx.Flash.Success("Organization profile has been successfully updated.") |
||||||
|
ctx.Redirect("/org/" + org.Name + "/settings") |
||||||
|
} |
||||||
|
|
||||||
|
func DeletePost(ctx *middleware.Context, params martini.Params) { |
||||||
|
ctx.Data["Title"] = "Settings" |
||||||
|
|
||||||
|
org, err := models.GetUserByName(params["org"]) |
||||||
|
if err != nil { |
||||||
|
if err == models.ErrUserNotExist { |
||||||
|
ctx.Handle(404, "org.DeletePost(GetUserByName)", err) |
||||||
|
} else { |
||||||
|
ctx.Handle(500, "org.DeletePost(GetUserByName)", err) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
ctx.Data["Org"] = org |
||||||
|
|
||||||
|
if !models.IsOrganizationOwner(org.Id, ctx.User.Id) { |
||||||
|
ctx.Error(403) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
tmpUser := models.User{ |
||||||
|
Passwd: ctx.Query("password"), |
||||||
|
Salt: ctx.User.Salt, |
||||||
|
} |
||||||
|
tmpUser.EncodePasswd() |
||||||
|
if tmpUser.Passwd != ctx.User.Passwd { |
||||||
|
ctx.Flash.Error("Password is not correct. Make sure you are owner of this account.") |
||||||
|
} else { |
||||||
|
if err := models.DeleteOrganization(org); err != nil { |
||||||
|
switch err { |
||||||
|
case models.ErrUserOwnRepos: |
||||||
|
ctx.Flash.Error("This organization still have ownership of repository, you have to delete or transfer them first.") |
||||||
|
default: |
||||||
|
ctx.Handle(500, "org.DeletePost(DeleteOrganization)", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} else { |
||||||
|
ctx.Redirect("/") |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ctx.Redirect("/org/" + org.Name + "/settings") |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package org |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/go-martini/martini" |
||||||
|
"github.com/gogits/gogs/modules/middleware" |
||||||
|
) |
||||||
|
|
||||||
|
func Teams(ctx *middleware.Context, params martini.Params) { |
||||||
|
ctx.Data["Title"] = "Organization "+params["org"]+" Teams" |
||||||
|
ctx.HTML(200, "org/teams") |
||||||
|
} |
||||||
|
|
||||||
|
func NewTeam(ctx *middleware.Context, params martini.Params) { |
||||||
|
ctx.Data["Title"] = "Organization "+params["org"]+" New Team" |
||||||
|
ctx.HTML(200, "org/new_team") |
||||||
|
} |
||||||
|
|
||||||
|
func EditTeam(ctx *middleware.Context, params martini.Params){ |
||||||
|
ctx.Data["Title"] = "Organization "+params["org"]+" Edit Team" |
||||||
|
ctx.HTML(200,"org/edit_team") |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
{{template "base/navbar" .}} |
||||||
|
<div id="body" class="container" data-page="admin"> |
||||||
|
{{template "admin/nav" .}} |
||||||
|
<div id="admin-container" class="col-md-10"> |
||||||
|
<ul class="nav nav-tabs"> |
||||||
|
<li{{if .PageIsMonitorCron}} class="active"{{end}}><a href="/admin/monitor">Cron Tasks</a></li> |
||||||
|
<li{{if .PageIsMonitorProcess}} class="active"{{end}}><a href="/admin/monitor?tab=process">Processes</a></li> |
||||||
|
</ul> |
||||||
|
<div class="panel panel-default"> |
||||||
|
<div class="panel-body"> |
||||||
|
{{if .PageIsMonitorCron}} |
||||||
|
<table class="table table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Name</th> |
||||||
|
<th>Schedule</th> |
||||||
|
<th>Next Time</th> |
||||||
|
<th>Previous Time</th> |
||||||
|
<th>Execute Times</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{range .Entries}} |
||||||
|
<tr> |
||||||
|
<td>{{.Description}}</td> |
||||||
|
<td>{{.Spec}}</td> |
||||||
|
<td>{{.Next}}</td> |
||||||
|
<td>{{.Prev}}</td> |
||||||
|
<td>{{.ExecTimes}}</td> |
||||||
|
</tr> |
||||||
|
{{end}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,38 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
{{template "base/navbar" .}} |
||||||
|
<div id="body" class="container" data-page="admin"> |
||||||
|
{{template "admin/nav" .}} |
||||||
|
<div id="admin-container" class="col-md-10"> |
||||||
|
<ul class="nav nav-tabs"> |
||||||
|
<li{{if .PageIsMonitorCron}} class="active"{{end}}><a href="/admin/monitor">Cron Tasks</a></li> |
||||||
|
<li{{if .PageIsMonitorProcess}} class="active"{{end}}><a href="/admin/monitor?tab=process">Processes</a></li> |
||||||
|
</ul> |
||||||
|
<div class="panel panel-default"> |
||||||
|
<div class="panel-body"> |
||||||
|
{{if .PageIsMonitorProcess}} |
||||||
|
<table class="table table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Pid</th> |
||||||
|
<th>Description</th> |
||||||
|
<th>Start Time</th> |
||||||
|
<th>Execution Time</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{range .Processes}} |
||||||
|
<tr> |
||||||
|
<td>{{.Pid}}</td> |
||||||
|
<td>{{.Description}}</td> |
||||||
|
<td>{{.Start}}</td> |
||||||
|
<td>{{TimeSince .Start}}</td> |
||||||
|
</tr> |
||||||
|
{{end}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
{{end}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,75 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
{{template "base/navbar" .}} |
||||||
|
<div id="body-nav" class="org-nav org-nav-auto"> |
||||||
|
<div class="container clearfix"> |
||||||
|
<div id="org-nav-wrapper"> |
||||||
|
<ul class="nav nav-pills pull-right"> |
||||||
|
<li><a href="#"><i class="fa fa-users"></i>Members |
||||||
|
<span class="label label-default">5</span></a> |
||||||
|
</li> |
||||||
|
<li class="active"><a href="#"><i class="fa fa-tags"></i>Teams |
||||||
|
<span class="label label-default">2</span></a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
<img class="pull-left org-small-logo" src="https://avatars3.githubusercontent.com/u/6656686?s=140" alt="" width="60"/> |
||||||
|
<div id="org-nav-info"> |
||||||
|
<h2 class="org-name">Organization Name</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div id="body" class="container"> |
||||||
|
<div id="org"> |
||||||
|
<form id="org-teams-edit" class="form-horizontal card"> |
||||||
|
<h3>Edit team</h3> |
||||||
|
<div class="form-group"> |
||||||
|
<label class="col-md-2 control-label">Team Name<strong class="text-danger">*</strong></label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<input name="team" type="text" class="form-control" placeholder="Type your team name" value="" required="required"> |
||||||
|
<span class="help-block">You'll use this name to mention this team in conversations.</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form-group"> |
||||||
|
<label class="col-md-2 control-label">Description</label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<input name="desc" type="text" class="form-control" placeholder="Type your team description (optional)" value=""> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form-group"> |
||||||
|
<label class="col-md-2 control-label">Permission</label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<div class="radio"> |
||||||
|
<label> |
||||||
|
<input type="radio" name="permission" value="pull" checked=""> |
||||||
|
<strong>Read & Clone</strong> |
||||||
|
</label> |
||||||
|
<p>This team will be able to view and clone its repositories.</p> |
||||||
|
</div> |
||||||
|
<div class="radio"> |
||||||
|
<label> |
||||||
|
<input type="radio" name="permission" value="push"> |
||||||
|
<strong>Push, Read & Clone</strong> |
||||||
|
</label> |
||||||
|
<p>This team will be able to read its repositories, as well as push to them.</p> |
||||||
|
</div> |
||||||
|
<div class="radio"> |
||||||
|
<label> |
||||||
|
<input type="radio" name="permission" value="admin"> |
||||||
|
<strong>Collaboration, Push, Read & Clone</strong> |
||||||
|
</label> |
||||||
|
<p>This team will be able to push/pull to its repositories, as well as add other collaborators to them.</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr/> |
||||||
|
<div class="form-group"> |
||||||
|
<label class="col-md-2"> </label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<button class="btn btn-primary">Edit this team</button> |
||||||
|
<button class="btn btn-danger pull-right" value="delete" name="delete">Delete this team</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,56 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
{{template "base/navbar" .}} |
||||||
|
<div id="body-nav" class="org-nav org-nav-auto"> |
||||||
|
<div class="container clearfix"> |
||||||
|
<div id="org-nav-wrapper"> |
||||||
|
<ul class="nav nav-pills pull-right"> |
||||||
|
<li class="active"><a href="#"><i class="fa fa-users"></i>Members |
||||||
|
<span class="label label-default">5</span></a> |
||||||
|
</li> |
||||||
|
<li><a href="#"><i class="fa fa-tags"></i>Teams |
||||||
|
<span class="label label-default">2</span></a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
<img class="pull-left org-small-logo" src="https://avatars3.githubusercontent.com/u/6656686?s=140" alt="" width="60"/> |
||||||
|
<div id="org-nav-info"> |
||||||
|
<h2 class="org-name">Organization Name</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div id="body" class="container"> |
||||||
|
<div id="org"> |
||||||
|
<div id="org-members"> |
||||||
|
<div class="member"> |
||||||
|
<div class="avatar col-md-1"> |
||||||
|
<img src="https://avatars3.githubusercontent.com/u/2142787?s=140" alt=""/> |
||||||
|
</div> |
||||||
|
<div class="name col-md-4"> |
||||||
|
<a href="#"><strong>fuxiaohei</strong><span class="nick">傅小黑</span></a> |
||||||
|
</div> |
||||||
|
<div class="role col-md-2 pull-right"> |
||||||
|
<strong>Member</strong> |
||||||
|
</div> |
||||||
|
<div class="status col-md-1 pull-right"> |
||||||
|
<strong>Public</strong> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="member"> |
||||||
|
<div class="avatar col-md-1"> |
||||||
|
<img src="https://avatars3.githubusercontent.com/u/2142787?s=140" alt=""/> |
||||||
|
</div> |
||||||
|
<div class="name col-md-4"> |
||||||
|
<a href="#"><strong>fuxiaohei</strong><span class="nick">傅小黑</span></a> |
||||||
|
</div> |
||||||
|
<div class="role col-md-2 pull-right"> |
||||||
|
<strong><i class="fa fa-user"></i>Owner</strong> |
||||||
|
</div> |
||||||
|
<div class="status col-md-1 pull-right"> |
||||||
|
<i class="fa fa-lock"></i>Private |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,32 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
{{template "base/navbar" .}} |
||||||
|
<div class="container" id="body"> |
||||||
|
<form action="/org/create" method="post" class="form-horizontal card" id="org-create"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<h3>Create New Organization</h3> |
||||||
|
{{template "base/alert" .}} |
||||||
|
<div class="form-group {{if .Err_OrgName}}has-error has-feedback{{end}}"> |
||||||
|
<label class="col-md-2 control-label">Organization<strong class="text-danger">*</strong></label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<input name="orgname" type="text" class="form-control" placeholder="Type your organization name" value="{{.orgname}}" required="required"> |
||||||
|
<span class="help-block">Great organization names are short and memorable. </span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group {{if .Err_Email}}has-error has-feedback{{end}}"> |
||||||
|
<label class="col-md-2 control-label">Email<strong class="text-danger">*</strong></label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<input name="email" type="text" class="form-control" placeholder="Type organization's email" value="{{.email}}" required="required"> |
||||||
|
<span class="help-block">Organization's Email receives all notifications and confirmations.</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<div class="col-md-offset-2 col-md-8"> |
||||||
|
<button type="submit" class="btn btn-lg btn-primary">Create An Organization</button> |
||||||
|
<a href="/" class="text-danger">Cancel</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,74 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
{{template "base/navbar" .}} |
||||||
|
<div id="body-nav" class="org-nav org-nav-auto"> |
||||||
|
<div class="container clearfix"> |
||||||
|
<div id="org-nav-wrapper"> |
||||||
|
<ul class="nav nav-pills pull-right"> |
||||||
|
<li><a href="#"><i class="fa fa-users"></i>Members |
||||||
|
<span class="label label-default">5</span></a> |
||||||
|
</li> |
||||||
|
<li class="active"><a href="#"><i class="fa fa-tags"></i>Teams |
||||||
|
<span class="label label-default">2</span></a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
<img class="pull-left org-small-logo" src="https://avatars3.githubusercontent.com/u/6656686?s=140" alt="" width="60"/> |
||||||
|
<div id="org-nav-info"> |
||||||
|
<h2 class="org-name">Organization Name</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div id="body" class="container"> |
||||||
|
<div id="org"> |
||||||
|
<form id="org-teams-create" class="form-horizontal card"> |
||||||
|
<h3>Create new team</h3> |
||||||
|
<div class="form-group"> |
||||||
|
<label class="col-md-2 control-label">Team Name<strong class="text-danger">*</strong></label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<input name="team" type="text" class="form-control" placeholder="Type your team name" value="" required="required"> |
||||||
|
<span class="help-block">You'll use this name to mention this team in conversations.</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form-group"> |
||||||
|
<label class="col-md-2 control-label">Description</label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<input name="desc" type="text" class="form-control" placeholder="Type your team description (optional)" value=""> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="form-group"> |
||||||
|
<label class="col-md-2 control-label">Permission</label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<div class="radio"> |
||||||
|
<label> |
||||||
|
<input type="radio" name="permission" value="pull" checked=""> |
||||||
|
<strong>Read & Clone</strong> |
||||||
|
</label> |
||||||
|
<p>This team will be able to view and clone its repositories.</p> |
||||||
|
</div> |
||||||
|
<div class="radio"> |
||||||
|
<label> |
||||||
|
<input type="radio" name="permission" value="push"> |
||||||
|
<strong>Push, Read & Clone</strong> |
||||||
|
</label> |
||||||
|
<p>This team will be able to read its repositories, as well as push to them.</p> |
||||||
|
</div> |
||||||
|
<div class="radio"> |
||||||
|
<label> |
||||||
|
<input type="radio" name="permission" value="admin"> |
||||||
|
<strong>Collaboration, Push, Read & Clone</strong> |
||||||
|
</label> |
||||||
|
<p>This team will be able to push/pull to its repositories, as well as add other collaborators to them.</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr/> |
||||||
|
<div class="form-group"> |
||||||
|
<label class="col-md-2"> </label> |
||||||
|
<div class="col-md-8"> |
||||||
|
<button class="btn btn-primary">Create team</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,85 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
{{template "base/navbar" .}} |
||||||
|
<div id="body-nav" class="org-nav"> |
||||||
|
<div class="container clearfix"> |
||||||
|
<div class="col-md-8" id="org-nav-wrapper"> |
||||||
|
<img class="pull-left org-logo" src="https://avatars3.githubusercontent.com/u/6656686?s=140" alt="" width="100"/> |
||||||
|
<div id="org-nav-info"> |
||||||
|
<h2 class="org-name">Organization Name</h2> |
||||||
|
<p class="org-description">Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language.</p> |
||||||
|
<ul class="org-meta list-inline"> |
||||||
|
<li><i class="fa fa-link"></i><a href="#">http://gogs.io</a></li> |
||||||
|
<li><i class="fa fa-envelope"></i><a href="#">info@gogs.io</a></li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div id="body" class="container"> |
||||||
|
<div id="org"> |
||||||
|
<div class="org-main col-md-8"> |
||||||
|
<div class="org-toolbar clearfix"> |
||||||
|
<button class="btn pull-right btn-success"><i class="fa fa-plus"></i> New Repository</button> |
||||||
|
</div> |
||||||
|
<hr style="width: 100%;border-color: #DDD"/> |
||||||
|
<div class="org-repo-list" id="org-repo-list"> |
||||||
|
<div class="org-repo-item"> |
||||||
|
<div class="org-repo-status pull-right"> |
||||||
|
<ul class="list-inline"> |
||||||
|
<li><strong>Go</strong></li> |
||||||
|
<li><i class="i fa fa-star"></i><strong>6</strong></li> |
||||||
|
<li><i class="fa fa-code-fork"></i><strong>2</strong></li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<h3 class="org-repo-name"><a href="#">gogs</a></h3> |
||||||
|
<p class="org-repo-description">Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language.</p> |
||||||
|
<p class="org-repo-update">Updated 17 hours ago</p> |
||||||
|
</div> |
||||||
|
<div class="org-repo-item"> |
||||||
|
<div class="org-repo-status pull-right"> |
||||||
|
<ul class="list-inline"> |
||||||
|
<li><strong>Go</strong></li> |
||||||
|
<li><i class="i fa fa-star"></i><strong>6</strong></li> |
||||||
|
<li><i class="fa fa-code-fork"></i><strong>2</strong></li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<h3 class="org-repo-name"><a href="#">gogs</a></h3> |
||||||
|
<p class="org-repo-description">Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language.</p> |
||||||
|
<p class="org-repo-update">Updated 17 hours ago</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="org-sidebar col-md-4"> |
||||||
|
<div class="org-panel panel panel-default" id="org-sidebar-members"> |
||||||
|
<div class="panel-heading"><strong>Members</strong></div> |
||||||
|
<div class="panel-body"> |
||||||
|
<a class="org-member" href="#" data-toggle="tooltip" title="username" data-placement="bottom"><img src="https://avatars3.githubusercontent.com/u/6656686?s=140" alt=""/></a> |
||||||
|
<a class="org-member" href="#" data-toggle="tooltip" title="username" data-placement="bottom"><img src="https://avatars3.githubusercontent.com/u/6656686?s=140" alt=""/></a> |
||||||
|
<a class="org-member" href="#" data-toggle="tooltip" title="username" data-placement="bottom"><img src="https://avatars3.githubusercontent.com/u/6656686?s=140" alt=""/></a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="org-panel panel panel-default" id="org-sidebar-teams"> |
||||||
|
<div class="panel-heading"><strong>Teams</strong></div> |
||||||
|
<div class="panel-body"> |
||||||
|
<div class="org-team"> |
||||||
|
<a href="#"> |
||||||
|
<p class="org-team-name"><strong>Team name</strong></p> |
||||||
|
<p class="org-team-meta"> |
||||||
|
4 members · 10 repositories |
||||||
|
</p> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class="org-team"> |
||||||
|
<a href="#"> |
||||||
|
<p class="org-team-name"><strong>Team name</strong></p> |
||||||
|
<p class="org-team-meta"> |
||||||
|
4 members · 10 repositories |
||||||
|
</p> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,130 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
{{template "base/navbar" .}} |
||||||
|
<div id="body-nav"> |
||||||
|
<div class="container"> |
||||||
|
<div class="btn-group pull-left" id="dashboard-switch"> |
||||||
|
<button type="button" class="btn btn-default"> |
||||||
|
<img src="{{.Org.AvatarLink}}?s=28" alt="user-avatar" title="username"> |
||||||
|
{{.Org.Name}} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<ul class="nav nav-pills pull-right"> |
||||||
|
<li><a href="/org/{{.Org.Name}}/dashboard/">News Feed</a></li> |
||||||
|
<li><a href="/org/{{.Org.Name}}/dashboard/issues">Issues</a></li> |
||||||
|
<li class="active"><a href="/org/{{.Org.Name}}/settings">Settings</a></li> |
||||||
|
<!-- <li><a href="/pulls">Pull Requests</a></li> |
||||||
|
<li><a href="/stars">Stars</a></li> --> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div id="body" class="container" data-page="org"> |
||||||
|
<div id="user-setting-nav" class="col-md-2 repo-setting-nav"> |
||||||
|
<ul class="list-group"> |
||||||
|
<li class="list-group-item active"><a href="#">Options</a></li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<div id="repo-setting-container" class="col-md-10"> |
||||||
|
{{template "base/alert" .}} |
||||||
|
<div class="panel panel-default"> |
||||||
|
<div class="panel-heading"> |
||||||
|
Organization Options |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="panel-body"> |
||||||
|
<form action="/org/{{.Org.Name}}/settings" method="post" class="form-horizontal"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<input type="hidden" name="action" value="update"> |
||||||
|
|
||||||
|
<div class="form-group{{if .Err_DisplayName}} has-error has-feedback{{end}}"> |
||||||
|
<label class="col-md-3 text-right" for="org-setting-name">Display Name</label> |
||||||
|
<div class="col-md-9"> |
||||||
|
<input class="form-control" name="display_name" value="{{.Org.FullName}}" title="" id="org-setting-name"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group{{if .Err_Email}} has-error has-feedback{{end}}"> |
||||||
|
<label class="col-md-3 text-right" for="org-email">Email</label> |
||||||
|
<div class="col-md-9"> |
||||||
|
<input class="form-control" name="email" value="{{.Org.Email}}" title="" id="org-email" type="email"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group{{if .Err_Description}} has-error has-feedback{{end}}"> |
||||||
|
<label class="col-md-3 text-right" for="org-desc">Description</label> |
||||||
|
<div class="col-md-9"> |
||||||
|
<textarea class="form-control" name="desc" id="org-desc" rows="3">{{.Org.Description}}</textarea> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group{{if .Err_Website}} has-error has-feedback{{end}}"> |
||||||
|
<label class="col-md-3 text-right" for="org-site">Official Site</label> |
||||||
|
<div class="col-md-9"> |
||||||
|
<input type="url" class="form-control" name="site" value="{{.Org.Website}}" id="org-site"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group{{if .Err_Location}} has-error has-feedback{{end}}"> |
||||||
|
<label class="col-md-3 text-right" for="org-location">Location</label> |
||||||
|
<div class="col-md-9"> |
||||||
|
<input class="form-control" name="location" value="{{.Org.Location}}" title="" id="org-location"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<div class="col-md-9 col-md-offset-3"> |
||||||
|
<button class="btn btn-primary" type="submit">Save Options</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="panel panel-warning"> |
||||||
|
<div class="panel-heading"> |
||||||
|
Danger Zone |
||||||
|
</div> |
||||||
|
<div class="panel-body"> |
||||||
|
<button type="button" class="btn btn-default pull-right" href="#delete-org-modal" data-toggle="modal"> |
||||||
|
Delete this organization |
||||||
|
</button> |
||||||
|
<dd> |
||||||
|
<dt>Delete this organization</dt> |
||||||
|
<dl>Once you delete this organization and all repositories in, there is no going back. Please be |
||||||
|
certain. |
||||||
|
</dl> |
||||||
|
</dd> |
||||||
|
|
||||||
|
<div class="modal fade" id="delete-org-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" |
||||||
|
aria-hidden="true"> |
||||||
|
<div class="modal-dialog"> |
||||||
|
<form action="/org/{{.Org.Name}}/settings/delete" method="post" |
||||||
|
class="modal-content"> |
||||||
|
{{.CsrfTokenHtml}} |
||||||
|
<div class="modal-header"> |
||||||
|
<button type="button" class="close" data-dismiss="modal" |
||||||
|
aria-hidden="true">×</button> |
||||||
|
<h4 class="modal-title" id="myModalLabel">Delete organization</h4> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="modal-body"> |
||||||
|
<div class="form-group"> |
||||||
|
<label>Make sure your are owner of this organization. Please enter your password.<strong class="text-danger">*</strong></label> |
||||||
|
<input name="password" class="form-control" type="password" placeholder="Type your account password" required="required"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> |
||||||
|
<button class="btn btn-danger btn-lg">I understand the consequences, delete this |
||||||
|
organization |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
@ -0,0 +1,71 @@ |
|||||||
|
{{template "base/head" .}} |
||||||
|
{{template "base/navbar" .}} |
||||||
|
<div id="body-nav" class="org-nav org-nav-auto"> |
||||||
|
<div class="container clearfix"> |
||||||
|
<div id="org-nav-wrapper"> |
||||||
|
<ul class="nav nav-pills pull-right"> |
||||||
|
<li><a href="#"><i class="fa fa-users"></i>Members |
||||||
|
<span class="label label-default">5</span></a> |
||||||
|
</li> |
||||||
|
<li class="active"><a href="#"><i class="fa fa-tags"></i>Teams |
||||||
|
<span class="label label-default">2</span></a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
<img class="pull-left org-small-logo" src="https://avatars3.githubusercontent.com/u/6656686?s=140" alt="" width="60"/> |
||||||
|
<div id="org-nav-info"> |
||||||
|
<h2 class="org-name">Organization Name</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div id="body" class="container"> |
||||||
|
<div id="org"> |
||||||
|
<div id="org-teams"> |
||||||
|
<div id="org-teams-action"> |
||||||
|
<div class="col-md-12"> |
||||||
|
<a href="#"><button class="btn btn-success"><i class="fa fa-plus-square"></i>New Team</button></a> |
||||||
|
<hr/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="org-team col-md-6"> |
||||||
|
<div class="panel panel-default"> |
||||||
|
<h2 class="panel-heading org-team-name"><a href="#"><strong>Team Name</strong></a></h2> |
||||||
|
<div class="panel-body"> |
||||||
|
<p class="org-team-meta">4 members · 10 repositories</p> |
||||||
|
<p class="org-team-members"> |
||||||
|
<a href="#"> |
||||||
|
<img class="img-thumbnail" src="https://avatars2.githubusercontent.com/u/2946214?s=60" alt=""/> |
||||||
|
</a> |
||||||
|
<a href="#"> |
||||||
|
<img class="img-thumbnail" src="https://avatars2.githubusercontent.com/u/2946214?s=60" alt=""/> |
||||||
|
</a> |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div class="panel-footer"> |
||||||
|
<button class="pull-right btn btn-default">Join</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="org-team col-md-6"> |
||||||
|
<div class="panel panel-default"> |
||||||
|
<h2 class="panel-heading org-team-name"><a href="#"><strong>Team Name</strong></a></h2> |
||||||
|
<div class="panel-body"> |
||||||
|
<p class="org-team-meta">4 members · 10 repositories</p> |
||||||
|
<p class="org-team-members"> |
||||||
|
<a href="#"> |
||||||
|
<img class="img-thumbnail" src="https://avatars2.githubusercontent.com/u/2946214?s=60" alt=""/> |
||||||
|
</a> |
||||||
|
<a href="#"> |
||||||
|
<img class="img-thumbnail" src="https://avatars2.githubusercontent.com/u/2946214?s=60" alt=""/> |
||||||
|
</a> |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div class="panel-footer"> |
||||||
|
<button class="pull-right btn btn-danger">Leave</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "base/footer" .}} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue