2013-08-23 04:33:23 +00:00
|
|
|
/*
|
|
|
|
Copyright 2013 Google Inc.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2017-11-26 09:05:38 +00:00
|
|
|
"context"
|
2017-12-06 18:45:06 +00:00
|
|
|
"encoding/json"
|
2013-08-23 04:33:23 +00:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"strings"
|
2013-10-23 14:56:57 +00:00
|
|
|
"sync"
|
2013-08-23 04:33:23 +00:00
|
|
|
"time"
|
|
|
|
|
2016-09-23 23:38:12 +00:00
|
|
|
"cloud.google.com/go/datastore"
|
2017-12-06 18:45:06 +00:00
|
|
|
"github.com/mailgun/mailgun-go"
|
2015-12-29 05:08:17 +00:00
|
|
|
|
Rename import paths from camlistore.org to perkeep.org.
Part of the project renaming, issue #981.
After this, users will need to mv their $GOPATH/src/camlistore.org to
$GOPATH/src/perkeep.org. Sorry.
This doesn't yet rename the tools like camlistored, camput, camget,
camtool, etc.
Also, this only moves the lru package to internal. More will move to
internal later.
Also, this doesn't yet remove the "/pkg/" directory. That'll likely
happen later.
This updates some docs, but not all.
devcam test now passes again, even with Go 1.10 (which requires vet
checks are clean too). So a bunch of vet tests are fixed in this CL
too, and a bunch of other broken tests are now fixed (introduced from
the past week of merging the CL backlog).
Change-Id: If580db1691b5b99f8ed6195070789b1f44877dd4
2018-01-01 22:41:41 +00:00
|
|
|
"perkeep.org/pkg/osutil"
|
2013-08-23 04:33:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2017-12-06 18:45:06 +00:00
|
|
|
emailNow = flag.String("email_now", "", "[debug] if non-empty, this commit hash is emailed immediately, without starting the webserver.")
|
|
|
|
mailgunCfgFile = flag.String("mailgun_config", "", "[optional] Mailgun JSON configuration for sending emails on new commits.")
|
|
|
|
emailsTo = flag.String("email_dest", "", "[optional] The email address for new commit emails.")
|
2013-08-23 04:33:23 +00:00
|
|
|
)
|
|
|
|
|
2017-12-06 18:45:06 +00:00
|
|
|
type mailgunCfg struct {
|
|
|
|
Domain string `json:"domain"`
|
|
|
|
APIKey string `json:"apiKey"`
|
|
|
|
PublicAPIKey string `json:"publicAPIKey"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// mailgun is for sending the camweb startup e-mail, and the commits e-mails. No
|
|
|
|
// e-mails are sent if it is nil. It is set in sendStartingEmail, and it is nil
|
|
|
|
// if mailgunCfgFile is not set.
|
|
|
|
var mailGun mailgun.Mailgun
|
|
|
|
|
|
|
|
func mailgunCfgFromGCS() (*mailgunCfg, error) {
|
|
|
|
var cfg mailgunCfg
|
|
|
|
data, err := fromGCS(*mailgunCfgFile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not JSON decode website's mailgun config: %v", err)
|
|
|
|
}
|
|
|
|
return &cfg, nil
|
|
|
|
}
|
|
|
|
|
2013-08-23 04:33:23 +00:00
|
|
|
func startEmailCommitLoop(errc chan<- error) {
|
|
|
|
if *emailsTo == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if *emailNow != "" {
|
2017-12-11 19:50:05 +00:00
|
|
|
dir, err := osutil.GoPackagePath(prodDomain)
|
2013-08-23 04:33:23 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
if err := emailCommit(dir, *emailNow); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
errc <- commitEmailLoop()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
// tokenc holds tokens for the /mailnow handler.
|
|
|
|
// Hitting /mailnow (unauthenticated) forces a 'git fetch origin
|
|
|
|
// master'. Because it's unauthenticated, we don't want to allow
|
|
|
|
// attackers to force us to hit git. The /mailnow handler tries to
|
|
|
|
// take a token from tokenc.
|
|
|
|
var tokenc = make(chan bool, 3)
|
|
|
|
|
|
|
|
var fetchc = make(chan bool, 1)
|
|
|
|
|
|
|
|
var knownCommit = map[string]bool{} // commit -> true
|
|
|
|
|
|
|
|
var diffMarker = []byte("diff --git a/")
|
|
|
|
|
|
|
|
func emailCommit(dir, hash string) (err error) {
|
2017-12-06 18:45:06 +00:00
|
|
|
if mailGun == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-09-05 21:50:19 +00:00
|
|
|
cmd := execGit(dir, "show", nil, "show", hash)
|
2013-08-23 04:33:23 +00:00
|
|
|
body, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
2017-12-06 18:45:06 +00:00
|
|
|
return fmt.Errorf("error runnning git show: %v\n%s", err, body)
|
2013-08-23 04:33:23 +00:00
|
|
|
}
|
|
|
|
if !bytes.Contains(body, diffMarker) {
|
|
|
|
// Boring merge commit. Don't email.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-09-05 21:50:19 +00:00
|
|
|
cmd = execGit(dir, "show_pretty", nil, "show", "--pretty=oneline", hash)
|
2013-08-23 04:33:23 +00:00
|
|
|
out, err := cmd.Output()
|
|
|
|
if err != nil {
|
2017-12-06 18:45:06 +00:00
|
|
|
return fmt.Errorf("error runnning git show_pretty: %v\n%s", err, string(out))
|
2013-08-23 04:33:23 +00:00
|
|
|
}
|
|
|
|
subj := out[41:] // remove hash and space
|
|
|
|
if i := bytes.IndexByte(subj, '\n'); i != -1 {
|
|
|
|
subj = subj[:i]
|
|
|
|
}
|
|
|
|
if len(subj) > 80 {
|
|
|
|
subj = subj[:80]
|
|
|
|
}
|
|
|
|
|
2017-12-06 18:45:06 +00:00
|
|
|
contents := fmt.Sprintf(`
|
2013-08-23 04:33:23 +00:00
|
|
|
|
|
|
|
https://camlistore.googlesource.com/camlistore/+/%s
|
|
|
|
|
2017-12-06 18:45:06 +00:00
|
|
|
%s`, hash, body)
|
|
|
|
|
|
|
|
m := mailGun.NewMessage(
|
|
|
|
"noreply@camlistore.org (Camlistore Commit)",
|
|
|
|
string(subj),
|
|
|
|
contents,
|
|
|
|
*emailsTo,
|
|
|
|
)
|
|
|
|
m.SetReplyTo("camlistore-commits@googlegroups.com")
|
|
|
|
if _, _, err := mailGun.Send(m); err != nil {
|
|
|
|
return fmt.Errorf("failed to send e-mail: %v", err)
|
2013-08-23 04:33:23 +00:00
|
|
|
}
|
2017-12-06 18:45:06 +00:00
|
|
|
return nil
|
2013-08-23 04:33:23 +00:00
|
|
|
}
|
|
|
|
|
2013-10-23 14:56:57 +00:00
|
|
|
var latestHash struct {
|
|
|
|
sync.Mutex
|
|
|
|
s string // hash of the most recent camlistore revision
|
|
|
|
}
|
|
|
|
|
2015-12-29 05:08:17 +00:00
|
|
|
// dsClient is our datastore client to track which commits we've
|
|
|
|
// emailed about. It's only non-nil in production.
|
|
|
|
var dsClient *datastore.Client
|
|
|
|
|
2013-08-23 04:33:23 +00:00
|
|
|
func commitEmailLoop() error {
|
|
|
|
http.HandleFunc("/mailnow", mailNowHandler)
|
|
|
|
|
2015-12-29 05:08:17 +00:00
|
|
|
var err error
|
|
|
|
dsClient, err = datastore.NewClient(context.Background(), "camlistore-website")
|
|
|
|
log.Printf("datastore = %v, %v", dsClient, err)
|
|
|
|
|
2013-08-23 04:33:23 +00:00
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case tokenc <- true:
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
time.Sleep(15 * time.Second)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2015-11-11 12:35:31 +00:00
|
|
|
dir := camSrcDir()
|
2013-08-23 04:33:23 +00:00
|
|
|
|
2013-10-23 14:56:57 +00:00
|
|
|
http.HandleFunc("/latesthash", latestHashHandler)
|
2015-12-29 05:08:17 +00:00
|
|
|
http.HandleFunc("/debug/email", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
fmt.Fprintf(w, "ds = %v, %v", dsClient, err)
|
|
|
|
})
|
2013-08-23 04:33:23 +00:00
|
|
|
|
|
|
|
for {
|
|
|
|
pollCommits(dir)
|
|
|
|
|
|
|
|
// Poll every minute or whenever we're forced with the
|
|
|
|
// /mailnow handler.
|
|
|
|
select {
|
|
|
|
case <-time.After(1 * time.Minute):
|
|
|
|
case <-fetchc:
|
2013-08-23 05:03:07 +00:00
|
|
|
log.Printf("Polling git due to explicit trigger.")
|
2013-08-23 04:33:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-05 21:50:19 +00:00
|
|
|
// execGit runs the git command with gitArgs. All the other arguments are only
|
|
|
|
// relevant if *gitContainer, in which case we run in a docker container.
|
|
|
|
func execGit(workdir string, containerName string, mounts map[string]string, gitArgs ...string) *exec.Cmd {
|
2015-11-11 12:35:31 +00:00
|
|
|
var cmd *exec.Cmd
|
|
|
|
if *gitContainer {
|
2016-09-05 21:50:19 +00:00
|
|
|
removeContainer(containerName)
|
2016-04-25 19:55:11 +00:00
|
|
|
args := []string{
|
2015-11-11 12:35:31 +00:00
|
|
|
"run",
|
|
|
|
"--rm",
|
2016-09-05 21:50:19 +00:00
|
|
|
"--name=" + containerName,
|
2016-04-25 19:55:11 +00:00
|
|
|
}
|
|
|
|
for host, container := range mounts {
|
2016-04-26 00:29:03 +00:00
|
|
|
args = append(args, "-v", host+":"+container+":ro")
|
2016-04-25 19:55:11 +00:00
|
|
|
}
|
|
|
|
args = append(args, []string{
|
|
|
|
"-v", workdir + ":" + workdir,
|
|
|
|
"--workdir=" + workdir,
|
2015-11-11 12:35:31 +00:00
|
|
|
"camlistore/git",
|
2016-04-25 19:55:11 +00:00
|
|
|
"git"}...)
|
|
|
|
args = append(args, gitArgs...)
|
2015-11-11 12:35:31 +00:00
|
|
|
cmd = exec.Command("docker", args...)
|
|
|
|
} else {
|
|
|
|
cmd = exec.Command("git", gitArgs...)
|
2016-04-25 19:55:11 +00:00
|
|
|
cmd.Dir = workdir
|
2015-11-11 12:35:31 +00:00
|
|
|
}
|
|
|
|
return cmd
|
|
|
|
}
|
|
|
|
|
2015-12-29 05:08:17 +00:00
|
|
|
// GitCommit is a datastore entity to track which commits we've
|
|
|
|
// already emailed about.
|
|
|
|
type GitCommit struct {
|
|
|
|
Emailed bool
|
|
|
|
}
|
|
|
|
|
2013-08-23 04:33:23 +00:00
|
|
|
func pollCommits(dir string) {
|
2016-09-05 21:50:19 +00:00
|
|
|
cmd := execGit(dir, "pull_origin", nil, "pull", "origin")
|
2013-08-23 04:33:23 +00:00
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
2016-04-26 00:29:03 +00:00
|
|
|
log.Printf("Error running git pull origin master in %s: %v\n%s", dir, err, out)
|
2013-08-23 04:33:23 +00:00
|
|
|
return
|
|
|
|
}
|
2016-04-26 00:29:03 +00:00
|
|
|
log.Printf("Ran git pull.")
|
2015-12-29 05:08:17 +00:00
|
|
|
// TODO: see if .git/refs/remotes/origin/master
|
|
|
|
// changed. (quicker than running recentCommits each time)
|
2013-08-23 05:03:07 +00:00
|
|
|
|
2013-08-23 04:33:23 +00:00
|
|
|
hashes, err := recentCommits(dir)
|
|
|
|
if err != nil {
|
|
|
|
log.Print(err)
|
|
|
|
return
|
|
|
|
}
|
2013-10-23 14:56:57 +00:00
|
|
|
latestHash.Lock()
|
|
|
|
latestHash.s = hashes[0]
|
|
|
|
latestHash.Unlock()
|
2017-12-05 16:49:05 +00:00
|
|
|
githubSyncC := make(chan bool, 1)
|
|
|
|
go func() {
|
|
|
|
if githubSSHKey != "" {
|
|
|
|
if err := syncToGithub(dir, hashes[0]); err != nil {
|
|
|
|
log.Printf("Failed to push commit %v to github: %v", hashes[0], err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
githubSyncC <- true
|
|
|
|
}()
|
2013-08-23 04:33:23 +00:00
|
|
|
for _, commit := range hashes {
|
|
|
|
if knownCommit[commit] {
|
|
|
|
continue
|
|
|
|
}
|
2015-12-29 05:08:17 +00:00
|
|
|
if dsClient != nil {
|
|
|
|
ctx := context.Background()
|
|
|
|
key := datastore.NewKey(ctx, "git_commit", commit, 0, nil)
|
|
|
|
var gc GitCommit
|
|
|
|
if err := dsClient.Get(ctx, key, &gc); err == nil && gc.Emailed {
|
|
|
|
log.Printf("Already emailed about commit %v; skipping", commit)
|
|
|
|
knownCommit[commit] = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
2017-12-06 18:45:06 +00:00
|
|
|
if err := emailCommit(dir, commit); err != nil {
|
|
|
|
log.Printf("Error with commit e-mail: %v", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
log.Printf("Emailed commit %s", commit)
|
|
|
|
knownCommit[commit] = true
|
|
|
|
if dsClient != nil {
|
|
|
|
ctx := context.Background()
|
|
|
|
key := datastore.NewKey(ctx, "git_commit", commit, 0, nil)
|
|
|
|
_, err := dsClient.Put(ctx, key, &GitCommit{Emailed: true})
|
|
|
|
log.Printf("datastore put of git_commit(%v): %v", commit, err)
|
2013-08-23 04:33:23 +00:00
|
|
|
}
|
|
|
|
}
|
2017-12-05 16:49:05 +00:00
|
|
|
<-githubSyncC
|
2013-08-23 04:33:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func recentCommits(dir string) (hashes []string, err error) {
|
2016-09-05 21:50:19 +00:00
|
|
|
cmd := execGit(dir, "log_origin_master", nil, "log", "--since=1 month ago", "--pretty=oneline", "origin/master")
|
2013-08-23 04:33:23 +00:00
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error running git log in %s: %v\n%s", dir, err, out)
|
|
|
|
}
|
|
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
|
|
v := strings.SplitN(line, " ", 2)
|
|
|
|
if len(v) > 1 {
|
|
|
|
hashes = append(hashes, v[0])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func mailNowHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
select {
|
|
|
|
case <-tokenc:
|
2013-08-23 05:03:07 +00:00
|
|
|
log.Printf("/mailnow got a token")
|
2013-08-23 04:33:23 +00:00
|
|
|
default:
|
|
|
|
// Too many requests. Ignore.
|
2013-08-23 05:03:07 +00:00
|
|
|
log.Printf("Ignoring /mailnow request; too soon.")
|
2013-08-23 04:33:23 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
select {
|
|
|
|
case fetchc <- true:
|
2013-08-23 05:03:07 +00:00
|
|
|
log.Printf("/mailnow triggered a git fetch")
|
2013-08-23 04:33:23 +00:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
}
|
2013-10-23 14:56:57 +00:00
|
|
|
|
|
|
|
func latestHashHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
latestHash.Lock()
|
|
|
|
defer latestHash.Unlock()
|
|
|
|
fmt.Fprint(w, latestHash.s)
|
|
|
|
}
|