Run VCS commands with a timeout

Fixes issue #268.
diff --git a/gosrc/vcs.go b/gosrc/vcs.go
index 9bc5fd0..95e7c4a 100644
--- a/gosrc/vcs.go
+++ b/gosrc/vcs.go
@@ -10,6 +10,7 @@
 
 import (
 	"bytes"
+	"errors"
 	"io/ioutil"
 	"log"
 	"net/http"
@@ -19,6 +20,7 @@
 	"path/filepath"
 	"regexp"
 	"strings"
+	"time"
 )
 
 func init() {
@@ -30,6 +32,13 @@
 	getVCSDirFn = getVCSDir
 }
 
+const (
+	lsRemoteTimeout = 5 * time.Minute
+	cloneTimeout    = 10 * time.Minute
+	fetchTimeout    = 5 * time.Minute
+	checkoutTimeout = 1 * time.Minute
+)
+
 // Store temporary data in this directory.
 var TempDir = filepath.Join(os.TempDir(), "gddo")
 
@@ -115,7 +124,7 @@
 		cmd := exec.Command("git", "ls-remote", "--heads", "--tags", schemes[i]+"://"+repo+".git")
 		log.Println(strings.Join(cmd.Args, " "))
 		var err error
-		p, err = cmd.Output()
+		p, err = outputWithTimeout(cmd, lsRemoteTimeout)
 		if err == nil {
 			scheme = schemes[i]
 			break
@@ -151,7 +160,7 @@
 		}
 		cmd := exec.Command("git", "clone", scheme+"://"+repo+".git", dir)
 		log.Println(strings.Join(cmd.Args, " "))
-		if err := cmd.Run(); err != nil {
+		if err := runWithTimeout(cmd, cloneTimeout); err != nil {
 			return "", "", err
 		}
 	case string(bytes.TrimRight(p, "\n")) == commit:
@@ -160,14 +169,14 @@
 		cmd := exec.Command("git", "fetch")
 		log.Println(strings.Join(cmd.Args, " "))
 		cmd.Dir = dir
-		if err := cmd.Run(); err != nil {
+		if err := runWithTimeout(cmd, fetchTimeout); err != nil {
 			return "", "", err
 		}
 	}
 
 	cmd := exec.Command("git", "checkout", "--detach", "--force", commit)
 	cmd.Dir = dir
-	if err := cmd.Run(); err != nil {
+	if err := runWithTimeout(cmd, checkoutTimeout); err != nil {
 		return "", "", err
 	}
 
@@ -205,14 +214,14 @@
 		}
 		cmd := exec.Command("svn", "checkout", scheme+"://"+repo, "-r", revno, dir)
 		log.Println(strings.Join(cmd.Args, " "))
-		if err := cmd.Run(); err != nil {
+		if err := runWithTimeout(cmd, cloneTimeout); err != nil {
 			return "", "", err
 		}
 	case localRevno != revno:
 		cmd := exec.Command("svn", "update", "-r", revno)
 		log.Println(strings.Join(cmd.Args, " "))
 		cmd.Dir = dir
-		if err := cmd.Run(); err != nil {
+		if err := runWithTimeout(cmd, fetchTimeout); err != nil {
 			return "", "", err
 		}
 	}
@@ -225,7 +234,7 @@
 func getSVNRevision(target string) (string, error) {
 	cmd := exec.Command("svn", "info", target)
 	log.Println(strings.Join(cmd.Args, " "))
-	out, err := cmd.Output()
+	out, err := outputWithTimeout(cmd, lsRemoteTimeout)
 	if err != nil {
 		return "", err
 	}
@@ -319,3 +328,22 @@
 		Files:          files,
 	}, nil
 }
+
+func runWithTimeout(cmd *exec.Cmd, timeout time.Duration) error {
+	if err := cmd.Start(); err != nil {
+		return err
+	}
+	t := time.AfterFunc(timeout, func() { cmd.Process.Kill() })
+	defer t.Stop()
+	return cmd.Wait()
+}
+
+func outputWithTimeout(cmd *exec.Cmd, timeout time.Duration) ([]byte, error) {
+	if cmd.Stdout != nil {
+		return nil, errors.New("exec: Stdout already set")
+	}
+	var b bytes.Buffer
+	cmd.Stdout = &b
+	err := runWithTimeout(cmd, timeout)
+	return b.Bytes(), err
+}