Git: Minor fixes. Refactor. Added tests.

This commit is contained in:
Abiola Ibrahim
2015-05-22 01:38:07 +01:00
parent d311345aa5
commit f44cd5d740
8 changed files with 781 additions and 36 deletions
+52 -32
View File
@@ -3,15 +3,14 @@ package git
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"sync"
"time"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/git/gitos"
)
// DefaultInterval is the minimum interval to delay before
@@ -24,8 +23,11 @@ const numRetries = 3
// gitBinary holds the absolute path to git executable
var gitBinary string
// shell holds the shell to be used. Either sh or bash.
var shell string
// initMutex prevents parallel attempt to validate
// git availability in PATH
// git requirements.
var initMutex sync.Mutex = sync.Mutex{}
// Logger is used to log errors; if nil, the default log.Logger is used.
@@ -120,20 +122,20 @@ func (r *Repo) pull() error {
// 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
var gitSsh, script gitos.File
// ensure temporary files deleted after usage
defer func() {
if gitSsh != nil {
os.Remove(gitSsh.Name())
gos.Remove(gitSsh.Name())
}
if script != nil {
os.Remove(script.Name())
gos.Remove(script.Name())
}
}()
var err error
// write git.sh script to temp file
gitSsh, err = writeScriptFile(gitWrapperScript(gitBinary))
gitSsh, err = writeScriptFile(gitWrapperScript())
if err != nil {
return err
}
@@ -163,9 +165,9 @@ func (r *Repo) pullWithKey(params []string) error {
func (r *Repo) Prepare() error {
// check if directory exists or is empty
// if not, create directory
fs, err := ioutil.ReadDir(r.Path)
fs, err := gos.ReadDir(r.Path)
if err != nil || len(fs) == 0 {
return os.MkdirAll(r.Path, os.FileMode(0755))
return gos.MkdirAll(r.Path, os.FileMode(0755))
}
// validate git repo
@@ -180,9 +182,15 @@ func (r *Repo) Prepare() error {
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 repoUrl, err = r.getRepoUrl(); err == nil {
// add .git suffix if missing for adequate comparison.
if !strings.HasSuffix(repoUrl, ".git") {
repoUrl += ".git"
}
if 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)
@@ -205,7 +213,7 @@ func (r *Repo) getMostRecentCommit() (string, error) {
// getRepoUrl retrieves remote origin url for the git repository at path
func (r *Repo) getRepoUrl() (string, error) {
_, err := os.Stat(r.Path)
_, err := gos.Stat(r.Path)
if err != nil {
return "", err
}
@@ -230,9 +238,9 @@ func (r *Repo) postPullCommand() error {
return err
}
// InitGit validates git installation and locates the git executable
// binary in PATH
func InitGit() error {
// Init validates git installation, locates the git executable
// binary in PATH and check for available shell to use.
func Init() error {
// prevent concurrent call
initMutex.Lock()
defer initMutex.Unlock()
@@ -245,18 +253,30 @@ func InitGit() error {
// locate git binary in path
var err error
gitBinary, err = exec.LookPath("git")
return err
if gitBinary, err = gos.LookPath("git"); err != nil {
return fmt.Errorf("Git middleware requires git installed. Cannot find git binary in PATH")
}
// locate bash in PATH. If not found, fallback to sh.
// If neither is found, return error.
shell = "bash"
if _, err = gos.LookPath("bash"); err != nil {
shell = "sh"
if _, err = gos.LookPath("sh"); err != nil {
return fmt.Errorf("Git middleware requires either bash or sh.")
}
}
return nil
}
// 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
cmd := gos.Command(command, args...)
cmd.Stdout(os.Stderr)
cmd.Stderr(os.Stderr)
cmd.Dir(dir)
if err := cmd.Start(); err != nil {
return err
}
@@ -267,8 +287,8 @@ func runCmd(command string, args []string, dir string) error {
// 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
cmd := gos.Command(command, args...)
cmd.Dir(dir)
var err error
if output, err := cmd.Output(); err == nil {
return string(bytes.TrimSpace(output)), nil
@@ -279,8 +299,8 @@ func runCmdOutput(command string, args []string, dir string) (string, error) {
// 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 {
func writeScriptFile(content []byte) (file gitos.File, err error) {
if file, err = gos.TempFile("", "caddy"); err != nil {
return nil, err
}
if _, err = file.Write(content); err != nil {
@@ -293,8 +313,8 @@ func writeScriptFile(content []byte) (file *os.File, err error) {
}
// gitWrapperScript forms content for git.sh script
var gitWrapperScript = func(gitBinary string) []byte {
return []byte(fmt.Sprintf(`#!/bin/bash
func gitWrapperScript() []byte {
return []byte(fmt.Sprintf(`#!/bin/%v
# The MIT License (MIT)
# Copyright (c) 2013 Alvin Abad
@@ -323,17 +343,17 @@ fi
# Run the git command
%v "$@"
`, gitBinary))
`, shell, 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
func bashScript(gitShPath string, repo *Repo, params []string) []byte {
return []byte(fmt.Sprintf(`#!/bin/%v
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, " ")))
`, shell, repo.Host, gitShPath, repo.KeyPath, strings.Join(params, " ")))
}
+218
View File
@@ -0,0 +1,218 @@
package git
import (
"io/ioutil"
"log"
"testing"
"time"
"github.com/mholt/caddy/middleware/git/gittest"
)
// init sets the OS used to fakeOS.
func init() {
SetOS(gittest.FakeOS)
}
func check(t *testing.T, err error) {
if err != nil {
t.Errorf("Error not expected but found %v", err)
}
}
func TestInit(t *testing.T) {
err := Init()
check(t, err)
}
func TestHelpers(t *testing.T) {
f, err := writeScriptFile([]byte("script"))
check(t, err)
var b [6]byte
_, err = f.Read(b[:])
check(t, err)
if string(b[:]) != "script" {
t.Errorf("Expected script found %v", string(b[:]))
}
out, err := runCmdOutput(gitBinary, []string{"-version"}, "")
check(t, err)
if out != gittest.CmdOutput {
t.Errorf("Expected %v found %v", gittest.CmdOutput, out)
}
err = runCmd(gitBinary, []string{"-version"}, "")
check(t, err)
wScript := gitWrapperScript()
if string(wScript) != expectedWrapperScript {
t.Errorf("Expected %v found %v", expectedWrapperScript, string(wScript))
}
f, err = writeScriptFile(wScript)
check(t, err)
repo := &Repo{Host: "github.com", KeyPath: "~/.key"}
script := string(bashScript(f.Name(), repo, []string{"clone", "git@github.com/repo/user"}))
if script != expectedBashScript {
t.Errorf("Expected %v found %v", expectedBashScript, script)
}
}
func TestGit(t *testing.T) {
// prepare
repos := []*Repo{
nil,
&Repo{Path: "gitdir", Url: "success.git"},
}
for _, r := range repos {
repo := createRepo(r)
err := repo.Prepare()
check(t, err)
}
// pull with success
logFile := gittest.Open("file")
Logger = log.New(logFile, "", 0)
tests := []struct {
repo *Repo
output string
}{
{
&Repo{Path: "gitdir", Url: "git@github.com:user/repo.git", KeyPath: "~/.key", Then: "echo Hello"},
`git@github.com:user/repo.git pulled.
Command echo Hello successful.
`,
},
{
&Repo{Path: "gitdir", Url: "https://github.com/user/repo.git", Then: "echo Hello"},
`https://github.com/user/repo.git pulled.
Command echo Hello successful.
`,
},
{
&Repo{Url: "git@github.com:user/repo"},
`git@github.com:user/repo pulled.
`,
},
}
for i, test := range tests {
gittest.CmdOutput = test.repo.Url
test.repo = createRepo(test.repo)
err := test.repo.Prepare()
check(t, err)
err = test.repo.Pull()
check(t, err)
out, err := ioutil.ReadAll(logFile)
check(t, err)
if test.output != string(out) {
t.Errorf("Pull with Success %v: Expected %v found %v", i, test.output, string(out))
}
}
// pull with error
repos = []*Repo{
&Repo{Path: "gitdir", Url: "http://github.com:u/repo.git"},
&Repo{Path: "gitdir", Url: "https://github.com/user/repo.git", Then: "echo Hello"},
&Repo{Path: "gitdir"},
&Repo{Path: "gitdir", KeyPath: ".key"},
}
gittest.CmdOutput = "git@github.com:u1/repo.git"
for i, repo := range repos {
repo = createRepo(repo)
err := repo.Prepare()
if err == nil {
t.Errorf("Pull with Error %v: Error expected but not found %v", i, err)
continue
}
expected := "Another git repo 'git@github.com:u1/repo.git' exists at gitdir"
if expected != err.Error() {
t.Errorf("Pull with Error %v: Expected %v found %v", i, expected, err.Error())
}
}
}
func createRepo(r *Repo) *Repo {
repo := &Repo{
Url: "git@github.com/user/test",
Path: ".",
Host: "github.com",
Branch: "master",
Interval: time.Second * 60,
}
if r == nil {
return repo
}
if r.Branch != "" {
repo.Branch = r.Branch
}
if r.Host != "" {
repo.Branch = r.Branch
}
if r.Interval != 0 {
repo.Interval = r.Interval
}
if r.KeyPath != "" {
repo.KeyPath = r.KeyPath
}
if r.Path != "" {
repo.Path = r.Path
}
if r.Then != "" {
repo.Then = r.Then
}
if r.Url != "" {
repo.Url = r.Url
}
return repo
}
var expectedBashScript = `#!/bin/bash
mkdir -p ~/.ssh;
touch ~/.ssh/known_hosts;
ssh-keyscan -t rsa,dsa github.com 2>&1 | sort -u - ~/.ssh/known_hosts > ~/.ssh/tmp_hosts;
cat ~/.ssh/tmp_hosts >> ~/.ssh/known_hosts;
` + gittest.TempFileName + ` -i ~/.key clone git@github.com/repo/user;
`
var expectedWrapperScript = `#!/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
/usr/bin/git "$@"
`
+160
View File
@@ -0,0 +1,160 @@
package gitos
import (
"io"
"io/ioutil"
"os"
"os/exec"
)
// File is an abstraction for file (os.File).
type File interface {
// Name returns the name of the file
Name() string
// Stat returns the FileInfo structure describing file.
Stat() (os.FileInfo, error)
// Close closes the File, rendering it unusable for I/O.
Close() error
// Chmod changes the mode of the file.
Chmod(os.FileMode) error
// Read reads up to len(b) bytes from the File. It returns the number of
// bytes read and an error, if any.
Read([]byte) (int, error)
// Write writes len(b) bytes to the File. It returns the number of bytes
// written and an error, if any.
Write([]byte) (int, error)
}
// Cmd is an abstraction for external commands (os.Cmd).
type Cmd interface {
// Run starts the specified command and waits for it to complete.
Run() error
// Start starts the specified command but does not wait for it to complete.
Start() error
// Wait waits for the command to exit. It must have been started by Start.
Wait() error
// Output runs the command and returns its standard output.
Output() ([]byte, error)
// Dir sets the working directory of the command.
Dir(string)
// Stdin sets the process's standard input.
Stdin(io.Reader)
// Stdout sets the process's standard output.
Stdout(io.Writer)
// Stderr sets the process's standard output.
Stderr(io.Writer)
}
// gitCmd represents external commands executed by git.
type gitCmd struct {
*exec.Cmd
}
// Dir sets the working directory of the command.
func (g *gitCmd) Dir(dir string) {
g.Cmd.Dir = dir
}
// Stdin sets the process's standard input.
func (g *gitCmd) Stdin(stdin io.Reader) {
g.Cmd.Stdin = stdin
}
// Stdout sets the process's standard output.
func (g *gitCmd) Stdout(stdout io.Writer) {
g.Cmd.Stdout = stdout
}
// Stderr sets the process's standard output.
func (g *gitCmd) Stderr(stderr io.Writer) {
g.Cmd.Stderr = stderr
}
// OS is an abstraction for required OS level functions.
type OS interface {
// Command returns the Cmd to execute the named program with the
// given arguments.
Command(string, ...string) Cmd
// Mkdir creates a new directory with the specified name and permission
// bits.
Mkdir(string, os.FileMode) error
// MkdirAll creates a directory named path, along with any necessary
// parents.
MkdirAll(string, os.FileMode) error
// Stat returns a FileInfo describing the named file.
Stat(string) (os.FileInfo, error)
// Remove removes the named file or directory.
Remove(string) error
// ReadDir reads the directory named by dirname and returns a list of
// directory entries.
ReadDir(string) ([]os.FileInfo, error)
// LookPath searches for an executable binary named file in the directories
// named by the PATH environment variable.
LookPath(string) (string, error)
// TempFile creates a new temporary file in the directory dir with a name
// beginning with prefix, opens the file for reading and writing, and
// returns the resulting File.
TempFile(string, string) (File, error)
}
// GitOS is the implementation of OS for git.
type GitOS struct{}
// Mkdir calls os.Mkdir.
func (g GitOS) Mkdir(name string, perm os.FileMode) error {
return os.Mkdir(name, perm)
}
// MkdirAll calls os.MkdirAll.
func (g GitOS) MkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}
// Stat calls os.Stat.
func (g GitOS) Stat(name string) (os.FileInfo, error) {
return os.Stat(name)
}
// Remove calls os.Remove.
func (g GitOS) Remove(name string) error {
return os.Remove(name)
}
// LookPath calls exec.LookPath.
func (g GitOS) LookPath(file string) (string, error) {
return exec.LookPath(file)
}
// TempFile calls ioutil.TempFile.
func (g GitOS) TempFile(dir, prefix string) (File, error) {
return ioutil.TempFile(dir, prefix)
}
// ReadDir calls ioutil.ReadDir.
func (g GitOS) ReadDir(dirname string) ([]os.FileInfo, error) {
return ioutil.ReadDir(dirname)
}
// Command calls exec.Command.
func (g GitOS) Command(name string, args ...string) Cmd {
return &gitCmd{exec.Command(name, args...)}
}
+167
View File
@@ -0,0 +1,167 @@
// Package gittest is a test package for the git middleware.
// It implements a mock gitos.OS, gitos.Cmd and gitos.File.
package gittest
import (
"io"
"os"
"time"
"github.com/mholt/caddy/middleware/git/gitos"
)
// FakeOS implements a mock gitos.OS, gitos.Cmd and gitos.File.
var FakeOS = fakeOS{}
// CmdOutput is the output of any call to the mocked gitos.Cmd's Output().
var CmdOutput = "success"
// TempFileName is the name of any file returned by mocked gitos.OS's TempFile().
var TempFileName = "tempfile"
// dirs mocks a fake git dir if filename is "gitdir".
var dirs = map[string][]os.FileInfo{
"gitdir": {
fakeInfo{name: ".git", dir: true},
},
}
// Open creates a new mock gitos.File.
func Open(name string) gitos.File {
return &fakeFile{name: name}
}
// fakeFile is a mock gitos.File.
type fakeFile struct {
name string
dir bool
content []byte
info fakeInfo
}
func (f fakeFile) Name() string {
return f.name
}
func (f fakeFile) Stat() (os.FileInfo, error) {
return fakeInfo{name: f.name}, nil
}
func (f fakeFile) Close() error {
return nil
}
func (f fakeFile) Chmod(mode os.FileMode) error {
f.info.mode = mode
return nil
}
func (f *fakeFile) Read(b []byte) (int, error) {
if len(f.content) == 0 {
return 0, io.EOF
}
n := copy(b, f.content)
f.content = f.content[n:]
return n, nil
}
func (f *fakeFile) Write(b []byte) (int, error) {
f.content = append(f.content, b...)
return len(b), nil
}
// fakeCmd is a mock git.Cmd.
type fakeCmd struct{}
func (f fakeCmd) Run() error {
return nil
}
func (f fakeCmd) Start() error {
return nil
}
func (f fakeCmd) Wait() error {
return nil
}
func (f fakeCmd) Output() ([]byte, error) {
return []byte(CmdOutput), nil
}
func (f fakeCmd) Dir(dir string) {}
func (f fakeCmd) Stdin(stdin io.Reader) {}
func (f fakeCmd) Stdout(stdout io.Writer) {}
func (f fakeCmd) Stderr(stderr io.Writer) {}
// fakeInfo is a mock os.FileInfo.
type fakeInfo struct {
name string
dir bool
mode os.FileMode
}
func (f fakeInfo) Name() string {
return f.name
}
func (f fakeInfo) Size() int64 {
return 1024
}
func (f fakeInfo) Mode() os.FileMode {
return f.mode
}
func (f fakeInfo) ModTime() time.Time {
return time.Now().Truncate(time.Hour)
}
func (f fakeInfo) IsDir() bool {
return f.dir
}
func (f fakeInfo) Sys() interface{} {
return nil
}
// fakeOS is a mock git.OS.
type fakeOS struct{}
func (f fakeOS) Mkdir(name string, perm os.FileMode) error {
return nil
}
func (f fakeOS) MkdirAll(path string, perm os.FileMode) error {
return nil
}
func (f fakeOS) Stat(name string) (os.FileInfo, error) {
return fakeInfo{name: name}, nil
}
func (f fakeOS) Remove(name string) error {
return nil
}
func (f fakeOS) LookPath(file string) (string, error) {
return "/usr/bin/" + file, nil
}
func (f fakeOS) TempFile(dir, prefix string) (gitos.File, error) {
return &fakeFile{name: TempFileName, info: fakeInfo{name: TempFileName}}, nil
}
func (f fakeOS) ReadDir(dirname string) ([]os.FileInfo, error) {
if f, ok := dirs[dirname]; ok {
return f, nil
}
return nil, nil
}
func (f fakeOS) Command(name string, args ...string) gitos.Cmd {
return fakeCmd{}
}
+12
View File
@@ -0,0 +1,12 @@
package git
import "github.com/mholt/caddy/middleware/git/gitos"
// gos is the OS used by git.
var gos gitos.OS = gitos.GitOS{}
// SetOS sets the OS to be used. Intended to be used for tests
// to abstract OS level git actions.
func SetOS(os gitos.OS) {
gos = os
}