2015-05-01 10:11:01 +08:00
|
|
|
package git
|
|
|
|
|
|
|
|
import (
|
2015-05-05 01:49:49 +08:00
|
|
|
"bytes"
|
2015-05-01 10:11:01 +08:00
|
|
|
"fmt"
|
2015-05-05 01:49:49 +08:00
|
|
|
"io/ioutil"
|
2015-05-02 02:19:25 +08:00
|
|
|
"log"
|
2015-05-02 08:44:51 +08:00
|
|
|
"os"
|
2015-05-05 01:49:49 +08:00
|
|
|
"os/exec"
|
2015-05-01 10:11:01 +08:00
|
|
|
"strings"
|
2015-05-05 01:49:49 +08:00
|
|
|
"sync"
|
2015-05-01 10:11:01 +08:00
|
|
|
"time"
|
|
|
|
|
2015-05-02 02:19:25 +08:00
|
|
|
"github.com/mholt/caddy/middleware"
|
|
|
|
)
|
2015-05-01 10:11:01 +08:00
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// DefaultInterval is the minimum interval to delay before
|
|
|
|
// requesting another git pull
|
|
|
|
const DefaultInterval time.Duration = time.Hour * 1
|
|
|
|
|
|
|
|
// Number of retries if git pull fails
|
|
|
|
const numRetries = 3
|
|
|
|
|
|
|
|
// gitBinary holds the absolute path to git executable
|
|
|
|
var gitBinary string
|
|
|
|
|
|
|
|
// initMutex prevents parallel attempt to validate
|
|
|
|
// git availability in PATH
|
|
|
|
var initMutex sync.Mutex = sync.Mutex{}
|
|
|
|
|
2015-05-02 02:19:25 +08:00
|
|
|
// Logger is used to log errors; if nil, the default log.Logger is used.
|
|
|
|
var Logger *log.Logger
|
2015-05-01 10:11:01 +08:00
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// logger is an helper function to retrieve the available logger
|
|
|
|
func logger() *log.Logger {
|
|
|
|
if Logger == nil {
|
|
|
|
Logger = log.New(os.Stderr, "", log.LstdFlags)
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
2015-05-05 01:49:49 +08:00
|
|
|
return Logger
|
|
|
|
}
|
2015-05-02 02:19:25 +08:00
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// Repo is the structure that holds required information
|
|
|
|
// of a git repository.
|
|
|
|
type Repo struct {
|
|
|
|
Url string // Repository URL
|
|
|
|
Path string // Directory to pull to
|
|
|
|
Host string // Git domain host e.g. github.com
|
|
|
|
Branch string // Git branch
|
|
|
|
KeyPath string // Path to private ssh key
|
|
|
|
Interval time.Duration // Interval between pulls
|
|
|
|
Then string // Command to execute after successful git pull
|
|
|
|
pulled bool // true if there was a successful pull
|
|
|
|
lastPull time.Time // time of the last successful pull
|
|
|
|
lastCommit string // hash for the most recent commit
|
|
|
|
sync.Mutex
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// Pull attempts a git clone.
|
|
|
|
// It retries at most numRetries times if error occurs
|
|
|
|
func (r *Repo) Pull() error {
|
|
|
|
r.Lock()
|
|
|
|
defer r.Unlock()
|
|
|
|
// if it is less than interval since last pull, return
|
|
|
|
if time.Since(r.lastPull) <= r.Interval {
|
|
|
|
return nil
|
|
|
|
}
|
2015-05-01 10:11:01 +08:00
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// keep last commit hash for comparison later
|
|
|
|
lastCommit := r.lastCommit
|
2015-05-01 10:11:01 +08:00
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
var err error
|
|
|
|
// Attempt to pull at most numRetries times
|
|
|
|
for i := 0; i < numRetries; i++ {
|
|
|
|
if err = r.pull(); err == nil {
|
|
|
|
break
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
2015-05-05 01:49:49 +08:00
|
|
|
logger().Println(err)
|
|
|
|
}
|
2015-05-01 10:11:01 +08:00
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if there are new changes,
|
|
|
|
// then execute post pull command
|
|
|
|
if r.lastCommit == lastCommit {
|
|
|
|
logger().Println("No new changes.")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return r.postPullCommand()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pull performs git clone, or git pull if repository exists
|
|
|
|
func (r *Repo) pull() error {
|
|
|
|
params := []string{"clone", "-b", r.Branch, r.Url, r.Path}
|
|
|
|
if r.pulled {
|
|
|
|
params = []string{"pull", "origin", r.Branch}
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// if key is specified, pull using ssh key
|
|
|
|
if r.KeyPath != "" {
|
|
|
|
return r.pullWithKey(params)
|
|
|
|
}
|
|
|
|
|
|
|
|
dir := ""
|
|
|
|
if r.pulled {
|
|
|
|
dir = r.Path
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
2015-05-05 01:49:49 +08:00
|
|
|
if err = runCmd(gitBinary, params, dir); err == nil {
|
|
|
|
r.pulled = true
|
|
|
|
r.lastPull = time.Now()
|
|
|
|
logger().Printf("%v pulled.\n", r.Url)
|
|
|
|
r.lastCommit, err = r.getMostRecentCommit()
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// pullWithKey is used for private repositories and requires an ssh key.
|
|
|
|
// Note: currently only limited to Linux and OSX.
|
|
|
|
func (r *Repo) pullWithKey(params []string) error {
|
|
|
|
var gitSsh, script *os.File
|
|
|
|
// ensure temporary files deleted after usage
|
|
|
|
defer func() {
|
|
|
|
if gitSsh != nil {
|
|
|
|
os.Remove(gitSsh.Name())
|
|
|
|
}
|
|
|
|
if script != nil {
|
|
|
|
os.Remove(script.Name())
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
2015-05-05 01:49:49 +08:00
|
|
|
}()
|
|
|
|
|
|
|
|
var err error
|
|
|
|
// write git.sh script to temp file
|
|
|
|
gitSsh, err = writeScriptFile(gitWrapperScript(gitBinary))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// write git clone bash script to file
|
|
|
|
script, err = writeScriptFile(bashScript(gitSsh.Name(), r, params))
|
2015-05-01 10:11:01 +08:00
|
|
|
if err != nil {
|
2015-05-05 01:49:49 +08:00
|
|
|
return err
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
dir := ""
|
|
|
|
if r.pulled {
|
|
|
|
dir = r.Path
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
if err = runCmd(script.Name(), nil, dir); err == nil {
|
|
|
|
r.pulled = true
|
|
|
|
r.lastPull = time.Now()
|
|
|
|
logger().Printf("%v pulled.\n", r.Url)
|
|
|
|
r.lastCommit, err = r.getMostRecentCommit()
|
|
|
|
}
|
|
|
|
return err
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// Prepare prepares for a git pull
|
|
|
|
// and validates the configured directory
|
|
|
|
func (r *Repo) Prepare() error {
|
|
|
|
// check if directory exists or is empty
|
|
|
|
// if not, create directory
|
|
|
|
fs, err := ioutil.ReadDir(r.Path)
|
|
|
|
if err != nil || len(fs) == 0 {
|
|
|
|
return os.MkdirAll(r.Path, os.FileMode(0755))
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// validate git repo
|
|
|
|
isGit := false
|
|
|
|
for _, f := range fs {
|
|
|
|
if f.IsDir() && f.Name() == ".git" {
|
|
|
|
isGit = true
|
|
|
|
break
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
if isGit {
|
|
|
|
// check if same repository
|
|
|
|
var repoUrl string
|
|
|
|
if repoUrl, err = r.getRepoUrl(); err == nil && repoUrl == r.Url {
|
|
|
|
r.pulled = true
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Cannot retrieve repo url for %v Error: %v", r.Path, err)
|
|
|
|
}
|
|
|
|
return fmt.Errorf("Another git repo '%v' exists at %v", repoUrl, r.Path)
|
|
|
|
}
|
|
|
|
return fmt.Errorf("Cannot git clone into %v, directory not empty.", r.Path)
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// getMostRecentCommit gets the hash of the most recent commit to the
|
|
|
|
// repository. Useful for checking if changes occur.
|
|
|
|
func (r *Repo) getMostRecentCommit() (string, error) {
|
|
|
|
command := gitBinary + ` --no-pager log -n 1 --pretty=format:"%H"`
|
|
|
|
c, args, err := middleware.SplitCommandAndArgs(command)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return runCmdOutput(c, args, r.Path)
|
2015-05-01 10:11:01 +08:00
|
|
|
}
|
2015-05-02 08:44:51 +08:00
|
|
|
|
2015-05-05 01:49:49 +08:00
|
|
|
// getRepoUrl retrieves remote origin url for the git repository at path
|
|
|
|
func (r *Repo) getRepoUrl() (string, error) {
|
|
|
|
_, err := os.Stat(r.Path)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
2015-05-02 08:44:51 +08:00
|
|
|
}
|
2015-05-05 01:49:49 +08:00
|
|
|
args := []string{"config", "--get", "remote.origin.url"}
|
|
|
|
return runCmdOutput(gitBinary, args, r.Path)
|
|
|
|
}
|
|
|
|
|
|
|
|
// postPullCommand executes r.Then.
|
|
|
|
// It is trigged after successful git pull
|
|
|
|
func (r *Repo) postPullCommand() error {
|
|
|
|
if r.Then == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
c, args, err := middleware.SplitCommandAndArgs(r.Then)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = runCmd(c, args, r.Path); err == nil {
|
|
|
|
logger().Printf("Command %v successful.\n", r.Then)
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// InitGit validates git installation and locates the git executable
|
|
|
|
// binary in PATH
|
|
|
|
func InitGit() error {
|
|
|
|
// prevent concurrent call
|
|
|
|
initMutex.Lock()
|
|
|
|
defer initMutex.Unlock()
|
|
|
|
|
|
|
|
// if validation has been done before and binary located in
|
|
|
|
// PATH, return.
|
|
|
|
if gitBinary != "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// locate git binary in path
|
|
|
|
var err error
|
|
|
|
gitBinary, err = exec.LookPath("git")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// runCmd is a helper function to run commands.
|
|
|
|
// It runs command with args from directory at dir.
|
|
|
|
// The executed process outputs to os.Stderr
|
|
|
|
func runCmd(command string, args []string, dir string) error {
|
|
|
|
cmd := exec.Command(command, args...)
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
cmd.Stdout = os.Stderr
|
|
|
|
cmd.Dir = dir
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return cmd.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
// runCmdOutput is a helper function to run commands and return output.
|
|
|
|
// It runs command with args from directory at dir.
|
|
|
|
// If successful, returns output and nil error
|
|
|
|
func runCmdOutput(command string, args []string, dir string) (string, error) {
|
|
|
|
cmd := exec.Command(command, args...)
|
|
|
|
cmd.Dir = dir
|
|
|
|
var err error
|
|
|
|
if output, err := cmd.Output(); err == nil {
|
|
|
|
return string(bytes.TrimSpace(output)), nil
|
|
|
|
}
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
// writeScriptFile writes content to a temporary file.
|
|
|
|
// It changes the temporary file mode to executable and
|
|
|
|
// closes it to prepare it for execution.
|
|
|
|
func writeScriptFile(content []byte) (file *os.File, err error) {
|
|
|
|
if file, err = ioutil.TempFile("", "caddy"); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if _, err = file.Write(content); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err = file.Chmod(os.FileMode(0755)); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return file, file.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
// gitWrapperScript forms content for git.sh script
|
|
|
|
var gitWrapperScript = func(gitBinary string) []byte {
|
|
|
|
return []byte(fmt.Sprintf(`#!/bin/bash
|
|
|
|
|
|
|
|
# The MIT License (MIT)
|
|
|
|
# Copyright (c) 2013 Alvin Abad
|
|
|
|
|
|
|
|
if [ $# -eq 0 ]; then
|
|
|
|
echo "Git wrapper script that can specify an ssh-key file
|
|
|
|
Usage:
|
|
|
|
git.sh -i ssh-key-file git-command
|
|
|
|
"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
|
|
# remove temporary file on exit
|
|
|
|
trap 'rm -f /tmp/.git_ssh.$$' 0
|
|
|
|
|
|
|
|
if [ "$1" = "-i" ]; then
|
|
|
|
SSH_KEY=$2; shift; shift
|
|
|
|
echo "ssh -i $SSH_KEY \$@" > /tmp/.git_ssh.$$
|
|
|
|
chmod +x /tmp/.git_ssh.$$
|
|
|
|
export GIT_SSH=/tmp/.git_ssh.$$
|
|
|
|
fi
|
|
|
|
|
|
|
|
# in case the git command is repeated
|
|
|
|
[ "$1" = "git" ] && shift
|
|
|
|
|
|
|
|
# Run the git command
|
|
|
|
%v "$@"
|
|
|
|
|
|
|
|
`, gitBinary))
|
|
|
|
}
|
|
|
|
|
|
|
|
// bashScript forms content of bash script to clone or update a repo using ssh
|
|
|
|
var bashScript = func(gitShPath string, repo *Repo, params []string) []byte {
|
|
|
|
return []byte(fmt.Sprintf(`#!/bin/bash
|
|
|
|
|
|
|
|
mkdir -p ~/.ssh;
|
|
|
|
touch ~/.ssh/known_hosts;
|
|
|
|
ssh-keyscan -t rsa,dsa %v 2>&1 | sort -u - ~/.ssh/known_hosts > ~/.ssh/tmp_hosts;
|
|
|
|
cat ~/.ssh/tmp_hosts >> ~/.ssh/known_hosts;
|
|
|
|
%v -i %v %v;
|
|
|
|
`, repo.Host, gitShPath, repo.KeyPath, strings.Join(params, " ")))
|
2015-05-02 08:44:51 +08:00
|
|
|
}
|