mirror of
https://github.com/samsonjs/retrogit.git
synced 2026-03-25 09:25:49 +00:00
Changes necessary to run with the Go 1.11 App Engine runtime.
This commit is contained in:
parent
eb7efc60bc
commit
6c662e2109
14 changed files with 118 additions and 99 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# RetroGit
|
# RetroGit
|
||||||
|
|
||||||
Service that shows you your GitHub commits from a year ago. Includes a mail digest to that you can see each day what you were up to in the past.
|
Service that shows you your GitHub commits from previous years. Includes a mail digest to that you can see each day what you were up to in the past.
|
||||||
|
|
||||||
It's currently running at [https://www.retrogit.com/](https://www.retrogit.com/).
|
It's currently running at [https://www.retrogit.com/](https://www.retrogit.com/).
|
||||||
|
|
||||||
|
|
@ -21,5 +21,5 @@ The server can the be accessed at [http://localhost:8080/](http://localhost:8080
|
||||||
## Deploying to App Engine
|
## Deploying to App Engine
|
||||||
|
|
||||||
```
|
```
|
||||||
gcloud app deploy --project retrogit app/app.yaml
|
./deploy.sh
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"appengine"
|
"google.golang.org/appengine/datastore"
|
||||||
"appengine/datastore"
|
|
||||||
|
|
||||||
"code.google.com/p/goauth2/oauth"
|
"code.google.com/p/goauth2/oauth"
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
|
|
@ -28,7 +28,7 @@ type Account struct {
|
||||||
WeeklyDay time.Weekday
|
WeeklyDay time.Weekday
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAccount(c appengine.Context, githubUserId int) (*Account, error) {
|
func getAccount(c context.Context, githubUserId int) (*Account, error) {
|
||||||
key := datastore.NewKey(c, "Account", "", int64(githubUserId), nil)
|
key := datastore.NewKey(c, "Account", "", int64(githubUserId), nil)
|
||||||
account := new(Account)
|
account := new(Account)
|
||||||
err := datastore.Get(c, key, account)
|
err := datastore.Get(c, key, account)
|
||||||
|
|
@ -63,7 +63,7 @@ func initAccount(account *Account) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllAccounts(c appengine.Context) ([]Account, error) {
|
func getAllAccounts(c context.Context) ([]Account, error) {
|
||||||
q := datastore.NewQuery("Account")
|
q := datastore.NewQuery("Account")
|
||||||
var accounts []Account
|
var accounts []Account
|
||||||
_, err := q.GetAll(c, &accounts)
|
_, err := q.GetAll(c, &accounts)
|
||||||
|
|
@ -88,7 +88,7 @@ func (account *Account) IsRepoIdExcluded(repoId int) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (account *Account) Put(c appengine.Context) error {
|
func (account *Account) Put(c context.Context) error {
|
||||||
w := new(bytes.Buffer)
|
w := new(bytes.Buffer)
|
||||||
err := gob.NewEncoder(w).Encode(&account.OAuthToken)
|
err := gob.NewEncoder(w).Encode(&account.OAuthToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -100,7 +100,7 @@ func (account *Account) Put(c appengine.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (account *Account) Delete(c appengine.Context) error {
|
func (account *Account) Delete(c context.Context) error {
|
||||||
key := datastore.NewKey(c, "Account", "", int64(account.GitHubUserId), nil)
|
key := datastore.NewKey(c, "Account", "", int64(account.GitHubUserId), nil)
|
||||||
err := datastore.Delete(c, key)
|
err := datastore.Delete(c, key)
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"appengine"
|
"google.golang.org/appengine"
|
||||||
|
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
31
app/app.go
31
app/app.go
|
|
@ -1,4 +1,4 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -6,13 +6,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
log_ "log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"appengine"
|
"google.golang.org/appengine"
|
||||||
"appengine/mail"
|
"google.golang.org/appengine/log"
|
||||||
|
"google.golang.org/appengine/mail"
|
||||||
|
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
|
|
@ -208,14 +209,14 @@ func handleAppError(e *AppError, w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.Errorf("GitHub fetch error was not of type github.ErrorResponse")
|
log.Errorf(c, "GitHub fetch error was not of type github.ErrorResponse")
|
||||||
}
|
}
|
||||||
} else if e.Type == AppErrorTypeRedirect {
|
} else if e.Type == AppErrorTypeRedirect {
|
||||||
http.Redirect(w, r, e.Message, e.Code)
|
http.Redirect(w, r, e.Message, e.Code)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if e.Type != AppErrorTypeBadInput {
|
if e.Type != AppErrorTypeBadInput {
|
||||||
c.Errorf("%v", e.Error)
|
log.Errorf(c, "%v", e.Error)
|
||||||
if !appengine.IsDevAppServer() {
|
if !appengine.IsDevAppServer() {
|
||||||
sendAppErrorMail(e, r)
|
sendAppErrorMail(e, r)
|
||||||
}
|
}
|
||||||
|
|
@ -226,11 +227,11 @@ func handleAppError(e *AppError, w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(e.Code)
|
w.WriteHeader(e.Code)
|
||||||
templateError := templates["internal-error"].Render(w, data)
|
templateError := templates["internal-error"].Render(w, data)
|
||||||
if templateError != nil {
|
if templateError != nil {
|
||||||
c.Errorf("Error %s rendering error template.", templateError.Error.Error())
|
log.Errorf(c, "Error %s rendering error template.", templateError.Error.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
c.Infof("%v", e.Error)
|
log.Infof(c, "%v", e.Error)
|
||||||
}
|
}
|
||||||
http.Error(w, e.Message, e.Code)
|
http.Error(w, e.Message, e.Code)
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +261,7 @@ Error: %s`,
|
||||||
c := appengine.NewContext(r)
|
c := appengine.NewContext(r)
|
||||||
err := mail.Send(c, errorMessage)
|
err := mail.Send(c, errorMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf("Error %s sending error email.", err.Error())
|
log.Errorf(c, "Error %s sending error email.", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,11 +324,11 @@ func loadTemplates() (templates map[string]*Template) {
|
||||||
}
|
}
|
||||||
sharedFileNames, err := filepath.Glob("templates/shared/*.html")
|
sharedFileNames, err := filepath.Glob("templates/shared/*.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("Could not read shared template file names %s", err.Error())
|
log_.Panicf("Could not read shared template file names %s", err.Error())
|
||||||
}
|
}
|
||||||
templateFileNames, err := filepath.Glob("templates/*.html")
|
templateFileNames, err := filepath.Glob("templates/*.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("Could not read template file names %s", err.Error())
|
log_.Panicf("Could not read template file names %s", err.Error())
|
||||||
}
|
}
|
||||||
templates = make(map[string]*Template)
|
templates = make(map[string]*Template)
|
||||||
for _, templateFileName := range templateFileNames {
|
for _, templateFileName := range templateFileNames {
|
||||||
|
|
@ -344,7 +345,7 @@ func loadTemplates() (templates map[string]*Template) {
|
||||||
_, templateFileName = filepath.Split(fileNames[0])
|
_, templateFileName = filepath.Split(fileNames[0])
|
||||||
parsedTemplate, err := template.New(templateFileName).Funcs(funcMap).ParseFiles(fileNames...)
|
parsedTemplate, err := template.New(templateFileName).Funcs(funcMap).ParseFiles(fileNames...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Could not parse template files for %s: %s", templateFileName, err.Error())
|
log_.Printf("Could not parse template files for %s: %s", templateFileName, err.Error())
|
||||||
}
|
}
|
||||||
templates[templateName] = &Template{parsedTemplate}
|
templates[templateName] = &Template{parsedTemplate}
|
||||||
}
|
}
|
||||||
|
|
@ -354,13 +355,13 @@ func loadTemplates() (templates map[string]*Template) {
|
||||||
func loadStyles() (result map[string]template.CSS) {
|
func loadStyles() (result map[string]template.CSS) {
|
||||||
stylesBytes, err := ioutil.ReadFile("config/styles.json")
|
stylesBytes, err := ioutil.ReadFile("config/styles.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("Could not read styles JSON: %s", err.Error())
|
log_.Panicf("Could not read styles JSON: %s", err.Error())
|
||||||
}
|
}
|
||||||
var stylesJson interface{}
|
var stylesJson interface{}
|
||||||
err = json.Unmarshal(stylesBytes, &stylesJson)
|
err = json.Unmarshal(stylesBytes, &stylesJson)
|
||||||
result = make(map[string]template.CSS)
|
result = make(map[string]template.CSS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Could not parse styles JSON %s: %s", stylesBytes, err.Error())
|
log_.Printf("Could not parse styles JSON %s: %s", stylesBytes, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var parse func(string, map[string]interface{}, *string)
|
var parse func(string, map[string]interface{}, *string)
|
||||||
|
|
@ -377,7 +378,7 @@ func loadStyles() (result map[string]template.CSS) {
|
||||||
parse(path+k, v.(map[string]interface{}), &nestedStyle)
|
parse(path+k, v.(map[string]interface{}), &nestedStyle)
|
||||||
result[path+k] = template.CSS(nestedStyle)
|
result[path+k] = template.CSS(nestedStyle)
|
||||||
default:
|
default:
|
||||||
log.Printf("Unexpected type for %s in styles JSON, ignoring", k)
|
log_.Printf("Unexpected type for %s in styles JSON, ignoring", k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
runtime: go
|
runtime: go111
|
||||||
api_version: go1
|
|
||||||
|
|
||||||
handlers:
|
handlers:
|
||||||
- url: /static
|
- url: /static
|
||||||
|
|
@ -11,11 +10,11 @@ handlers:
|
||||||
static_files: static/robots.txt
|
static_files: static/robots.txt
|
||||||
upload: static/robots.txt
|
upload: static/robots.txt
|
||||||
- url: /digest/cron
|
- url: /digest/cron
|
||||||
script: _go_app
|
script: auto
|
||||||
login: admin
|
login: admin
|
||||||
- url: /admin/.*
|
- url: /admin/.*
|
||||||
script: _go_app
|
script: auto
|
||||||
login: admin
|
login: admin
|
||||||
- url: /.*
|
- url: /.*
|
||||||
script: _go_app
|
script: auto
|
||||||
secure: always
|
secure: always
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -11,8 +12,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"appengine"
|
"google.golang.org/appengine/log"
|
||||||
"appengine/memcache"
|
"google.golang.org/appengine/memcache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Simple http.RoundTripper implementation which wraps an existing transport and
|
// Simple http.RoundTripper implementation which wraps an existing transport and
|
||||||
|
|
@ -20,7 +21,7 @@ import (
|
||||||
// iteration cycle during development.
|
// iteration cycle during development.
|
||||||
type CachingTransport struct {
|
type CachingTransport struct {
|
||||||
Transport http.RoundTripper
|
Transport http.RoundTripper
|
||||||
Context appengine.Context
|
Context context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||||
|
|
@ -50,7 +51,7 @@ func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, er
|
||||||
|
|
||||||
cachedRespItem, err := memcache.Get(t.Context, cacheKey)
|
cachedRespItem, err := memcache.Get(t.Context, cacheKey)
|
||||||
if err != nil && err != memcache.ErrCacheMiss {
|
if err != nil && err != memcache.ErrCacheMiss {
|
||||||
t.Context.Errorf("Error getting cached response: %v", err)
|
log.Errorf(t.Context, "Error getting cached response: %v", err)
|
||||||
return t.Transport.RoundTrip(req)
|
return t.Transport.RoundTrip(req)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -59,17 +60,17 @@ func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, er
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
} else {
|
} else {
|
||||||
t.Context.Errorf("Error readings bytes for cached response: %v", err)
|
log.Errorf(t.Context, "Error readings bytes for cached response: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.Context.Infof("Fetching %s", req.URL)
|
log.Infof(t.Context, "Fetching %s", req.URL)
|
||||||
resp, err = t.Transport.RoundTrip(req)
|
resp, err = t.Transport.RoundTrip(req)
|
||||||
if err != nil || resp.StatusCode != 200 {
|
if err != nil || resp.StatusCode != 200 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
respBytes, err := httputil.DumpResponse(resp, true)
|
respBytes, err := httputil.DumpResponse(resp, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Context.Errorf("Error dumping bytes for cached response: %v", err)
|
log.Errorf(t.Context, "Error dumping bytes for cached response: %v", err)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
var expiration time.Duration = time.Hour
|
var expiration time.Duration = time.Hour
|
||||||
|
|
@ -86,7 +87,7 @@ func (t *CachingTransport) RoundTrip(req *http.Request) (resp *http.Response, er
|
||||||
Expiration: expiration,
|
Expiration: expiration,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Context.Errorf("Error setting cached response for %s (cache key %s, %d bytes to cache): %v",
|
log.Errorf(t.Context, "Error setting cached response for %s (cache key %s, %d bytes to cache): %v",
|
||||||
req.URL, cacheKey, len(respBytes), err)
|
req.URL, cacheKey, len(respBytes), err)
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"appengine"
|
"google.golang.org/appengine/log"
|
||||||
|
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
)
|
)
|
||||||
|
|
@ -171,7 +172,7 @@ type Digest struct {
|
||||||
RepoErrors map[string]error
|
RepoErrors map[string]error
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDigest(c appengine.Context, githubClient *github.Client, account *Account) (*Digest, error) {
|
func newDigest(c context.Context, githubClient *github.Client, account *Account) (*Digest, error) {
|
||||||
user, _, err := githubClient.Users.Get("")
|
user, _, err := githubClient.Users.Get("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -225,7 +226,7 @@ func newDigest(c appengine.Context, githubClient *github.Client, account *Accoun
|
||||||
|
|
||||||
digest.fetch(githubClient)
|
digest.fetch(githubClient)
|
||||||
for repoFullName, err := range digest.RepoErrors {
|
for repoFullName, err := range digest.RepoErrors {
|
||||||
c.Errorf("Error fetching %s: %s", repoFullName, err.Error())
|
log.Errorf(c, "Error fetching %s: %s", repoFullName, err.Error())
|
||||||
}
|
}
|
||||||
return digest, nil
|
return digest, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
app/index.yaml
Normal file
2
app/index.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
indexes:
|
||||||
|
# AUTOGENERATED
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
// From Martini's recovery package:
|
// From Martini's recovery package:
|
||||||
// https://github.com/go-martini/martini/blob/master/recovery.go
|
// https://github.com/go-martini/martini/blob/master/recovery.go
|
||||||
|
|
|
||||||
38
app/repos.go
38
app/repos.go
|
|
@ -1,13 +1,15 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"appengine"
|
"google.golang.org/appengine"
|
||||||
"appengine/datastore"
|
"google.golang.org/appengine/datastore"
|
||||||
"appengine/delay"
|
"google.golang.org/appengine/delay"
|
||||||
"appengine/taskqueue"
|
"google.golang.org/appengine/log"
|
||||||
|
"google.golang.org/appengine/taskqueue"
|
||||||
|
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
)
|
)
|
||||||
|
|
@ -23,16 +25,16 @@ type RepoVintage struct {
|
||||||
Vintage time.Time `datastore:",noindex"`
|
Vintage time.Time `datastore:",noindex"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getVintageKey(c appengine.Context, userId int, repoId int) *datastore.Key {
|
func getVintageKey(c context.Context, userId int, repoId int) *datastore.Key {
|
||||||
return datastore.NewKey(c, "RepoVintage", fmt.Sprintf("%d-%d", userId, repoId), 0, nil)
|
return datastore.NewKey(c, "RepoVintage", fmt.Sprintf("%d-%d", userId, repoId), 0, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var computeVintageFunc *delay.Function
|
var computeVintageFunc *delay.Function
|
||||||
|
|
||||||
func computeVintage(c appengine.Context, userId int, userLogin string, repoId int, repoOwnerLogin string, repoName string) error {
|
func computeVintage(c context.Context, userId int, userLogin string, repoId int, repoOwnerLogin string, repoName string) error {
|
||||||
account, err := getAccount(c, userId)
|
account, err := getAccount(c, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf("Could not load account %d: %s. Presumed deleted, aborting computing vintage for %s/%s", userId, err.Error(), repoOwnerLogin, repoName)
|
log.Errorf(c, "Could not load account %d: %s. Presumed deleted, aborting computing vintage for %s/%s", userId, err.Error(), repoOwnerLogin, repoName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,7 +44,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
|
||||||
|
|
||||||
repo, response, err := githubClient.Repositories.Get(repoOwnerLogin, repoName)
|
repo, response, err := githubClient.Repositories.Get(repoOwnerLogin, repoName)
|
||||||
if response.StatusCode == 403 || response.StatusCode == 404 {
|
if response.StatusCode == 403 || response.StatusCode == 404 {
|
||||||
c.Warningf("Got a %d when trying to look up %s/%s (%d)", response.StatusCode, repoOwnerLogin, repoName, repoId)
|
log.Warningf(c, "Got a %d when trying to look up %s/%s (%d)", response.StatusCode, repoOwnerLogin, repoName, repoId)
|
||||||
_, err = datastore.Put(c, getVintageKey(c, userId, repoId), &RepoVintage{
|
_, err = datastore.Put(c, getVintageKey(c, userId, repoId), &RepoVintage{
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
RepoId: repoId,
|
RepoId: repoId,
|
||||||
|
|
@ -50,7 +52,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
c.Errorf("Could not load repo %s/%s (%d): %s", repoOwnerLogin, repoName, repoId, err.Error())
|
log.Errorf(c, "Could not load repo %s/%s (%d): %s", repoOwnerLogin, repoName, repoId, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +71,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
|
||||||
// GitHub returns with a 409 when a repository is empty.
|
// GitHub returns with a 409 when a repository is empty.
|
||||||
commits = make([]github.RepositoryCommit, 0)
|
commits = make([]github.RepositoryCommit, 0)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
c.Errorf("Could not load commits for repo %s (%d): %s", *repo.FullName, repoId, err.Error())
|
log.Errorf(c, "Could not load commits for repo %s (%d): %s", *repo.FullName, repoId, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,10 +80,10 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
|
||||||
if len(commits) > 0 {
|
if len(commits) > 0 {
|
||||||
stats, response, err := githubClient.Repositories.ListContributorsStats(repoOwnerLogin, repoName)
|
stats, response, err := githubClient.Repositories.ListContributorsStats(repoOwnerLogin, repoName)
|
||||||
if response.StatusCode == 202 {
|
if response.StatusCode == 202 {
|
||||||
c.Infof("Stats were not available for %s, will try again later", *repo.FullName)
|
log.Infof(c, "Stats were not available for %s, will try again later", *repo.FullName)
|
||||||
task, err := computeVintageFunc.Task(userId, userLogin, repoId, repoOwnerLogin, repoName)
|
task, err := computeVintageFunc.Task(userId, userLogin, repoId, repoOwnerLogin, repoName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf("Could create delayed task for %s: %s", *repo.FullName, err.Error())
|
log.Errorf(c, "Could create delayed task for %s: %s", *repo.FullName, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
task.Delay = time.Second * 10
|
task.Delay = time.Second * 10
|
||||||
|
|
@ -89,7 +91,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf("Could not load stats for repo %s: %s", *repo.FullName, err.Error())
|
log.Errorf(c, "Could not load stats for repo %s: %s", *repo.FullName, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, stat := range stats {
|
for _, stat := range stats {
|
||||||
|
|
@ -111,7 +113,7 @@ func computeVintage(c appengine.Context, userId int, userLogin string, repoId in
|
||||||
Vintage: vintage,
|
Vintage: vintage,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf("Could save vintage for repo %s: %s", *repo.FullName, err.Error())
|
log.Errorf(c, "Could save vintage for repo %s: %s", *repo.FullName, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,7 +124,7 @@ func init() {
|
||||||
computeVintageFunc = delay.Func("computeVintage", computeVintage)
|
computeVintageFunc = delay.Func("computeVintage", computeVintage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillVintages(c appengine.Context, user *github.User, repos []*Repo) error {
|
func fillVintages(c context.Context, user *github.User, repos []*Repo) error {
|
||||||
if len(repos) > VintageChunkSize {
|
if len(repos) > VintageChunkSize {
|
||||||
for chunkStart := 0; chunkStart < len(repos); chunkStart += VintageChunkSize {
|
for chunkStart := 0; chunkStart < len(repos); chunkStart += VintageChunkSize {
|
||||||
chunkEnd := chunkStart + VintageChunkSize
|
chunkEnd := chunkStart + VintageChunkSize
|
||||||
|
|
@ -151,7 +153,7 @@ func fillVintages(c appengine.Context, user *github.User, repos []*Repo) error {
|
||||||
if err == datastore.ErrNoSuchEntity {
|
if err == datastore.ErrNoSuchEntity {
|
||||||
vintages[i] = nil
|
vintages[i] = nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
c.Errorf("%d/%s vintage fetch error: %s", i, *repos[i].FullName, err.Error())
|
log.Errorf(c, "%d/%s vintage fetch error: %s", i, *repos[i].FullName, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +240,7 @@ type UserRepos struct {
|
||||||
Repos []*Repo
|
Repos []*Repo
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRepos(c appengine.Context, githubClient *github.Client, account *Account, user *github.User) (*Repos, error) {
|
func getRepos(c context.Context, githubClient *github.Client, account *Account, user *github.User) (*Repos, error) {
|
||||||
clientUserRepos := make([]github.Repository, 0)
|
clientUserRepos := make([]github.Repository, 0)
|
||||||
page := 1
|
page := 1
|
||||||
for {
|
for {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
log_ "log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -13,11 +14,12 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"appengine"
|
"google.golang.org/appengine"
|
||||||
"appengine/datastore"
|
"google.golang.org/appengine/datastore"
|
||||||
"appengine/delay"
|
"google.golang.org/appengine/delay"
|
||||||
"appengine/mail"
|
"google.golang.org/appengine/log"
|
||||||
"appengine/urlfetch"
|
"google.golang.org/appengine/mail"
|
||||||
|
"google.golang.org/appengine/urlfetch"
|
||||||
|
|
||||||
"code.google.com/p/goauth2/oauth"
|
"code.google.com/p/goauth2/oauth"
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
|
|
@ -33,7 +35,7 @@ var sessionStore *sessions.CookieStore
|
||||||
var sessionConfig SessionConfig
|
var sessionConfig SessionConfig
|
||||||
var templates map[string]*Template
|
var templates map[string]*Template
|
||||||
|
|
||||||
func init() {
|
func main() {
|
||||||
templates = loadTemplates()
|
templates = loadTemplates()
|
||||||
timezones = initTimezones()
|
timezones = initTimezones()
|
||||||
sessionStore, sessionConfig = initSession()
|
sessionStore, sessionConfig = initSession()
|
||||||
|
|
@ -62,6 +64,8 @@ func init() {
|
||||||
router.Handle("/admin/repos", AppHandler(reposAdminHandler)).Name("repos-admin")
|
router.Handle("/admin/repos", AppHandler(reposAdminHandler)).Name("repos-admin")
|
||||||
router.Handle("/admin/delete-account", AppHandler(deleteAccountAdminHandler)).Name("delete-account-admin")
|
router.Handle("/admin/delete-account", AppHandler(deleteAccountAdminHandler)).Name("delete-account-admin")
|
||||||
http.Handle("/", router)
|
http.Handle("/", router)
|
||||||
|
|
||||||
|
appengine.Main()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initGithubOAuthConfig(includePrivateRepos bool) (config oauth.Config) {
|
func initGithubOAuthConfig(includePrivateRepos bool) (config oauth.Config) {
|
||||||
|
|
@ -72,11 +76,11 @@ func initGithubOAuthConfig(includePrivateRepos bool) (config oauth.Config) {
|
||||||
path += ".json"
|
path += ".json"
|
||||||
configBytes, err := ioutil.ReadFile(path)
|
configBytes, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("Could not read GitHub OAuth config from %s: %s", path, err.Error())
|
log_.Panicf("Could not read GitHub OAuth config from %s: %s", path, err.Error())
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(configBytes, &config)
|
err = json.Unmarshal(configBytes, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("Could not parse GitHub OAuth config %s: %s", configBytes, err.Error())
|
log_.Panicf("Could not parse GitHub OAuth config %s: %s", configBytes, err.Error())
|
||||||
}
|
}
|
||||||
repoScopeModifier := ""
|
repoScopeModifier := ""
|
||||||
if !includePrivateRepos {
|
if !includePrivateRepos {
|
||||||
|
|
@ -232,12 +236,12 @@ func digestCronHandler(w http.ResponseWriter, r *http.Request) *AppError {
|
||||||
if account.Frequency == "weekly" {
|
if account.Frequency == "weekly" {
|
||||||
now := time.Now().In(account.TimezoneLocation)
|
now := time.Now().In(account.TimezoneLocation)
|
||||||
if now.Weekday() != account.WeeklyDay {
|
if now.Weekday() != account.WeeklyDay {
|
||||||
c.Infof("Skipping %d, since it wants weekly digests on %ss and today is a %s.",
|
log.Infof(c, "Skipping %d, since it wants weekly digests on %ss and today is a %s.",
|
||||||
account.GitHubUserId, account.WeeklyDay, now.Weekday())
|
account.GitHubUserId, account.WeeklyDay, now.Weekday())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.Infof("Enqueing task for %d...", account.GitHubUserId)
|
log.Infof(c, "Enqueing task for %d...", account.GitHubUserId)
|
||||||
sendDigestForAccountFunc.Call(c, account.GitHubUserId)
|
sendDigestForAccountFunc.Call(c, account.GitHubUserId)
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, "Done")
|
fmt.Fprint(w, "Done")
|
||||||
|
|
@ -246,31 +250,31 @@ func digestCronHandler(w http.ResponseWriter, r *http.Request) *AppError {
|
||||||
|
|
||||||
var sendDigestForAccountFunc = delay.Func(
|
var sendDigestForAccountFunc = delay.Func(
|
||||||
"sendDigestForAccount",
|
"sendDigestForAccount",
|
||||||
func(c appengine.Context, githubUserId int) error {
|
func(c context.Context, githubUserId int) error {
|
||||||
c.Infof("Sending digest for %d...", githubUserId)
|
log.Infof(c, "Sending digest for %d...", githubUserId)
|
||||||
account, err := getAccount(c, githubUserId)
|
account, err := getAccount(c, githubUserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf(" Error looking up account: %s", err.Error())
|
log.Errorf(c, " Error looking up account: %s", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sent, err := sendDigestForAccount(account, c)
|
sent, err := sendDigestForAccount(account, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf(" Error: %s", err.Error())
|
log.Errorf(c, " Error: %s", err.Error())
|
||||||
if !appengine.IsDevAppServer() {
|
if !appengine.IsDevAppServer() {
|
||||||
sendDigestErrorMail(err, c, githubUserId)
|
sendDigestErrorMail(err, c, githubUserId)
|
||||||
}
|
}
|
||||||
} else if sent {
|
} else if sent {
|
||||||
c.Infof(" Sent!")
|
log.Infof(c, " Sent!")
|
||||||
} else {
|
} else {
|
||||||
c.Infof(" Not sent, digest was empty")
|
log.Infof(c, " Not sent, digest was empty")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
func sendDigestErrorMail(e error, c appengine.Context, gitHubUserId int) {
|
func sendDigestErrorMail(e error, c context.Context, gitHubUserId int) {
|
||||||
if strings.Contains(e.Error(), ": 502") {
|
if strings.Contains(e.Error(), ": 502") {
|
||||||
// Ignore 502s from GitHub, there's nothing we do about them.
|
// Ignore 502s from GitHub, there's nothing we do about them.
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
errorMessage := &mail.Message{
|
errorMessage := &mail.Message{
|
||||||
Sender: "RetroGit Admin <digests@retrogit.com>",
|
Sender: "RetroGit Admin <digests@retrogit.com>",
|
||||||
|
|
@ -280,11 +284,11 @@ func sendDigestErrorMail(e error, c appengine.Context, gitHubUserId int) {
|
||||||
}
|
}
|
||||||
err := mail.Send(c, errorMessage)
|
err := mail.Send(c, errorMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf("Error %s sending error email.", err.Error())
|
log.Errorf(c, "Error %s sending error email.", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendDigestForAccount(account *Account, c appengine.Context) (bool, error) {
|
func sendDigestForAccount(account *Account, c context.Context) (bool, error) {
|
||||||
oauthTransport := githubOAuthTransport(c)
|
oauthTransport := githubOAuthTransport(c)
|
||||||
oauthTransport.Token = &account.OAuthToken
|
oauthTransport.Token = &account.OAuthToken
|
||||||
githubClient := github.NewClient(oauthTransport.Client())
|
githubClient := github.NewClient(oauthTransport.Client())
|
||||||
|
|
@ -295,7 +299,7 @@ func sendDigestForAccount(account *Account, c appengine.Context) (bool, error) {
|
||||||
gitHubStatus := gitHubError.Response.StatusCode
|
gitHubStatus := gitHubError.Response.StatusCode
|
||||||
if gitHubStatus == http.StatusUnauthorized ||
|
if gitHubStatus == http.StatusUnauthorized ||
|
||||||
gitHubStatus == http.StatusForbidden {
|
gitHubStatus == http.StatusForbidden {
|
||||||
c.Errorf(" GitHub auth error while getting email adddress, skipping: %s", err.Error())
|
log.Errorf(c, " GitHub auth error while getting email adddress, skipping: %s", err.Error())
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -312,7 +316,7 @@ func sendDigestForAccount(account *Account, c appengine.Context) (bool, error) {
|
||||||
gitHubStatus := gitHubError.Response.StatusCode
|
gitHubStatus := gitHubError.Response.StatusCode
|
||||||
if gitHubStatus == http.StatusUnauthorized ||
|
if gitHubStatus == http.StatusUnauthorized ||
|
||||||
gitHubStatus == http.StatusForbidden {
|
gitHubStatus == http.StatusForbidden {
|
||||||
c.Errorf(" GitHub auth error while getting digest, sending error email: %s", err.Error())
|
log.Errorf(c, " GitHub auth error while getting digest, sending error email: %s", err.Error())
|
||||||
var authErrorHtml bytes.Buffer
|
var authErrorHtml bytes.Buffer
|
||||||
if err := templates["github-auth-error-email"].Execute(&authErrorHtml, nil); err != nil {
|
if err := templates["github-auth-error-email"].Execute(&authErrorHtml, nil); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|
@ -516,11 +520,11 @@ func setInitialTimezoneHandler(w http.ResponseWriter, r *http.Request, state *Ap
|
||||||
|
|
||||||
var cacheDigestForAccountFunc = delay.Func(
|
var cacheDigestForAccountFunc = delay.Func(
|
||||||
"cacheDigestForAccount",
|
"cacheDigestForAccount",
|
||||||
func(c appengine.Context, githubUserId int) error {
|
func(c context.Context, githubUserId int) error {
|
||||||
c.Infof("Caching digest for %d...", githubUserId)
|
log.Infof(c, "Caching digest for %d...", githubUserId)
|
||||||
account, err := getAccount(c, githubUserId)
|
account, err := getAccount(c, githubUserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf(" Error looking up account: %s", err.Error())
|
log.Errorf(c, " Error looking up account: %s", err.Error())
|
||||||
// Not returning error since we don't want these tasks to be
|
// Not returning error since we don't want these tasks to be
|
||||||
// retried.
|
// retried.
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -531,9 +535,9 @@ var cacheDigestForAccountFunc = delay.Func(
|
||||||
githubClient := github.NewClient(oauthTransport.Client())
|
githubClient := github.NewClient(oauthTransport.Client())
|
||||||
_, err = newDigest(c, githubClient, account)
|
_, err = newDigest(c, githubClient, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf(" Error computing digest: %s", err.Error())
|
log.Errorf(c, " Error computing digest: %s", err.Error())
|
||||||
}
|
}
|
||||||
c.Infof(" Done!")
|
log.Infof(c, " Done!")
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -544,12 +548,12 @@ func deleteAccountHandler(w http.ResponseWriter, r *http.Request, state *AppSign
|
||||||
return RedirectToRoute("index")
|
return RedirectToRoute("index")
|
||||||
}
|
}
|
||||||
|
|
||||||
func githubOAuthTransport(c appengine.Context) *oauth.Transport {
|
func githubOAuthTransport(c context.Context) *oauth.Transport {
|
||||||
appengineTransport := &urlfetch.Transport{Context: c}
|
ctx_with_timeout, _ := context.WithTimeout(c, time.Second*60)
|
||||||
appengineTransport.Deadline = time.Second * 60
|
appengineTransport := &urlfetch.Transport{Context: ctx_with_timeout}
|
||||||
cachingTransport := &CachingTransport{
|
cachingTransport := &CachingTransport{
|
||||||
Transport: appengineTransport,
|
Transport: appengineTransport,
|
||||||
Context: c,
|
Context: ctx_with_timeout,
|
||||||
}
|
}
|
||||||
return &oauth.Transport{
|
return &oauth.Transport{
|
||||||
Config: &githubOauthConfig,
|
Config: &githubOauthConfig,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"appengine"
|
"google.golang.org/appengine"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package retrogit
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
|
||||||
9
deploy.sh
Executable file
9
deploy.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# With the Go 1.11 runtime, if we're not using modules, all source (including
|
||||||
|
# the app itself) must live under GOPATH. Copy it there before deploying.
|
||||||
|
DEST="$GOPATH/src/retrogit"
|
||||||
|
rm -rf $DEST
|
||||||
|
cp -r app $DEST
|
||||||
|
cd $DEST
|
||||||
|
gcloud app deploy --project retro-git app.yaml
|
||||||
Loading…
Reference in a new issue