blob: 112cd66709e049cec739868986e24f778a8ae945 [file] [log] [blame]
// Copyright 2016 Google Inc. All rights reserved.
//
// 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 cache
import (
"bytes"
"fmt"
"log"
"net"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"
git "gopkg.in/src-d/go-git.v4"
)
// gitCache manages a set of bare git repositories. Repositories are
// recognized by URLs. Port numbers in git URLs are not part of the
// cache key.
type gitCache struct {
// directory to hold the repositories.
dir string
// Directory to store log files for fetches and clones.
logDir string
}
// newGitCache constructs a gitCache object.
func newGitCache(baseDir string, opts Options) (*gitCache, error) {
c := gitCache{
dir: filepath.Join(baseDir),
logDir: filepath.Join(baseDir, "slothfs-logs"),
}
if err := os.MkdirAll(c.logDir, 0700); err != nil {
return nil, err
}
if err := os.MkdirAll(c.dir, 0700); err != nil {
return nil, err
}
if opts.FetchFrequency > 0 {
go c.recurringFetch(opts.FetchFrequency)
}
return &c, nil
}
func (c *gitCache) recurringFetch(freq time.Duration) {
ticker := time.NewTicker(freq)
for {
if err := c.FetchAll(); err != nil {
log.Printf("FetchAll: %v", err)
}
<-ticker.C
}
}
// logfile returns a logfile open for writing with a unique name.
func (c *gitCache) logfile() (*os.File, error) {
nm := fmt.Sprintf("%s/git.%s.log", c.logDir, time.Now().Format(time.RFC3339Nano))
nm = strings.Replace(nm, ":", "_", -1)
return os.Create(nm)
}
// Fetch updates the local clone of the given repository.
func (c *gitCache) Fetch(dir string) error {
if err := c.runGit(c.dir, "--git-dir="+dir, "fetch", "origin"); err != nil {
return err
}
return nil
}
// FetchAll finds all known repos and runs git-fetch on them.
func (c *gitCache) FetchAll() error {
dir, err := filepath.EvalSymlinks(c.dir)
if err != nil {
return err
}
var dirs []string
if err := filepath.Walk(dir, func(n string, fi os.FileInfo, err error) error {
if fi.IsDir() && strings.HasSuffix(n, ".git") {
dirs = append(dirs, n)
return filepath.SkipDir
}
return nil
}); err != nil {
return err
}
for _, d := range dirs {
if err := c.Fetch(d); err != nil {
return fmt.Errorf("fetch %s: %v", d, err)
}
}
return nil
}
// gitPath transforms a URL into a path under the gitCache directory.
func (c *gitCache) gitPath(u string) (string, error) {
parsed, err := url.Parse(u)
if err != nil {
return "", err
}
if h, _, err := net.SplitHostPort(parsed.Host); err == nil {
parsed.Host = h
}
p := path.Clean(parsed.Path)
if path.Base(p) == ".git" {
p = path.Dir(p)
}
return filepath.Join(c.dir, parsed.Host, p+".git"), nil
}
// runGit runs git with the given arguments under the given directory.
func (c *gitCache) runGit(dir string, args ...string) error {
logfile, err := c.logfile()
if err != nil {
return err
}
defer logfile.Close()
cmd := exec.Command("git", args...)
cmd.Dir = dir
var out, errOut bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &errOut
runErr := cmd.Run()
if _, err := fmt.Fprintf(logfile, "args: %s\ndir:%s\nEXIT: %s\n\nOUT\n%s\n\nERR\n\n", cmd.Args,
cmd.Dir, out.String(), errOut.String()); err != nil {
return fmt.Errorf("logfile write for %s (%v): %v",
args, runErr, err)
}
if err := logfile.Close(); err != nil {
return fmt.Errorf("logfile close for %s (%v): %v",
args, runErr, err)
}
if err != nil {
log.Printf("ran %s exit %v", cmd.Args, err)
}
return runErr
}
// OpenLocal returns an opened repository for the given URL, if it is available locally.
func (c *gitCache) OpenLocal(url string) *git.Repository {
p, err := c.gitPath(url)
if err != nil {
return nil
}
repo, err := git.PlainOpen(p)
if err != nil {
return nil
}
return repo
}
// Open returns an opened repository for the given URL. If necessary,
// the repository is cloned.
func (c *gitCache) Open(url string) (*git.Repository, error) {
// TODO(hanwen): multiple concurrent calls to Open() with the
// same URL may race, resulting in a double clone. It's unclear
// what will happen in that case.
p, err := c.gitPath(url)
if err != nil {
return nil, err
}
if _, err := os.Lstat(p); os.IsNotExist(err) {
dir, base := filepath.Split(p)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, err
}
if err := c.runGit(dir, "clone", "--bare", "--progress", "--verbose", url, base); err != nil {
return nil, err
}
}
repo, err := git.PlainOpen(p)
return repo, err
}