Commit 9cd1587c authored by Abiola Ibrahim's avatar Abiola Ibrahim

git: post pull command. retries after pull failure.

parent 782ba324
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
// branch // branch
// key // key
// interval // interval
// then command args
// } // }
// repo - git repository // repo - git repository
// compulsory. Both ssh (e.g. git@github.com:user/project.git) // compulsory. Both ssh (e.g. git@github.com:user/project.git)
...@@ -15,7 +16,6 @@ ...@@ -15,7 +16,6 @@
// //
// path - directory to pull into, relative to site root // path - directory to pull into, relative to site root
// optional. Defaults to site root. // optional. Defaults to site root.
// If set, must be a subdirectory to site root to be valid.
// //
// branch - git branch or tag // branch - git branch or tag
// optional. Defaults to master // optional. Defaults to master
...@@ -26,6 +26,9 @@ ...@@ -26,6 +26,9 @@
// interval- interval between git pulls in seconds // interval- interval between git pulls in seconds
// optional. Defaults to 3600 (1 Hour). // optional. Defaults to 3600 (1 Hour).
// //
// then - command to execute after successful pull
// optional. If set, will execute only when there are new changes.
//
// Examples : // Examples :
// //
// public repo pulled into site root // public repo pulled into site root
...@@ -34,7 +37,7 @@ ...@@ -34,7 +37,7 @@
// public repo pulled into <root>/mysite // public repo pulled into <root>/mysite
// git https://github.com/user/myproject mysite // git https://github.com/user/myproject mysite
// //
// private repo pulled into <root>/mysite with tag v1.0 and interval of 1 day // private repo pulled into <root>/mysite with tag v1.0 and interval of 1 day.
// git { // git {
// repo git@github.com:user/myproject // repo git@github.com:user/myproject
// branch v1.0 // branch v1.0
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/url" "net/url"
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
...@@ -56,7 +57,7 @@ func parse(c middleware.Controller) (*Repo, error) { ...@@ -56,7 +57,7 @@ func parse(c middleware.Controller) (*Repo, error) {
switch len(args) { switch len(args) {
case 2: case 2:
repo.Path = filepath.Join(c.Root(), args[1]) repo.Path = filepath.Clean(c.Root() + string(filepath.Separator) + args[1])
fallthrough fallthrough
case 1: case 1:
repo.Url = args[0] repo.Url = args[0]
...@@ -73,7 +74,7 @@ func parse(c middleware.Controller) (*Repo, error) { ...@@ -73,7 +74,7 @@ func parse(c middleware.Controller) (*Repo, error) {
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
repo.Path = filepath.Join(c.Root(), c.Val()) repo.Path = filepath.Clean(c.Root() + string(filepath.Separator) + c.Val())
case "branch": case "branch":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
...@@ -92,6 +93,12 @@ func parse(c middleware.Controller) (*Repo, error) { ...@@ -92,6 +93,12 @@ func parse(c middleware.Controller) (*Repo, error) {
if t > 0 { if t > 0 {
repo.Interval = time.Duration(t) * time.Second repo.Interval = time.Duration(t) * time.Second
} }
case "then":
thenArgs := c.RemainingArgs()
if len(thenArgs) == 0 {
return nil, c.ArgErr()
}
repo.Then = strings.Join(thenArgs, " ")
} }
} }
} }
...@@ -125,7 +132,7 @@ func parse(c middleware.Controller) (*Repo, error) { ...@@ -125,7 +132,7 @@ func parse(c middleware.Controller) (*Repo, error) {
return nil, err return nil, err
} }
return repo, prepare(repo) return repo, repo.prepare()
} }
// sanitizeHttp cleans up repository url and converts to https format // sanitizeHttp cleans up repository url and converts to https format
...@@ -165,3 +172,11 @@ func sanitizeGit(repoUrl string) (string, string, error) { ...@@ -165,3 +172,11 @@ func sanitizeGit(repoUrl string) (string, string, error) {
host := hostUrl[:i] host := hostUrl[:i]
return repoUrl, host, nil return repoUrl, host, nil
} }
// logger is an helper function to retrieve the available logger
func logger() *log.Logger {
if Logger == nil {
Logger = log.New(os.Stderr, "", log.LstdFlags)
}
return Logger
}
package git package git
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/mholt/caddy/middleware"
) )
// DefaultInterval is the minimum interval to delay before // DefaultInterval is the minimum interval to delay before
// requesting another git pull // requesting another git pull
const DefaultInterval time.Duration = time.Hour * 1 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 // gitBinary holds the absolute path to git executable
var gitBinary string var gitBinary string
...@@ -31,12 +36,15 @@ type Repo struct { ...@@ -31,12 +36,15 @@ type Repo struct {
Branch string // Git branch Branch string // Git branch
KeyPath string // Path to private ssh key KeyPath string // Path to private ssh key
Interval time.Duration // Interval between pulls Interval time.Duration // Interval between pulls
Then string // Command to execute after successful git pull
pulled bool // true if there was a successful pull pulled bool // true if there was a successful pull
lastPull time.Time // time of the last successful pull lastPull time.Time // time of the last successful pull
lastCommit string // hash for the most recent commit
sync.Mutex sync.Mutex
} }
// Pull performs git clone, or git pull if repository exists // Pull attempts a git clone.
// It retries at most numRetries times if error occurs
func (r *Repo) Pull() error { func (r *Repo) Pull() error {
r.Lock() r.Lock()
defer r.Unlock() defer r.Unlock()
...@@ -45,6 +53,33 @@ func (r *Repo) Pull() error { ...@@ -45,6 +53,33 @@ func (r *Repo) Pull() error {
return nil return nil
} }
// keep last commit hash for comparison later
lastCommit := r.lastCommit
var err error
// Attempt to pull at most numRetries times
for i := 0; i < numRetries; i++ {
if err = r.pull(); err == nil {
break
}
logger().Println(err)
}
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} params := []string{"clone", "-b", r.Branch, r.Url, r.Path}
if r.pulled { if r.pulled {
params = []string{"pull", "origin", r.Branch} params = []string{"pull", "origin", r.Branch}
...@@ -52,35 +87,27 @@ func (r *Repo) Pull() error { ...@@ -52,35 +87,27 @@ func (r *Repo) Pull() error {
// if key is specified, pull using ssh key // if key is specified, pull using ssh key
if r.KeyPath != "" { if r.KeyPath != "" {
return pullWithKey(r, params) return r.pullWithKey(params)
} }
cmd := exec.Command(gitBinary, params...) dir := ""
cmd.Env = os.Environ()
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
if r.pulled { if r.pulled {
cmd.Dir = r.Path dir = r.Path
} }
var err error var err error
if err = cmd.Start(); err != nil { if err = runCmd(gitBinary, params, dir); err == nil {
return err
}
if err = cmd.Wait(); err == nil {
r.pulled = true r.pulled = true
r.lastPull = time.Now() r.lastPull = time.Now()
log.Printf("%v pulled.\n", r.Url) logger().Printf("%v pulled.\n", r.Url)
r.lastCommit, err = r.getMostRecentCommit()
} }
return err return err
} }
// pullWithKey performs git clone or git pull if repository exists. // pullWithKey is used for private repositories and requires an ssh key.
// It is used for private repositories and requires an ssh key.
// Note: currently only limited to Linux and OSX. // Note: currently only limited to Linux and OSX.
func pullWithKey(r *Repo, params []string) error { func (r *Repo) pullWithKey(params []string) error {
var gitSsh, script *os.File var gitSsh, script *os.File
// ensure temporary files deleted after usage // ensure temporary files deleted after usage
defer func() { defer func() {
...@@ -105,30 +132,23 @@ func pullWithKey(r *Repo, params []string) error { ...@@ -105,30 +132,23 @@ func pullWithKey(r *Repo, params []string) error {
return err return err
} }
// execute the git clone bash script dir := ""
cmd := exec.Command(script.Name())
cmd.Env = os.Environ()
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
if r.pulled { if r.pulled {
cmd.Dir = r.Path dir = r.Path
} }
if err = cmd.Start(); err != nil { if err = runCmd(script.Name(), nil, dir); err == nil {
return err
}
if err = cmd.Wait(); err == nil {
r.pulled = true r.pulled = true
r.lastPull = time.Now() r.lastPull = time.Now()
log.Printf("%v pulled.\n", r.Url) logger().Printf("%v pulled.\n", r.Url)
r.lastCommit, err = r.getMostRecentCommit()
} }
return err return err
} }
// prepare prepares for a git pull // prepare prepares for a git pull
// and validates the configured directory // and validates the configured directory
func prepare(r *Repo) error { func (r *Repo) prepare() error {
// check if directory exists or is empty // check if directory exists or is empty
// if not, create directory // if not, create directory
fs, err := ioutil.ReadDir(r.Path) fs, err := ioutil.ReadDir(r.Path)
...@@ -148,7 +168,7 @@ func prepare(r *Repo) error { ...@@ -148,7 +168,7 @@ func prepare(r *Repo) error {
if isGit { if isGit {
// check if same repository // check if same repository
var repoUrl string var repoUrl string
if repoUrl, err = getRepoUrl(r.Path); err == nil && repoUrl == r.Url { if repoUrl, err = r.getRepoUrl(); err == nil && repoUrl == r.Url {
r.pulled = true r.pulled = true
return nil return nil
} }
...@@ -160,23 +180,42 @@ func prepare(r *Repo) error { ...@@ -160,23 +180,42 @@ func prepare(r *Repo) error {
return fmt.Errorf("Cannot git clone into %v, directory not empty.", r.Path) return fmt.Errorf("Cannot git clone into %v, directory not empty.", r.Path)
} }
// getRepoUrl retrieves remote origin url for the git repository at path // getMostRecentCommit gets the hash of the most recent commit to the
func getRepoUrl(path string) (string, error) { // repository. Useful for checking if changes occur.
args := []string{"config", "--get", "remote.origin.url"} func (r *Repo) getMostRecentCommit() (string, error) {
command := gitBinary + ` --no-pager log -n 1 --pretty=format:"%H"`
_, err := os.Stat(path) c, args, err := middleware.SplitCommandAndArgs(command)
if err != nil { if err != nil {
return "", err return "", err
} }
return runCmdOutput(c, args, r.Path)
}
cmd := exec.Command(gitBinary, args...) // getRepoUrl retrieves remote origin url for the git repository at path
cmd.Dir = path func (r *Repo) getRepoUrl() (string, error) {
output, err := cmd.Output() _, err := os.Stat(r.Path)
if err != nil { if err != nil {
return "", err return "", err
} }
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
}
return strings.TrimSpace(string(output)), nil 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 // initGit validates git installation and locates the git executable
...@@ -199,6 +238,33 @@ func initGit() error { ...@@ -199,6 +238,33 @@ func initGit() error {
} }
// 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. // writeScriptFile writes content to a temporary file.
// It changes the temporary file mode to executable and // It changes the temporary file mode to executable and
// closes it to prepare it for execution. // closes it to prepare it for execution.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment