diff --git a/config/setup/git.go b/config/setup/git.go index 499ca3fac..a9390e51f 100644 --- a/config/setup/git.go +++ b/config/setup/git.go @@ -96,6 +96,8 @@ func gitParse(c *Controller) (*git.Repo, error) { return nil, c.ArgErr() } repo.Then = strings.Join(thenArgs, " ") + default: + return nil, c.ArgErr() } } } @@ -124,8 +126,8 @@ func gitParse(c *Controller) (*git.Repo, error) { return nil, err } - // validate git availability in PATH - if err = git.InitGit(); err != nil { + // validate git requirements + if err = git.Init(); err != nil { return nil, err } @@ -153,19 +155,39 @@ func sanitizeHttp(repoUrl string) (string, string, error) { } repoUrl = "https://" + url.Host + url.Path + + // add .git suffix if missing + if !strings.HasSuffix(repoUrl, ".git") { + repoUrl += ".git" + } + return repoUrl, url.Host, nil } -// sanitizeGit cleans up repository url and validate ssh format. +// sanitizeGit cleans up repository url and converts to ssh format for private +// repositories if required. // Returns sanitized url, hostName (e.g. github.com, bitbucket.com) // and possible error func sanitizeGit(repoUrl string) (string, string, error) { repoUrl = strings.TrimSpace(repoUrl) + + // check if valid ssh format if !strings.HasPrefix(repoUrl, "git@") || strings.Index(repoUrl, ":") < len("git@a:") { - return "", "", fmt.Errorf("Invalid git url %s", repoUrl) + // check if valid http format and convert to ssh + if url, err := url.Parse(repoUrl); err == nil && strings.HasPrefix(url.Scheme, "http") { + repoUrl = fmt.Sprintf("git@%v:%v", url.Host, url.Path[1:]) + } else { + return "", "", fmt.Errorf("Invalid git url %s", repoUrl) + } } hostUrl := repoUrl[len("git@"):] i := strings.Index(hostUrl, ":") host := hostUrl[:i] + + // add .git suffix if missing + if !strings.HasSuffix(repoUrl, ".git") { + repoUrl += ".git" + } + return repoUrl, host, nil } diff --git a/config/setup/git_test.go b/config/setup/git_test.go new file mode 100644 index 000000000..2c1a494c1 --- /dev/null +++ b/config/setup/git_test.go @@ -0,0 +1,144 @@ +package setup + +import ( + "testing" + "time" + + "github.com/mholt/caddy/middleware/git" + "github.com/mholt/caddy/middleware/git/gittest" +) + +// init sets the OS used to fakeOS +func init() { + git.SetOS(gittest.FakeOS) +} + +func TestGit(t *testing.T) { + c := newTestController(`git git@github.com:mholt/caddy.git`) + + mid, err := Git(c) + if err != nil { + t.Errorf("Expected no errors, but got: %v", err) + } + if mid != nil { + t.Fatal("Git middleware is a background service and expected to be nil.") + } +} + +func TestGitParse(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expected *git.Repo + }{ + {`git git@github.com:user/repo`, false, &git.Repo{ + Url: "https://github.com/user/repo.git", + }}, + {`git github.com/user/repo`, false, &git.Repo{ + Url: "https://github.com/user/repo.git", + }}, + {`git git@github.com/user/repo`, true, nil}, + {`git http://github.com/user/repo`, false, &git.Repo{ + Url: "https://github.com/user/repo.git", + }}, + {`git https://github.com/user/repo`, false, &git.Repo{ + Url: "https://github.com/user/repo.git", + }}, + {`git http://github.com/user/repo { + key ~/.key + }`, false, &git.Repo{ + KeyPath: "~/.key", + Url: "git@github.com:user/repo.git", + }}, + {`git git@github.com:user/repo { + key ~/.key + }`, false, &git.Repo{ + KeyPath: "~/.key", + Url: "git@github.com:user/repo.git", + }}, + {`git `, true, nil}, + {`git { + }`, true, nil}, + {`git { + repo git@github.com:user/repo.git`, true, nil}, + {`git { + repo git@github.com:user/repo + key ~/.key + }`, false, &git.Repo{ + KeyPath: "~/.key", + Url: "git@github.com:user/repo.git", + }}, + {`git { + repo git@github.com:user/repo + key ~/.key + interval 600 + }`, false, &git.Repo{ + KeyPath: "~/.key", + Url: "git@github.com:user/repo.git", + Interval: time.Second * 600, + }}, + {`git { + repo git@github.com:user/repo + branch dev + }`, false, &git.Repo{ + Branch: "dev", + Url: "https://github.com/user/repo.git", + }}, + {`git { + key ~/.key + }`, true, nil}, + {`git { + repo git@github.com:user/repo + key ~/.key + then echo hello world + }`, false, &git.Repo{ + KeyPath: "~/.key", + Url: "git@github.com:user/repo.git", + Then: "echo hello world", + }}, + } + + for i, test := range tests { + c := newTestController(test.input) + repo, err := gitParse(c) + if !test.shouldErr && err != nil { + t.Errorf("Test %v should not error but found %v", i, err) + continue + } + if test.shouldErr && err == nil { + t.Errorf("Test %v should error but found nil", i) + continue + } + if !reposEqual(test.expected, repo) { + t.Errorf("Test %v expects %v but found %v", i, test.expected, repo) + } + } +} + +func reposEqual(expected, repo *git.Repo) bool { + if expected == nil { + return repo == nil + } + if expected.Branch != "" && expected.Branch != repo.Branch { + return false + } + if expected.Host != "" && expected.Host != repo.Host { + return false + } + if expected.Interval != 0 && expected.Interval != repo.Interval { + return false + } + if expected.KeyPath != "" && expected.KeyPath != repo.KeyPath { + return false + } + if expected.Path != "" && expected.Path != repo.Path { + return false + } + if expected.Then != "" && expected.Then != repo.Then { + return false + } + if expected.Url != "" && expected.Url != repo.Url { + return false + } + return true +} diff --git a/middleware/git/git.go b/middleware/git/git.go index 5a617ce34..5ed87395d 100644 --- a/middleware/git/git.go +++ b/middleware/git/git.go @@ -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, " "))) } diff --git a/middleware/git/git_test.go b/middleware/git/git_test.go new file mode 100644 index 000000000..a2e8f1e82 --- /dev/null +++ b/middleware/git/git_test.go @@ -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 "$@" + +` diff --git a/middleware/git/gitos/gitos.go b/middleware/git/gitos/gitos.go new file mode 100644 index 000000000..5da3f4fa1 --- /dev/null +++ b/middleware/git/gitos/gitos.go @@ -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...)} +} diff --git a/middleware/git/gittest/gittest.go b/middleware/git/gittest/gittest.go new file mode 100644 index 000000000..9e9a116fd --- /dev/null +++ b/middleware/git/gittest/gittest.go @@ -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{} +} diff --git a/middleware/git/os.go b/middleware/git/os.go new file mode 100644 index 000000000..57c177159 --- /dev/null +++ b/middleware/git/os.go @@ -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 +} diff --git a/middleware/rewrite/rewrite_test.go b/middleware/rewrite/rewrite_test.go index 684e6b213..dd9c77df1 100644 --- a/middleware/rewrite/rewrite_test.go +++ b/middleware/rewrite/rewrite_test.go @@ -28,6 +28,7 @@ func TestRewrite(t *testing.T) { []string{"/abc/", "ab", "/abc/{file}", ".html|"}, []string{"/abcd/", "ab", "/a/{dir}/{file}", ".html|"}, []string{"/abcde/", "ab", "/a#{frag}", ".html|"}, + []string{"/ab/", `.*\.jpg`, "/ajpg", ""}, } for _, regexpRule := range regexpRules { @@ -76,6 +77,7 @@ func TestRewrite(t *testing.T) { {"/abcd/abcd.html", "/a/abcd/abcd.html"}, {"/abcde/abcde.html", "/a"}, {"/abcde/abcde.html#1234", "/a#1234"}, + {"/ab/ab.jpg", "/ajpg"}, } for i, test := range tests {