Move status table to cron package (#7370)

tokarchuk/v1.17
Lunny Xiao 5 years ago committed by techknowlogick
parent 760c473896
commit d2958d9f46
  1. 4
      go.mod
  2. 11
      go.sum
  3. 5
      models/branches.go
  4. 25
      models/repo.go
  5. 5
      models/repo_mirror.go
  6. 7
      models/user.go
  7. 56
      modules/cron/cron.go
  8. 234
      vendor/github.com/gogits/cron/parser.go
  9. 0
      vendor/github.com/gogs/cron/.gitignore
  10. 0
      vendor/github.com/gogs/cron/.travis.yml
  11. 0
      vendor/github.com/gogs/cron/LICENSE
  12. 0
      vendor/github.com/gogs/cron/README.md
  13. 0
      vendor/github.com/gogs/cron/constantdelay.go
  14. 92
      vendor/github.com/gogs/cron/cron.go
  15. 8
      vendor/github.com/gogs/cron/doc.go
  16. 380
      vendor/github.com/gogs/cron/parser.go
  17. 1
      vendor/github.com/gogs/cron/spec.go
  18. 54
      vendor/golang.org/x/oauth2/README.md
  19. 10
      vendor/golang.org/x/oauth2/go.mod
  20. 12
      vendor/golang.org/x/oauth2/go.sum
  21. 216
      vendor/golang.org/x/oauth2/internal/token.go
  22. 53
      vendor/golang.org/x/oauth2/oauth2.go
  23. 7
      vendor/golang.org/x/oauth2/token.go
  24. 6
      vendor/modules.txt

@ -57,7 +57,7 @@ require (
github.com/go-xorm/core v0.6.0 // indirect github.com/go-xorm/core v0.6.0 // indirect
github.com/go-xorm/xorm v0.7.3-0.20190620151208-f1b4f8368459 github.com/go-xorm/xorm v0.7.3-0.20190620151208-f1b4f8368459
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561 github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561
github.com/gogits/cron v0.0.0-20160810035002-7f3990acf183 github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14
github.com/google/go-cmp v0.3.0 // indirect github.com/google/go-cmp v0.3.0 // indirect
github.com/google/go-github/v24 v24.0.1 github.com/google/go-github/v24 v24.0.1
github.com/gorilla/context v1.1.1 github.com/gorilla/context v1.1.1
@ -111,7 +111,7 @@ require (
go.etcd.io/bbolt v1.3.2 // indirect go.etcd.io/bbolt v1.3.2 // indirect
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b
golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
golang.org/x/sys v0.0.0-20190620070143-6f217b454f45 golang.org/x/sys v0.0.0-20190620070143-6f217b454f45
golang.org/x/text v0.3.2 golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20190620154339-431033348dd0 // indirect golang.org/x/tools v0.0.0-20190620154339-431033348dd0 // indirect

@ -1,5 +1,4 @@
cloud.google.com/go v0.30.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.30.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -141,9 +140,9 @@ github.com/go-xorm/xorm v0.7.3-0.20190620151208-f1b4f8368459 h1:JGEuhH169J7Wtm1h
github.com/go-xorm/xorm v0.7.3-0.20190620151208-f1b4f8368459/go.mod h1:UK1YDlWscDspd23xW9HC24749jhvwO6riZ/HUt3gbHQ= github.com/go-xorm/xorm v0.7.3-0.20190620151208-f1b4f8368459/go.mod h1:UK1YDlWscDspd23xW9HC24749jhvwO6riZ/HUt3gbHQ=
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561 h1:deE7ritpK04PgtpyVOS2TYcQEld9qLCD5b5EbVNOuLA= github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561 h1:deE7ritpK04PgtpyVOS2TYcQEld9qLCD5b5EbVNOuLA=
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:YgYOrVn3Nj9Tq0EvjmFbphRytDj7JNRoWSStJZWDJTQ= github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:YgYOrVn3Nj9Tq0EvjmFbphRytDj7JNRoWSStJZWDJTQ=
github.com/gogits/cron v0.0.0-20160810035002-7f3990acf183 h1:EBTlva3AOSb80G3JSwY6ZMdILEZJ1JKuewrbqrNjWuE=
github.com/gogits/cron v0.0.0-20160810035002-7f3990acf183/go.mod h1:pX+V62FFmklia2fhP3P4YSY6iJdPO5jIDKFQ5fEd5QE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 h1:yXtpJr/LV6PFu4nTLgfjQdcMdzjbqqXMEnHfq0Or6p8=
github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14/go.mod h1:jPoNZLWDAqA5N3G5amEoiNbhVrmM+ZQEcnQvNQ2KaZk=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
@ -220,7 +219,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lafriks/xormstore v1.0.0 h1:P/IJzNSIpjXl/Up3o2Td5ZU/x4v6DEKLMaPQJGtmJCk= github.com/lafriks/xormstore v1.0.0 h1:P/IJzNSIpjXl/Up3o2Td5ZU/x4v6DEKLMaPQJGtmJCk=
github.com/lafriks/xormstore v1.0.0/go.mod h1:dD8vHNRfEp3Uy+JvX9cMi2SXcRKJ0x4pYKsZuy843Ic= github.com/lafriks/xormstore v1.0.0/go.mod h1:dD8vHNRfEp3Uy+JvX9cMi2SXcRKJ0x4pYKsZuy843Ic=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4= github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -366,6 +364,7 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -373,8 +372,8 @@ golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b h1:lkjdUzSyJ5P1+eal9fxXX9Xg2
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759 h1:TMrx+Qdx7uJAeUbv15N72h5Hmyb5+VDjEiMufAEAM04= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

@ -458,11 +458,6 @@ func (deletedBranch *DeletedBranch) LoadUser() {
// RemoveOldDeletedBranches removes old deleted branches // RemoveOldDeletedBranches removes old deleted branches
func RemoveOldDeletedBranches() { func RemoveOldDeletedBranches() {
if !taskStatusTable.StartIfNotRunning(`deleted_branches_cleanup`) {
return
}
defer taskStatusTable.Stop(`deleted_branches_cleanup`)
log.Trace("Doing: DeletedBranchesCleanup") log.Trace("Doing: DeletedBranchesCleanup")
deleteBefore := time.Now().Add(-setting.Cron.DeletedBranchesCleanup.OlderThan) deleteBefore := time.Now().Add(-setting.Cron.DeletedBranchesCleanup.OlderThan)

@ -2056,11 +2056,6 @@ func DeleteRepositoryArchives() error {
// DeleteOldRepositoryArchives deletes old repository archives. // DeleteOldRepositoryArchives deletes old repository archives.
func DeleteOldRepositoryArchives() { func DeleteOldRepositoryArchives() {
if !taskStatusTable.StartIfNotRunning(archiveCleanup) {
return
}
defer taskStatusTable.Stop(archiveCleanup)
log.Trace("Doing: ArchiveCleanup") log.Trace("Doing: ArchiveCleanup")
if err := x.Where("id > 0").Iterate(new(Repository), deleteOldRepositoryArchives); err != nil { if err := x.Where("id > 0").Iterate(new(Repository), deleteOldRepositoryArchives); err != nil {
@ -2187,23 +2182,8 @@ func SyncRepositoryHooks() error {
}) })
} }
// Prevent duplicate running tasks.
var taskStatusTable = sync.NewStatusTable()
const (
mirrorUpdate = "mirror_update"
gitFsck = "git_fsck"
checkRepos = "check_repos"
archiveCleanup = "archive_cleanup"
)
// GitFsck calls 'git fsck' to check repository health. // GitFsck calls 'git fsck' to check repository health.
func GitFsck() { func GitFsck() {
if !taskStatusTable.StartIfNotRunning(gitFsck) {
return
}
defer taskStatusTable.Stop(gitFsck)
log.Trace("Doing: GitFsck") log.Trace("Doing: GitFsck")
if err := x. if err := x.
@ -2272,11 +2252,6 @@ func repoStatsCheck(checker *repoChecker) {
// CheckRepoStats checks the repository stats // CheckRepoStats checks the repository stats
func CheckRepoStats() { func CheckRepoStats() {
if !taskStatusTable.StartIfNotRunning(checkRepos) {
return
}
defer taskStatusTable.Stop(checkRepos)
log.Trace("Doing: CheckRepoStats") log.Trace("Doing: CheckRepoStats")
checkers := []*repoChecker{ checkers := []*repoChecker{

@ -315,11 +315,6 @@ func DeleteMirrorByRepoID(repoID int64) error {
// MirrorUpdate checks and updates mirror repositories. // MirrorUpdate checks and updates mirror repositories.
func MirrorUpdate() { func MirrorUpdate() {
if !taskStatusTable.StartIfNotRunning(mirrorUpdate) {
return
}
defer taskStatusTable.Stop(mirrorUpdate)
log.Trace("Doing: MirrorUpdate") log.Trace("Doing: MirrorUpdate")
if err := x. if err := x.

@ -60,8 +60,6 @@ const (
algoPbkdf2 = "pbkdf2" algoPbkdf2 = "pbkdf2"
) )
const syncExternalUsers = "sync_external_users"
var ( var (
// ErrUserNotKeyOwner user does not own this key error // ErrUserNotKeyOwner user does not own this key error
ErrUserNotKeyOwner = errors.New("User does not own this public key") ErrUserNotKeyOwner = errors.New("User does not own this public key")
@ -1643,11 +1641,6 @@ func synchronizeLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []str
// SyncExternalUsers is used to synchronize users with external authorization source // SyncExternalUsers is used to synchronize users with external authorization source
func SyncExternalUsers() { func SyncExternalUsers() {
if !taskStatusTable.StartIfNotRunning(syncExternalUsers) {
return
}
defer taskStatusTable.Stop(syncExternalUsers)
log.Trace("Doing: SyncExternalUsers") log.Trace("Doing: SyncExternalUsers")
ls, err := LoginSources() ls, err := LoginSources()

@ -1,4 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved. // Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,15 +8,42 @@ package cron
import ( import (
"time" "time"
"github.com/gogits/cron"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sync"
"github.com/gogs/cron"
)
const (
mirrorUpdate = "mirror_update"
gitFsck = "git_fsck"
checkRepos = "check_repos"
archiveCleanup = "archive_cleanup"
syncExternalUsers = "sync_external_users"
deletedBranchesCleanup = "deleted_branches_cleanup"
) )
var c = cron.New() var c = cron.New()
// Prevent duplicate running tasks.
var taskStatusTable = sync.NewStatusTable()
// Func defines a cron function body
type Func func()
// WithUnique wrap a cron func with an unique running check
func WithUnique(name string, body Func) Func {
return func() {
if !taskStatusTable.StartIfNotRunning(name) {
return
}
defer taskStatusTable.Stop(name)
body()
}
}
// NewContext begins cron tasks // NewContext begins cron tasks
func NewContext() { func NewContext() {
var ( var (
@ -23,69 +51,69 @@ func NewContext() {
err error err error
) )
if setting.Cron.UpdateMirror.Enabled { if setting.Cron.UpdateMirror.Enabled {
entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, models.MirrorUpdate) entry, err = c.AddFunc("Update mirrors", setting.Cron.UpdateMirror.Schedule, WithUnique(mirrorUpdate, models.MirrorUpdate))
if err != nil { if err != nil {
log.Fatal("Cron[Update mirrors]: %v", err) log.Fatal("Cron[Update mirrors]: %v", err)
} }
if setting.Cron.UpdateMirror.RunAtStart { if setting.Cron.UpdateMirror.RunAtStart {
entry.Prev = time.Now() entry.Prev = time.Now()
entry.ExecTimes++ entry.ExecTimes++
go models.MirrorUpdate() go WithUnique(mirrorUpdate, models.MirrorUpdate)()
} }
} }
if setting.Cron.RepoHealthCheck.Enabled { if setting.Cron.RepoHealthCheck.Enabled {
entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, models.GitFsck) entry, err = c.AddFunc("Repository health check", setting.Cron.RepoHealthCheck.Schedule, WithUnique(gitFsck, models.GitFsck))
if err != nil { if err != nil {
log.Fatal("Cron[Repository health check]: %v", err) log.Fatal("Cron[Repository health check]: %v", err)
} }
if setting.Cron.RepoHealthCheck.RunAtStart { if setting.Cron.RepoHealthCheck.RunAtStart {
entry.Prev = time.Now() entry.Prev = time.Now()
entry.ExecTimes++ entry.ExecTimes++
go models.GitFsck() go WithUnique(gitFsck, models.GitFsck)()
} }
} }
if setting.Cron.CheckRepoStats.Enabled { if setting.Cron.CheckRepoStats.Enabled {
entry, err = c.AddFunc("Check repository statistics", setting.Cron.CheckRepoStats.Schedule, models.CheckRepoStats) entry, err = c.AddFunc("Check repository statistics", setting.Cron.CheckRepoStats.Schedule, WithUnique(checkRepos, models.CheckRepoStats))
if err != nil { if err != nil {
log.Fatal("Cron[Check repository statistics]: %v", err) log.Fatal("Cron[Check repository statistics]: %v", err)
} }
if setting.Cron.CheckRepoStats.RunAtStart { if setting.Cron.CheckRepoStats.RunAtStart {
entry.Prev = time.Now() entry.Prev = time.Now()
entry.ExecTimes++ entry.ExecTimes++
go models.CheckRepoStats() go WithUnique(checkRepos, models.CheckRepoStats)()
} }
} }
if setting.Cron.ArchiveCleanup.Enabled { if setting.Cron.ArchiveCleanup.Enabled {
entry, err = c.AddFunc("Clean up old repository archives", setting.Cron.ArchiveCleanup.Schedule, models.DeleteOldRepositoryArchives) entry, err = c.AddFunc("Clean up old repository archives", setting.Cron.ArchiveCleanup.Schedule, WithUnique(archiveCleanup, models.DeleteOldRepositoryArchives))
if err != nil { if err != nil {
log.Fatal("Cron[Clean up old repository archives]: %v", err) log.Fatal("Cron[Clean up old repository archives]: %v", err)
} }
if setting.Cron.ArchiveCleanup.RunAtStart { if setting.Cron.ArchiveCleanup.RunAtStart {
entry.Prev = time.Now() entry.Prev = time.Now()
entry.ExecTimes++ entry.ExecTimes++
go models.DeleteOldRepositoryArchives() go WithUnique(archiveCleanup, models.DeleteOldRepositoryArchives)()
} }
} }
if setting.Cron.SyncExternalUsers.Enabled { if setting.Cron.SyncExternalUsers.Enabled {
entry, err = c.AddFunc("Synchronize external users", setting.Cron.SyncExternalUsers.Schedule, models.SyncExternalUsers) entry, err = c.AddFunc("Synchronize external users", setting.Cron.SyncExternalUsers.Schedule, WithUnique(syncExternalUsers, models.SyncExternalUsers))
if err != nil { if err != nil {
log.Fatal("Cron[Synchronize external users]: %v", err) log.Fatal("Cron[Synchronize external users]: %v", err)
} }
if setting.Cron.SyncExternalUsers.RunAtStart { if setting.Cron.SyncExternalUsers.RunAtStart {
entry.Prev = time.Now() entry.Prev = time.Now()
entry.ExecTimes++ entry.ExecTimes++
go models.SyncExternalUsers() go WithUnique(syncExternalUsers, models.SyncExternalUsers)()
} }
} }
if setting.Cron.DeletedBranchesCleanup.Enabled { if setting.Cron.DeletedBranchesCleanup.Enabled {
entry, err = c.AddFunc("Remove old deleted branches", setting.Cron.DeletedBranchesCleanup.Schedule, models.RemoveOldDeletedBranches) entry, err = c.AddFunc("Remove old deleted branches", setting.Cron.DeletedBranchesCleanup.Schedule, WithUnique(deletedBranchesCleanup, models.RemoveOldDeletedBranches))
if err != nil { if err != nil {
log.Fatal("Cron[Remove old deleted branches]: %v", err) log.Fatal("Cron[Remove old deleted branches]: %v", err)
} }
if setting.Cron.DeletedBranchesCleanup.RunAtStart { if setting.Cron.DeletedBranchesCleanup.RunAtStart {
entry.Prev = time.Now() entry.Prev = time.Now()
entry.ExecTimes++ entry.ExecTimes++
go models.RemoveOldDeletedBranches() go WithUnique(deletedBranchesCleanup, models.RemoveOldDeletedBranches)()
} }
} }
c.Start() c.Start()

@ -1,234 +0,0 @@
package cron
import (
"fmt"
"log"
"math"
"strconv"
"strings"
"time"
)
// Parse returns a new crontab schedule representing the given spec.
// It returns a descriptive error if the spec is not valid.
//
// It accepts
// - Full crontab specs, e.g. "* * * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
func Parse(spec string) (_ Schedule, err error) {
// Convert panics into errors
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("%v", recovered)
}
}()
if spec[0] == '@' {
return parseDescriptor(spec), nil
}
// Split on whitespace. We require 5 or 6 fields.
// (second) (minute) (hour) (day of month) (month) (day of week, optional)
fields := strings.Fields(spec)
if len(fields) != 5 && len(fields) != 6 {
log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
}
// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
if len(fields) == 5 {
fields = append(fields, "*")
}
schedule := &SpecSchedule{
Second: getField(fields[0], seconds),
Minute: getField(fields[1], minutes),
Hour: getField(fields[2], hours),
Dom: getField(fields[3], dom),
Month: getField(fields[4], months),
Dow: getField(fields[5], dow),
}
return schedule, nil
}
// getField returns an Int with the bits set representing all of the times that
// the field represents. A "field" is a comma-separated list of "ranges".
func getField(field string, r bounds) uint64 {
// list = range {"," range}
var bits uint64
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
for _, expr := range ranges {
bits |= getRange(expr, r)
}
return bits
}
// getRange returns the bits indicated by the given expression:
// number | number "-" number [ "/" number ]
func getRange(expr string, r bounds) uint64 {
var (
start, end, step uint
rangeAndStep = strings.Split(expr, "/")
lowAndHigh = strings.Split(rangeAndStep[0], "-")
singleDigit = len(lowAndHigh) == 1
)
var extra_star uint64
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
start = r.min
end = r.max
extra_star = starBit
} else {
start = parseIntOrName(lowAndHigh[0], r.names)
switch len(lowAndHigh) {
case 1:
end = start
case 2:
end = parseIntOrName(lowAndHigh[1], r.names)
default:
log.Panicf("Too many hyphens: %s", expr)
}
}
switch len(rangeAndStep) {
case 1:
step = 1
case 2:
step = mustParseInt(rangeAndStep[1])
// Special handling: "N/step" means "N-max/step".
if singleDigit {
end = r.max
}
default:
log.Panicf("Too many slashes: %s", expr)
}
if start < r.min {
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
}
if start > end {
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
}
if step == 0 {
log.Panicf("Step of range should be a positive number: %s", expr)
}
return getBits(start, end, step) | extra_star
}
// parseIntOrName returns the (possibly-named) integer contained in expr.
func parseIntOrName(expr string, names map[string]uint) uint {
if names != nil {
if namedInt, ok := names[strings.ToLower(expr)]; ok {
return namedInt
}
}
return mustParseInt(expr)
}
// mustParseInt parses the given expression as an int or panics.
func mustParseInt(expr string) uint {
num, err := strconv.Atoi(expr)
if err != nil {
log.Panicf("Failed to parse int from %s: %s", expr, err)
}
if num < 0 {
log.Panicf("Negative number (%d) not allowed: %s", num, expr)
}
return uint(num)
}
// getBits sets all bits in the range [min, max], modulo the given step size.
func getBits(min, max, step uint) uint64 {
var bits uint64
// If step is 1, use shifts.
if step == 1 {
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
}
// Else, use a simple loop.
for i := min; i <= max; i += step {
bits |= 1 << i
}
return bits
}
// all returns all bits within the given bounds. (plus the star bit)
func all(r bounds) uint64 {
return getBits(r.min, r.max, 1) | starBit
}
// parseDescriptor returns a pre-defined schedule for the expression, or panics
// if none matches.
func parseDescriptor(spec string) Schedule {
switch spec {
case "@yearly", "@annually":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: 1 << months.min,
Dow: all(dow),
}
case "@monthly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: all(months),
Dow: all(dow),
}
case "@weekly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: 1 << dow.min,
}
case "@daily", "@midnight":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
case "@hourly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: all(hours),
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
}
const every = "@every "
if strings.HasPrefix(spec, every) {
duration, err := time.ParseDuration(spec[len(every):])
if err != nil {
log.Panicf("Failed to parse duration %s: %s", spec, err)
}
return Every(duration)
}
log.Panicf("Unrecognized descriptor: %s", spec)
return nil
}

@ -1,5 +1,3 @@
// This library implements a cron spec parser and runner. See the README for
// more details.
package cron package cron
import ( import (
@ -19,6 +17,7 @@ type Cron struct {
snapshot chan []*Entry snapshot chan []*Entry
running bool running bool
ErrorLog *log.Logger ErrorLog *log.Logger
location *time.Location
} }
// Job is an interface for submitted cron jobs. // Job is an interface for submitted cron jobs.
@ -74,8 +73,13 @@ func (s byTime) Less(i, j int) bool {
return s[i].Next.Before(s[j].Next) return s[i].Next.Before(s[j].Next)
} }
// New returns a new Cron job runner. // New returns a new Cron job runner, in the Local time zone.
func New() *Cron { func New() *Cron {
return NewWithLocation(time.Now().Location())
}
// NewWithLocation returns a new Cron job runner.
func NewWithLocation(location *time.Location) *Cron {
return &Cron{ return &Cron{
entries: nil, entries: nil,
add: make(chan *Entry), add: make(chan *Entry),
@ -83,6 +87,7 @@ func New() *Cron {
snapshot: make(chan []*Entry), snapshot: make(chan []*Entry),
running: false, running: false,
ErrorLog: nil, ErrorLog: nil,
location: location,
} }
} }
@ -131,6 +136,11 @@ func (c *Cron) Entries() []*Entry {
return c.entrySnapshot() return c.entrySnapshot()
} }
// Location gets the time zone location
func (c *Cron) Location() *time.Location {
return c.location
}
// Start the cron scheduler in its own go-routine, or no-op if already started. // Start the cron scheduler in its own go-routine, or no-op if already started.
func (c *Cron) Start() { func (c *Cron) Start() {
if c.running { if c.running {
@ -140,6 +150,15 @@ func (c *Cron) Start() {
go c.run() go c.run()
} }
// Run the cron scheduler, or no-op if already running.
func (c *Cron) Run() {
if c.running {
return
}
c.running = true
c.run()
}
func (c *Cron) runWithRecovery(j Job) { func (c *Cron) runWithRecovery(j Job) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -152,11 +171,11 @@ func (c *Cron) runWithRecovery(j Job) {
j.Run() j.Run()
} }
// Run the scheduler.. this is private just due to the need to synchronize // Run the scheduler. this is private just due to the need to synchronize
// access to the 'running' state variable. // access to the 'running' state variable.
func (c *Cron) run() { func (c *Cron) run() {
// Figure out the next activation times for each entry. // Figure out the next activation times for each entry.
now := time.Now().Local() now := c.now()
for _, entry := range c.entries { for _, entry := range c.entries {
entry.Next = entry.Schedule.Next(now) entry.Next = entry.Schedule.Next(now)
} }
@ -165,45 +184,47 @@ func (c *Cron) run() {
// Determine the next entry to run. // Determine the next entry to run.
sort.Sort(byTime(c.entries)) sort.Sort(byTime(c.entries))
var effective time.Time var timer *time.Timer
if len(c.entries) == 0 || c.entries[0].Next.IsZero() { if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
// If there are no entries yet, just sleep - it still handles new entries // If there are no entries yet, just sleep - it still handles new entries
// and stop requests. // and stop requests.
effective = now.AddDate(10, 0, 0) timer = time.NewTimer(100000 * time.Hour)
} else { } else {
effective = c.entries[0].Next timer = time.NewTimer(c.entries[0].Next.Sub(now))
} }
timer := time.NewTimer(effective.Sub(now)) for {
select { select {
case now = <-timer.C: case now = <-timer.C:
// Run every entry whose next time was this effective time. now = now.In(c.location)
for _, e := range c.entries { // Run every entry whose next time was less than now
if e.Next != effective { for _, e := range c.entries {
break if e.Next.After(now) || e.Next.IsZero() {
break
}
go c.runWithRecovery(e.Job)
e.ExecTimes++
e.Prev = e.Next
e.Next = e.Schedule.Next(now)
} }
go c.runWithRecovery(e.Job)
e.ExecTimes++
e.Prev = e.Next
e.Next = e.Schedule.Next(now)
}
continue
case newEntry := <-c.add: case newEntry := <-c.add:
c.entries = append(c.entries, newEntry) timer.Stop()
newEntry.Next = newEntry.Schedule.Next(time.Now().Local()) now = c.now()
newEntry.Next = newEntry.Schedule.Next(now)
c.entries = append(c.entries, newEntry)
case <-c.snapshot: case <-c.snapshot:
c.snapshot <- c.entrySnapshot() c.snapshot <- c.entrySnapshot()
continue
case <-c.stop: case <-c.stop:
timer.Stop() timer.Stop()
return return
} }
// 'now' should be updated after newEntry and snapshot cases. break
now = time.Now().Local() }
timer.Stop()
} }
} }
@ -241,3 +262,8 @@ func (c *Cron) entrySnapshot() []*Entry {
} }
return entries return entries
} }
// now returns current time in c location
func (c *Cron) now() time.Time {
return time.Now().In(c.location)
}

@ -84,16 +84,16 @@ You may use one of several pre-defined schedules in place of a cron expression.
Intervals Intervals
You may also schedule a job to execute at fixed intervals. This is supported by You may also schedule a job to execute at fixed intervals, starting at the time it's added
formatting the cron spec like this: or cron is run. This is supported by formatting the cron spec like this:
@every <duration> @every <duration>
where "duration" is a string accepted by time.ParseDuration where "duration" is a string accepted by time.ParseDuration
(http://golang.org/pkg/time/#ParseDuration). (http://golang.org/pkg/time/#ParseDuration).
For example, "@every 1h30m10s" would indicate a schedule that activates every For example, "@every 1h30m10s" would indicate a schedule that activates immediately,
1 hour, 30 minutes, 10 seconds. and then every 1 hour, 30 minutes, 10 seconds.
Note: The interval does not take the job runtime into account. For example, 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, if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes,

@ -0,0 +1,380 @@
package cron
import (
"fmt"
"math"
"strconv"
"strings"
"time"
)
// Configuration options for creating a parser. Most options specify which
// fields should be included, while others enable features. If a field is not
// included the parser will assume a default value. These options do not change
// the order fields are parse in.
type ParseOption int
const (
Second ParseOption = 1 << iota // Seconds field, default 0
Minute // Minutes field, default 0
Hour // Hours field, default 0
Dom // Day of month field, default *
Month // Month field, default *
Dow // Day of week field, default *
DowOptional // Optional day of week field, default *
Descriptor // Allow descriptors such as @monthly, @weekly, etc.
)
var places = []ParseOption{
Second,
Minute,
Hour,
Dom,
Month,
Dow,
}
var defaults = []string{
"0",
"0",
"0",
"*",
"*",
"*",
}
// A custom Parser that can be configured.
type Parser struct {
options ParseOption
optionals int
}
// Creates a custom Parser with custom options.
//
// // Standard parser without descriptors
// specParser := NewParser(Minute | Hour | Dom | Month | Dow)
// sched, err := specParser.Parse("0 0 15 */3 *")
//
// // Same as above, just excludes time fields
// subsParser := NewParser(Dom | Month | Dow)
// sched, err := specParser.Parse("15 */3 *")
//
// // Same as above, just makes Dow optional
// subsParser := NewParser(Dom | Month | DowOptional)
// sched, err := specParser.Parse("15 */3")
//
func NewParser(options ParseOption) Parser {
optionals := 0
if options&DowOptional > 0 {
options |= Dow
optionals++
}
return Parser{options, optionals}
}
// Parse returns a new crontab schedule representing the given spec.
// It returns a descriptive error if the spec is not valid.
// It accepts crontab specs and features configured by NewParser.
func (p Parser) Parse(spec string) (Schedule, error) {
if len(spec) == 0 {
return nil, fmt.Errorf("Empty spec string")
}
if spec[0] == '@' && p.options&Descriptor > 0 {
return parseDescriptor(spec)
}
// Figure out how many fields we need
max := 0
for _, place := range places {
if p.options&place > 0 {
max++
}
}
min := max - p.optionals
// Split fields on whitespace
fields := strings.Fields(spec)
// Validate number of fields
if count := len(fields); count < min || count > max {
if min == max {
return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec)
}
return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec)
}
// Fill in missing fields
fields = expandFields(fields, p.options)
var err error
field := func(field string, r bounds) uint64 {
if err != nil {
return 0
}
var bits uint64
bits, err = getField(field, r)
return bits
}
var (
second = field(fields[0], seconds)
minute = field(fields[1], minutes)
hour = field(fields[2], hours)
dayofmonth = field(fields[3], dom)
month = field(fields[4], months)
dayofweek = field(fields[5], dow)
)
if err != nil {
return nil, err
}
return &SpecSchedule{
Second: second,
Minute: minute,
Hour: hour,
Dom: dayofmonth,
Month: month,
Dow: dayofweek,
}, nil
}
func expandFields(fields []string, options ParseOption) []string {
n := 0
count := len(fields)
expFields := make([]string, len(places))
copy(expFields, defaults)
for i, place := range places {
if options&place > 0 {
expFields[i] = fields[n]
n++
}
if n == count {
break
}
}
return expFields
}
var standardParser = NewParser(
Minute | Hour | Dom | Month | Dow | Descriptor,
)
// ParseStandard returns a new crontab schedule representing the given standardSpec
// (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always
// pass 5 entries representing: minute, hour, day of month, month and day of week,
// in that order. It returns a descriptive error if the spec is not valid.
//
// It accepts
// - Standard crontab specs, e.g. "* * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
func ParseStandard(standardSpec string) (Schedule, error) {
return standardParser.Parse(standardSpec)
}
var defaultParser = NewParser(
Second | Minute | Hour | Dom | Month | DowOptional | Descriptor,
)
// 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, error) {
return defaultParser.Parse(spec)
}
// getField returns an Int with the bits set representing all of the times that
// the field represents or error parsing field value. A "field" is a comma-separated
// list of "ranges".
func getField(field string, r bounds) (uint64, error) {
var bits uint64
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
for _, expr := range ranges {
bit, err := getRange(expr, r)
if err != nil {
return bits, err
}
bits |= bit
}
return bits, nil
}
// getRange returns the bits indicated by the given expression:
// number | number "-" number [ "/" number ]
// or error parsing range.
func getRange(expr string, r bounds) (uint64, error) {
var (
start, end, step uint
rangeAndStep = strings.Split(expr, "/")
lowAndHigh = strings.Split(rangeAndStep[0], "-")
singleDigit = len(lowAndHigh) == 1
err error
)
var extra uint64
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
start = r.min
end = r.max
extra = starBit
} else {
start, err = parseIntOrName(lowAndHigh[0], r.names)
if err != nil {
return 0, err
}
switch len(lowAndHigh) {
case 1:
end = start
case 2:
end, err = parseIntOrName(lowAndHigh[1], r.names)
if err != nil {
return 0, err
}
default:
return 0, fmt.Errorf("Too many hyphens: %s", expr)
}
}
switch len(rangeAndStep) {
case 1:
step = 1
case 2:
step, err = mustParseInt(rangeAndStep[1])
if err != nil {
return 0, err
}
// Special handling: "N/step" means "N-max/step".
if singleDigit {
end = r.max
}
default:
return 0, fmt.Errorf("Too many slashes: %s", expr)
}
if start < r.min {
return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
}
if start > end {
return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
}
if step == 0 {
return 0, fmt.Errorf("Step of range should be a positive number: %s", expr)
}
return getBits(start, end, step) | extra, nil
}
// parseIntOrName returns the (possibly-named) integer contained in expr.
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
if names != nil {
if namedInt, ok := names[strings.ToLower(expr)]; ok {
return namedInt, nil
}
}
return mustParseInt(expr)
}
// mustParseInt parses the given expression as an int or returns an error.
func mustParseInt(expr string) (uint, error) {
num, err := strconv.Atoi(expr)
if err != nil {
return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
}
if num < 0 {
return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
}
return uint(num), nil
}
// 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 predefined schedule for the expression, or error if none matches.
func parseDescriptor(descriptor string) (Schedule, error) {
switch descriptor {
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),
}, nil
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),
}, nil
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,
}, nil
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),
}, nil
case "@hourly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: all(hours),
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}, nil
}
const every = "@every "
if strings.HasPrefix(descriptor, every) {
duration, err := time.ParseDuration(descriptor[len(every):])
if err != nil {
return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
}
return Every(duration), nil
}
return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
}

@ -151,7 +151,6 @@ func dayMatches(s *SpecSchedule, t time.Time) bool {
domMatch bool = 1<<uint(t.Day())&s.Dom > 0 domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0 dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
) )
if s.Dom&starBit > 0 || s.Dow&starBit > 0 { if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
return domMatch && dowMatch return domMatch && dowMatch
} }

@ -19,56 +19,12 @@ See godoc for further documentation and examples.
* [godoc.org/golang.org/x/oauth2](http://godoc.org/golang.org/x/oauth2) * [godoc.org/golang.org/x/oauth2](http://godoc.org/golang.org/x/oauth2)
* [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google) * [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google)
## Policy for new packages
## App Engine We no longer accept new provider-specific packages in this repo. For
defining provider endpoints and provider-specific OAuth2 behavior, we
In change 96e89be (March 2015), we removed the `oauth2.Context2` type in favor encourage you to create packages elsewhere. We'll keep the existing
of the [`context.Context`](https://golang.org/x/net/context#Context) type from packages for compatibility.
the `golang.org/x/net/context` package. Later replaced by the standard `context` package
of the [`context.Context`](https://golang.org/pkg/context#Context) type.
This means it's no longer possible to use the "Classic App Engine"
`appengine.Context` type with the `oauth2` package. (You're using
Classic App Engine if you import the package `"appengine"`.)
To work around this, you may use the new `"google.golang.org/appengine"`
package. This package has almost the same API as the `"appengine"` package,
but it can be fetched with `go get` and used on "Managed VMs" and well as
Classic App Engine.
See the [new `appengine` package's readme](https://github.com/golang/appengine#updating-a-go-app-engine-app)
for information on updating your app.
If you don't want to update your entire app to use the new App Engine packages,
you may use both sets of packages in parallel, using only the new packages
with the `oauth2` package.
```go
import (
"context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
newappengine "google.golang.org/appengine"
newurlfetch "google.golang.org/appengine/urlfetch"
"appengine"
)
func handler(w http.ResponseWriter, r *http.Request) {
var c appengine.Context = appengine.NewContext(r)
c.Infof("Logging a message with the old package")
var ctx context.Context = newappengine.NewContext(r)
client := &http.Client{
Transport: &oauth2.Transport{
Source: google.AppEngineTokenSource(ctx, "scope"),
Base: &newurlfetch.Transport{Context: ctx},
},
}
client.Get("...")
}
```
## Report Issues / Send Patches ## Report Issues / Send Patches

10
vendor/golang.org/x/oauth2/go.mod generated vendored

@ -0,0 +1,10 @@
module golang.org/x/oauth2
go 1.11
require (
cloud.google.com/go v0.34.0
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
google.golang.org/appengine v1.4.0
)

12
vendor/golang.org/x/oauth2/go.sum generated vendored

@ -0,0 +1,12 @@
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

@ -11,11 +11,13 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"math"
"mime" "mime"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"golang.org/x/net/context/ctxhttp" "golang.org/x/net/context/ctxhttp"
@ -77,6 +79,9 @@ func (e *tokenJSON) expiry() (t time.Time) {
type expirationTime int32 type expirationTime int32
func (e *expirationTime) UnmarshalJSON(b []byte) error { func (e *expirationTime) UnmarshalJSON(b []byte) error {
if len(b) == 0 || string(b) == "null" {
return nil
}
var n json.Number var n json.Number
err := json.Unmarshal(b, &n) err := json.Unmarshal(b, &n)
if err != nil { if err != nil {
@ -86,103 +91,78 @@ func (e *expirationTime) UnmarshalJSON(b []byte) error {
if err != nil { if err != nil {
return err return err
} }
if i > math.MaxInt32 {
i = math.MaxInt32
}
*e = expirationTime(i) *e = expirationTime(i)
return nil return nil
} }
var brokenAuthHeaderProviders = []string{ // RegisterBrokenAuthHeaderProvider previously did something. It is now a no-op.
"https://accounts.google.com/", //
"https://api.codeswholesale.com/oauth/token", // Deprecated: this function no longer does anything. Caller code that
"https://api.dropbox.com/", // wants to avoid potential extra HTTP requests made during
"https://api.dropboxapi.com/", // auto-probing of the provider's auth style should set
"https://api.instagram.com/", // Endpoint.AuthStyle.
"https://api.netatmo.net/", func RegisterBrokenAuthHeaderProvider(tokenURL string) {}
"https://api.odnoklassniki.ru/",
"https://api.pushbullet.com/", // AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type.
"https://api.soundcloud.com/", type AuthStyle int
"https://api.twitch.tv/",
"https://id.twitch.tv/",
"https://app.box.com/",
"https://api.box.com/",
"https://connect.stripe.com/",
"https://login.mailchimp.com/",
"https://login.microsoftonline.com/",
"https://login.salesforce.com/",
"https://login.windows.net",
"https://login.live.com/",
"https://login.live-int.com/",
"https://oauth.sandbox.trainingpeaks.com/",
"https://oauth.trainingpeaks.com/",
"https://oauth.vk.com/",
"https://openapi.baidu.com/",
"https://slack.com/",
"https://test-sandbox.auth.corp.google.com",
"https://test.salesforce.com/",
"https://user.gini.net/",
"https://www.douban.com/",
"https://www.googleapis.com/",
"https://www.linkedin.com/",
"https://www.strava.com/oauth/",
"https://www.wunderlist.com/oauth/",
"https://api.patreon.com/",
"https://sandbox.codeswholesale.com/oauth/token",
"https://api.sipgate.com/v1/authorization/oauth",
"https://api.medium.com/v1/tokens",
"https://log.finalsurge.com/oauth/token",
"https://multisport.todaysplan.com.au/rest/oauth/access_token",
"https://whats.todaysplan.com.au/rest/oauth/access_token",
"https://stackoverflow.com/oauth/access_token",
"https://account.health.nokia.com",
"https://accounts.zoho.com",
}
// brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints. const (
var brokenAuthHeaderDomains = []string{ AuthStyleUnknown AuthStyle = 0
".auth0.com", AuthStyleInParams AuthStyle = 1
".force.com", AuthStyleInHeader AuthStyle = 2
".myshopify.com", )
".okta.com",
".oktapreview.com", // authStyleCache is the set of tokenURLs we've successfully used via
// RetrieveToken and which style auth we ended up using.
// It's called a cache, but it doesn't (yet?) shrink. It's expected that
// the set of OAuth2 servers a program contacts over time is fixed and
// small.
var authStyleCache struct {
sync.Mutex
m map[string]AuthStyle // keyed by tokenURL
} }
func RegisterBrokenAuthHeaderProvider(tokenURL string) { // ResetAuthCache resets the global authentication style cache used
brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL) // for AuthStyleUnknown token requests.
func ResetAuthCache() {
authStyleCache.Lock()
defer authStyleCache.Unlock()
authStyleCache.m = nil
} }
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL // lookupAuthStyle reports which auth style we last used with tokenURL
// implements the OAuth2 spec correctly // when calling RetrieveToken and whether we have ever done so.
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background. func lookupAuthStyle(tokenURL string) (style AuthStyle, ok bool) {
// In summary: authStyleCache.Lock()
// - Reddit only accepts client secret in the Authorization header defer authStyleCache.Unlock()
// - Dropbox accepts either it in URL param or Auth header, but not both. style, ok = authStyleCache.m[tokenURL]
// - Google only accepts URL param (not spec compliant?), not Auth header return
// - Stripe only accepts client secret in Auth header with Bearer method, not Basic }
func providerAuthHeaderWorks(tokenURL string) bool {
for _, s := range brokenAuthHeaderProviders {
if strings.HasPrefix(tokenURL, s) {
// Some sites fail to implement the OAuth2 spec fully.
return false
}
}
if u, err := url.Parse(tokenURL); err == nil { // setAuthStyle adds an entry to authStyleCache, documented above.
for _, s := range brokenAuthHeaderDomains { func setAuthStyle(tokenURL string, v AuthStyle) {
if strings.HasSuffix(u.Host, s) { authStyleCache.Lock()
return false defer authStyleCache.Unlock()
} if authStyleCache.m == nil {
} authStyleCache.m = make(map[string]AuthStyle)
} }
authStyleCache.m[tokenURL] = v
// Assume the provider implements the spec properly
// otherwise. We can add more exceptions as they're
// discovered. We will _not_ be adding configurable hooks
// to this package to let users select server bugs.
return true
} }
func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) { // newTokenRequest returns a new *http.Request to retrieve a new token
bustedAuth := !providerAuthHeaderWorks(tokenURL) // from tokenURL using the provided clientID, clientSecret, and POST
if bustedAuth { // body parameters.
//
// inParams is whether the clientID & clientSecret should be encoded
// as the POST body. An 'inParams' value of true means to send it in
// the POST body (along with any values in v); false means to send it
// in the Authorization header.
func newTokenRequest(tokenURL, clientID, clientSecret string, v url.Values, authStyle AuthStyle) (*http.Request, error) {
if authStyle == AuthStyleInParams {
v = cloneURLValues(v)
if clientID != "" { if clientID != "" {
v.Set("client_id", clientID) v.Set("client_id", clientID)
} }
@ -195,15 +175,70 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
return nil, err return nil, err
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if !bustedAuth { if authStyle == AuthStyleInHeader {
req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret)) req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret))
} }
return req, nil
}
func cloneURLValues(v url.Values) url.Values {
v2 := make(url.Values, len(v))
for k, vv := range v {
v2[k] = append([]string(nil), vv...)
}
return v2
}
func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle) (*Token, error) {
needsAuthStyleProbe := authStyle == 0
if needsAuthStyleProbe {
if style, ok := lookupAuthStyle(tokenURL); ok {
authStyle = style
needsAuthStyleProbe = false
} else {
authStyle = AuthStyleInHeader // the first way we'll try
}
}
req, err := newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
if err != nil {
return nil, err
}
token, err := doTokenRoundTrip(ctx, req)
if err != nil && needsAuthStyleProbe {
// If we get an error, assume the server wants the
// clientID & clientSecret in a different form.
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
// In summary:
// - Reddit only accepts client secret in the Authorization header
// - Dropbox accepts either it in URL param or Auth header, but not both.
// - Google only accepts URL param (not spec compliant?), not Auth header
// - Stripe only accepts client secret in Auth header with Bearer method, not Basic
//
// We used to maintain a big table in this code of all the sites and which way
// they went, but maintaining it didn't scale & got annoying.
// So just try both ways.
authStyle = AuthStyleInParams // the second way we'll try
req, _ = newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
token, err = doTokenRoundTrip(ctx, req)
}
if needsAuthStyleProbe && err == nil {
setAuthStyle(tokenURL, authStyle)
}
// Don't overwrite `RefreshToken` with an empty value
// if this was a token refreshing request.
if token != nil && token.RefreshToken == "" {
token.RefreshToken = v.Get("refresh_token")
}
return token, err
}
func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
r, err := ctxhttp.Do(ctx, ContextClient(ctx), req) r, err := ctxhttp.Do(ctx, ContextClient(ctx), req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer r.Body.Close()
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
r.Body.Close()
if err != nil { if err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
} }
@ -229,7 +264,7 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
Raw: vals, Raw: vals,
} }
e := vals.Get("expires_in") e := vals.Get("expires_in")
if e == "" { if e == "" || e == "null" {
// TODO(jbd): Facebook's OAuth2 implementation is broken and // TODO(jbd): Facebook's OAuth2 implementation is broken and
// returns expires_in field in expires. Remove the fallback to expires, // returns expires_in field in expires. Remove the fallback to expires,
// when Facebook fixes their implementation. // when Facebook fixes their implementation.
@ -253,13 +288,8 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
} }
json.Unmarshal(body, &token.Raw) // no error checks for optional fields json.Unmarshal(body, &token.Raw) // no error checks for optional fields
} }
// Don't overwrite `RefreshToken` with an empty value
// if this was a token refreshing request.
if token.RefreshToken == "" {
token.RefreshToken = v.Get("refresh_token")
}
if token.AccessToken == "" { if token.AccessToken == "" {
return token, errors.New("oauth2: server response missing access_token") return nil, errors.New("oauth2: server response missing access_token")
} }
return token, nil return token, nil
} }

@ -26,17 +26,13 @@ import (
// Deprecated: Use context.Background() or context.TODO() instead. // Deprecated: Use context.Background() or context.TODO() instead.
var NoContext = context.TODO() var NoContext = context.TODO()
// RegisterBrokenAuthHeaderProvider registers an OAuth2 server // RegisterBrokenAuthHeaderProvider previously did something. It is now a no-op.
// identified by the tokenURL prefix as an OAuth2 implementation //
// which doesn't support the HTTP Basic authentication // Deprecated: this function no longer does anything. Caller code that
// scheme to authenticate with the authorization server. // wants to avoid potential extra HTTP requests made during
// Once a server is registered, credentials (client_id and client_secret) // auto-probing of the provider's auth style should set
// will be passed as query parameters rather than being present // Endpoint.AuthStyle.
// in the Authorization header. func RegisterBrokenAuthHeaderProvider(tokenURL string) {}
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
func RegisterBrokenAuthHeaderProvider(tokenURL string) {
internal.RegisterBrokenAuthHeaderProvider(tokenURL)
}
// Config describes a typical 3-legged OAuth2 flow, with both the // Config describes a typical 3-legged OAuth2 flow, with both the
// client application information and the server's endpoint URLs. // client application information and the server's endpoint URLs.
@ -71,13 +67,38 @@ type TokenSource interface {
Token() (*Token, error) Token() (*Token, error)
} }
// Endpoint contains the OAuth 2.0 provider's authorization and token // Endpoint represents an OAuth 2.0 provider's authorization and token
// endpoint URLs. // endpoint URLs.
type Endpoint struct { type Endpoint struct {
AuthURL string AuthURL string
TokenURL string TokenURL string
// AuthStyle optionally specifies how the endpoint wants the
// client ID & client secret sent. The zero value means to
// auto-detect.
AuthStyle AuthStyle
} }
// AuthStyle represents how requests for tokens are authenticated
// to the server.
type AuthStyle int
const (
// AuthStyleAutoDetect means to auto-detect which authentication
// style the provider wants by trying both ways and caching
// the successful way for the future.
AuthStyleAutoDetect AuthStyle = 0
// AuthStyleInParams sends the "client_id" and "client_secret"
// in the POST body as application/x-www-form-urlencoded parameters.
AuthStyleInParams AuthStyle = 1
// AuthStyleInHeader sends the client_id and client_password
// using HTTP Basic Authorization. This is an optional style
// described in the OAuth2 RFC 6749 section 2.3.1.
AuthStyleInHeader AuthStyle = 2
)
var ( var (
// AccessTypeOnline and AccessTypeOffline are options passed // AccessTypeOnline and AccessTypeOffline are options passed
// to the Options.AuthCodeURL method. They modify the // to the Options.AuthCodeURL method. They modify the
@ -124,7 +145,7 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
// //
// Opts may include AccessTypeOnline or AccessTypeOffline, as well // Opts may include AccessTypeOnline or AccessTypeOffline, as well
// as ApprovalForce. // as ApprovalForce.
// It can also be used to pass the PKCE challange. // It can also be used to pass the PKCE challenge.
// See https://www.oauth.com/oauth2-servers/pkce/ for more info. // See https://www.oauth.com/oauth2-servers/pkce/ for more info.
func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string { func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
var buf bytes.Buffer var buf bytes.Buffer
@ -164,8 +185,7 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
// and when other authorization grant types are not available." // and when other authorization grant types are not available."
// See https://tools.ietf.org/html/rfc6749#section-4.3 for more info. // See https://tools.ietf.org/html/rfc6749#section-4.3 for more info.
// //
// The HTTP client to use is derived from the context. // The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
// If nil, http.DefaultClient is used.
func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) { func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) {
v := url.Values{ v := url.Values{
"grant_type": {"password"}, "grant_type": {"password"},
@ -183,8 +203,7 @@ func (c *Config) PasswordCredentialsToken(ctx context.Context, username, passwor
// It is used after a resource provider redirects the user back // It is used after a resource provider redirects the user back
// to the Redirect URI (the URL obtained from AuthCodeURL). // to the Redirect URI (the URL obtained from AuthCodeURL).
// //
// The HTTP client to use is derived from the context. // The provided context optionally controls which HTTP client is used. See the HTTPClient variable.
// If a client is not provided via the context, http.DefaultClient is used.
// //
// The code will be in the *http.Request.FormValue("code"). Before // The code will be in the *http.Request.FormValue("code"). Before
// calling Exchange, be sure to validate FormValue("state"). // calling Exchange, be sure to validate FormValue("state").

@ -118,13 +118,16 @@ func (t *Token) Extra(key string) interface{} {
return v return v
} }
// timeNow is time.Now but pulled out as a variable for tests.
var timeNow = time.Now
// expired reports whether the token is expired. // expired reports whether the token is expired.
// t must be non-nil. // t must be non-nil.
func (t *Token) expired() bool { func (t *Token) expired() bool {
if t.Expiry.IsZero() { if t.Expiry.IsZero() {
return false return false
} }
return t.Expiry.Round(0).Add(-expiryDelta).Before(time.Now()) return t.Expiry.Round(0).Add(-expiryDelta).Before(timeNow())
} }
// Valid reports whether t is non-nil, has an AccessToken, and is not expired. // Valid reports whether t is non-nil, has an AccessToken, and is not expired.
@ -151,7 +154,7 @@ func tokenFromInternal(t *internal.Token) *Token {
// This token is then mapped from *internal.Token into an *oauth2.Token which is returned along // This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
// with an error.. // with an error..
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) { func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) {
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v) tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v, internal.AuthStyle(c.Endpoint.AuthStyle))
if err != nil { if err != nil {
if rErr, ok := err.(*internal.RetrieveError); ok { if rErr, ok := err.(*internal.RetrieveError); ok {
return nil, (*RetrieveError)(rErr) return nil, (*RetrieveError)(rErr)

@ -158,8 +158,8 @@ github.com/go-sql-driver/mysql
github.com/go-xorm/xorm github.com/go-xorm/xorm
# github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561 # github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561
github.com/gogits/chardet github.com/gogits/chardet
# github.com/gogits/cron v0.0.0-20160810035002-7f3990acf183 # github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14
github.com/gogits/cron github.com/gogs/cron
# github.com/golang/protobuf v1.3.1 # github.com/golang/protobuf v1.3.1
github.com/golang/protobuf/proto github.com/golang/protobuf/proto
# github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db # github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db
@ -369,7 +369,7 @@ golang.org/x/net/context/ctxhttp
golang.org/x/net/proxy golang.org/x/net/proxy
golang.org/x/net/context golang.org/x/net/context
golang.org/x/net/internal/socks golang.org/x/net/internal/socks
# golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759 # golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
golang.org/x/oauth2 golang.org/x/oauth2
golang.org/x/oauth2/internal golang.org/x/oauth2/internal
# golang.org/x/sys v0.0.0-20190620070143-6f217b454f45 # golang.org/x/sys v0.0.0-20190620070143-6f217b454f45

Loading…
Cancel
Save