Merge branch 'master' into multitransfer

Conflicts:
	lfs/pointer_smudge.go
	lfs/upload_queue.go
This commit is contained in:
rubyist 2015-05-21 13:47:52 -04:00
commit 22d3f4e6d6
52 changed files with 1176 additions and 242 deletions

2
.gitignore vendored

@ -8,3 +8,5 @@ servertest
man/* man/*
*.test *.test
tmp
test/remote

@ -1,9 +1,10 @@
package commands package commands
import ( import (
"os"
"github.com/github/git-lfs/lfs" "github.com/github/git-lfs/lfs"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os"
) )
var ( var (

@ -1,10 +1,11 @@
package commands package commands
import ( import (
"os"
"github.com/github/git-lfs/git" "github.com/github/git-lfs/git"
"github.com/github/git-lfs/lfs" "github.com/github/git-lfs/lfs"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os"
) )
var ( var (

@ -2,11 +2,12 @@ package commands
import ( import (
"errors" "errors"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
) )
var ( var (

@ -6,11 +6,12 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
) )
var ( var (

@ -1,11 +1,12 @@
package commands package commands
import ( import (
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
) )
var ( var (

@ -1,13 +1,14 @@
package commands package commands
import ( import (
"io/ioutil"
"os"
"strings"
"github.com/github/git-lfs/git" "github.com/github/git-lfs/git"
"github.com/github/git-lfs/lfs" "github.com/github/git-lfs/lfs"
"github.com/rubyist/tracerx" "github.com/rubyist/tracerx"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"io/ioutil"
"os"
"strings"
) )
var ( var (

@ -2,11 +2,12 @@ package commands
import ( import (
"bytes" "bytes"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
) )
var ( var (

@ -2,6 +2,7 @@ package commands
import ( import (
"fmt" "fmt"
"github.com/github/git-lfs/git" "github.com/github/git-lfs/git"
"github.com/github/git-lfs/lfs" "github.com/github/git-lfs/lfs"
"github.com/spf13/cobra" "github.com/spf13/cobra"

@ -3,12 +3,13 @@ package commands
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
) )
var ( var (

@ -2,11 +2,12 @@ package commands
import ( import (
"bufio" "bufio"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
) )
var ( var (

@ -3,8 +3,6 @@ package commands
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
"io" "io"
"log" "log"
"os" "os"
@ -12,6 +10,9 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/github/git-lfs/lfs"
"github.com/spf13/cobra"
) )
var ( var (
@ -65,7 +66,7 @@ func LoggedError(err error, format string, args ...interface{}) {
file := handlePanic(err) file := handlePanic(err)
if len(file) > 0 { if len(file) > 0 {
fmt.Fprintf(os.Stderr, "\nErrors logged to %s.\nUse `git lfs logs last` to view the log.\n", file) fmt.Fprintf(os.Stderr, "\nErrors logged to %s\nUse `git lfs logs last` to view the log.\n", file)
} }
} }
@ -105,7 +106,60 @@ func handlePanic(err error) string {
return "" return ""
} }
return logPanic(err, false) return logPanic(err)
}
func logPanic(loggedError error) string {
var fmtWriter io.Writer = os.Stderr
now := time.Now()
name := now.Format("20060102T150405.999999999")
full := filepath.Join(lfs.LocalLogDir, name+".log")
if err := os.MkdirAll(lfs.LocalLogDir, 0755); err != nil {
full = ""
fmt.Fprintf(fmtWriter, "Unable to log panic to %s: %s\n\n", lfs.LocalLogDir, err.Error())
} else if file, err := os.Create(full); err != nil {
filename := full
full = ""
defer func() {
fmt.Fprintf(fmtWriter, "Unable to log panic to %s\n\n", filename)
logPanicToWriter(fmtWriter, err)
}()
} else {
fmtWriter = file
defer file.Close()
}
logPanicToWriter(fmtWriter, loggedError)
return full
}
func logPanicToWriter(w io.Writer, loggedError error) {
fmt.Fprintf(w, "> %s", filepath.Base(os.Args[0]))
if len(os.Args) > 0 {
fmt.Fprintf(w, " %s", strings.Join(os.Args[1:], " "))
}
fmt.Fprintln(w)
logEnv(w)
fmt.Fprintln(w)
w.Write(ErrorBuffer.Bytes())
fmt.Fprintln(w)
fmt.Fprintln(w, loggedError.Error())
if wErr, ok := loggedError.(ErrorWithStack); ok {
fmt.Fprintln(w, wErr.InnerError())
for key, value := range wErr.Context() {
fmt.Fprintf(w, "%s=%s\n", key, value)
}
w.Write(wErr.Stack())
} else {
w.Write(lfs.Stack())
}
} }
func logEnv(w io.Writer) { func logEnv(w io.Writer) {
@ -114,56 +168,6 @@ func logEnv(w io.Writer) {
} }
} }
func logPanic(loggedError error, recursive bool) string {
var fmtWriter io.Writer = os.Stderr
if err := os.MkdirAll(lfs.LocalLogDir, 0755); err != nil {
fmt.Fprintf(fmtWriter, "Unable to log panic to %s: %s\n\n", lfs.LocalLogDir, err.Error())
return ""
}
now := time.Now()
name := now.Format("20060102T150405.999999999")
full := filepath.Join(lfs.LocalLogDir, name+".log")
file, err := os.Create(full)
if err == nil {
fmtWriter = file
defer file.Close()
}
fmt.Fprintf(fmtWriter, "> %s", filepath.Base(os.Args[0]))
if len(os.Args) > 0 {
fmt.Fprintf(fmtWriter, " %s", strings.Join(os.Args[1:], " "))
}
fmt.Fprint(fmtWriter, "\n")
logEnv(fmtWriter)
fmt.Fprint(fmtWriter, "\n")
fmtWriter.Write(ErrorBuffer.Bytes())
fmt.Fprint(fmtWriter, "\n")
fmt.Fprintln(fmtWriter, loggedError.Error())
if wErr, ok := loggedError.(ErrorWithStack); ok {
fmt.Fprintln(fmtWriter, wErr.InnerError())
for key, value := range wErr.Context() {
fmt.Fprintf(fmtWriter, "%s=%s\n", key, value)
}
fmtWriter.Write(wErr.Stack())
} else {
fmtWriter.Write(lfs.Stack())
}
if err != nil && !recursive {
fmt.Fprintf(fmtWriter, "Unable to log panic to %s\n\n", full)
logPanic(err, true)
}
return full
}
type ErrorWithStack interface { type ErrorWithStack interface {
Context() map[string]string Context() map[string]string
InnerError() string InnerError() string

@ -3,7 +3,6 @@ package commands
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/bmizerany/assert"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
@ -11,6 +10,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/bmizerany/assert"
) )
var ( var (

@ -1,12 +1,13 @@
package commands package commands
import ( import (
"github.com/bmizerany/assert"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/bmizerany/assert"
) )
func TestInit(t *testing.T) { func TestInit(t *testing.T) {

@ -1,12 +1,13 @@
package commands package commands
import ( import (
"github.com/bmizerany/assert"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/bmizerany/assert"
) )
func TestPointerWithBuildAndCompareStdinMismatch(t *testing.T) { func TestPointerWithBuildAndCompareStdinMismatch(t *testing.T) {

@ -2,11 +2,12 @@ package commands
import ( import (
"bytes" "bytes"
"github.com/bmizerany/assert"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/bmizerany/assert"
) )
func TestSmudge(t *testing.T) { func TestSmudge(t *testing.T) {

@ -1,11 +1,12 @@
package commands package commands
import ( import (
"github.com/bmizerany/assert"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/bmizerany/assert"
) )
func TestTrack(t *testing.T) { func TestTrack(t *testing.T) {

@ -2,8 +2,9 @@ package commands
import ( import (
"fmt" "fmt"
"github.com/github/git-lfs/lfs"
"testing" "testing"
"github.com/github/git-lfs/lfs"
) )
func TestVersionOnEmptyRepository(t *testing.T) { func TestVersionOnEmptyRepository(t *testing.T) {

@ -1,43 +1,12 @@
## Building on Linux ## Building on Linux
## Ubuntu 14.04 (Trusty Tahr) There are build scripts for recent versions of CentOS- and Debian-flavored
Linuxes in `../scripts/{centos,debian}-build`. Both install all prerequisites,
then build the client and the man pages in Docker containers for CentOS 7,
Debian 8, and Ubuntu 14.04.
### Building On CentOS 6, the client builds, but not the man pages, because of problems
getting the right version of Ruby.
``` Earlier versions of CentOS and Debian/Ubuntu have trouble building go, so they
sudo apt-get install golang-go git are non-starters.
./script/bootstrap
```
That will place a git-lfs binary in the `bin/` directory. Copy the binary to a directory in your path:
```
sudo cp bin/git-lfs /usr/local/bin
```
Try it:
```
[949][rubiojr@octox] git lfs
git-lfs v0.0.1
[~]
[949][rubiojr@octox] git lfs init
git lfs initialized
```
### Installing the man pages
You'll need ruby and rubygems to install the `ronn` gem:
```
sudo apt-get install ruby build-essential
sudo gem install ronn
./script/man
sudo mkdir -p /usr/local/share/man/man1
sudo cp man/*.1 /usr/local/share/man/man1
```
`git help lfs` should show the git-lfs man pages now.

@ -171,7 +171,7 @@ exist, and the sha-256 signature of the contents matches the given OID.
* Write the pointer file to STDOUT. * Write the pointer file to STDOUT.
Note that the `clean` filter does not push the file to the server. Use the Note that the `clean` filter does not push the file to the server. Use the
`git lfs sync` command to do that. `git push` command to do that (lfs files are pushed before commits in a pre-push hook).
The `smudge` filter runs as files are being checked out from the Git repository The `smudge` filter runs as files are being checked out from the Git repository
to the working directory. Git sends the content of the Git blob as STDIN, and to the working directory. Git sends the content of the Git blob as STDIN, and

@ -4,10 +4,11 @@ package git
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/rubyist/tracerx"
"io" "io"
"os/exec" "os/exec"
"strings" "strings"
"github.com/rubyist/tracerx"
) )
func LsRemote(remote, remoteRef string) (string, error) { func LsRemote(remote, remoteRef string) (string, error) {
@ -108,7 +109,8 @@ func simpleExec(stdin io.Reader, name string, arg ...string) (string, error) {
output, err := cmd.Output() output, err := cmd.Output()
if _, ok := err.(*exec.ExitError); ok { if _, ok := err.(*exec.ExitError); ok {
return "", nil return "", nil
} else if err != nil { }
if err != nil {
return fmt.Sprintf("Error running %s %s", name, arg), err return fmt.Sprintf("Error running %s %s", name, arg), err
} }

@ -6,7 +6,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/rubyist/tracerx"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -14,6 +13,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"github.com/rubyist/tracerx"
) )
const ( const (
@ -447,42 +448,42 @@ func doHttpRequest(req *http.Request, creds Creds) (*http.Response, *WrappedErro
func doApiRequestWithRedirects(req *http.Request, creds Creds, via []*http.Request) (*http.Response, *WrappedError) { func doApiRequestWithRedirects(req *http.Request, creds Creds, via []*http.Request) (*http.Response, *WrappedError) {
res, wErr := doHttpRequest(req, creds) res, wErr := doHttpRequest(req, creds)
if wErr != nil { if wErr != nil {
return res, wErr return nil, wErr
} }
if res.StatusCode == 307 { if res.StatusCode == 307 {
redirectedReq, redirectedCreds, err := newClientRequest(req.Method, res.Header.Get("Location")) redirectedReq, redirectedCreds, err := newClientRequest(req.Method, res.Header.Get("Location"))
if err != nil { if err != nil {
return res, Errorf(err, err.Error()) return nil, Errorf(err, err.Error())
} }
via = append(via, req) via = append(via, req)
if seeker, ok := req.Body.(io.Seeker); ok { seeker, ok := req.Body.(io.Seeker)
_, err := seeker.Seek(0, 0) if !ok {
if err != nil { return nil, Errorf(nil, "Request body needs to be an io.Seeker to handle redirects.")
return res, Error(err)
}
redirectedReq.Body = req.Body
redirectedReq.ContentLength = req.ContentLength
} else {
return res, Errorf(nil, "Request body needs to be an io.Seeker to handle redirects.")
} }
if _, err := seeker.Seek(0, 0); err != nil {
return nil, Error(err)
}
redirectedReq.Body = req.Body
redirectedReq.ContentLength = req.ContentLength
if err = checkRedirect(redirectedReq, via); err != nil { if err = checkRedirect(redirectedReq, via); err != nil {
return res, Errorf(err, err.Error()) return nil, Errorf(err, err.Error())
} }
return doApiRequestWithRedirects(redirectedReq, redirectedCreds, via) return doApiRequestWithRedirects(redirectedReq, redirectedCreds, via)
} }
return res, wErr return res, nil
} }
func doApiRequest(req *http.Request, creds Creds) (*http.Response, *objectResource, *WrappedError) { func doApiRequest(req *http.Request, creds Creds) (*http.Response, *objectResource, *WrappedError) {
via := make([]*http.Request, 0, 4) via := make([]*http.Request, 0, 4)
res, wErr := doApiRequestWithRedirects(req, creds, via) res, wErr := doApiRequestWithRedirects(req, creds, via)
if wErr != nil { if wErr != nil {
return res, nil, wErr return nil, nil, wErr
} }
obj := &objectResource{} obj := &objectResource{}
@ -490,9 +491,10 @@ func doApiRequest(req *http.Request, creds Creds) (*http.Response, *objectResour
if wErr != nil { if wErr != nil {
setErrorResponseContext(wErr, res) setErrorResponseContext(wErr, res)
return nil, nil, wErr
} }
return res, obj, wErr return res, obj, nil
} }
func doApiBatchRequest(req *http.Request, creds Creds) (*http.Response, []*objectResource, *WrappedError) { func doApiBatchRequest(req *http.Request, creds Creds) (*http.Response, []*objectResource, *WrappedError) {
@ -608,26 +610,33 @@ func newApiRequest(method, oid string) (*http.Request, Creds, error) {
} }
req, creds, err := newClientRequest(method, u.String()) req, creds, err := newClientRequest(method, u.String())
if err == nil { if err != nil {
req.Header.Set("Accept", mediaType) return nil, nil, err
if res.Header != nil { }
for key, value := range res.Header {
req.Header.Set(key, value) req.Header.Set("Accept", mediaType)
} if res.Header != nil {
for key, value := range res.Header {
req.Header.Set(key, value)
} }
} }
return req, creds, err
return req, creds, nil
} }
func newClientRequest(method, rawurl string) (*http.Request, Creds, error) { func newClientRequest(method, rawurl string) (*http.Request, Creds, error) {
req, err := http.NewRequest(method, rawurl, nil) req, err := http.NewRequest(method, rawurl, nil)
if err != nil { if err != nil {
return req, nil, err return nil, nil, err
} }
req.Header.Set("User-Agent", UserAgent) req.Header.Set("User-Agent", UserAgent)
creds, err := getCreds(req) creds, err := getCreds(req)
return req, creds, err if err != nil {
return nil, nil, err
}
return req, creds, nil
} }
func getCreds(req *http.Request) (Creds, error) { func getCreds(req *http.Request) (Creds, error) {
@ -656,18 +665,18 @@ func getCreds(req *http.Request) (Creds, error) {
return nil, nil return nil, nil
} }
func setErrorRequestContext(err *WrappedError, req *http.Request) {
err.Set("Endpoint", Config.Endpoint().Url)
err.Set("URL", fmt.Sprintf("%s %s", req.Method, req.URL.String()))
setErrorHeaderContext(err, "Response", req.Header)
}
func setErrorResponseContext(err *WrappedError, res *http.Response) { func setErrorResponseContext(err *WrappedError, res *http.Response) {
err.Set("Status", res.Status) err.Set("Status", res.Status)
setErrorHeaderContext(err, "Request", res.Header) setErrorHeaderContext(err, "Request", res.Header)
setErrorRequestContext(err, res.Request) setErrorRequestContext(err, res.Request)
} }
func setErrorRequestContext(err *WrappedError, req *http.Request) {
err.Set("Endpoint", Config.Endpoint().Url)
err.Set("URL", fmt.Sprintf("%s %s", req.Method, req.URL.String()))
setErrorHeaderContext(err, "Response", req.Header)
}
func setErrorHeaderContext(err *WrappedError, prefix string, head http.Header) { func setErrorHeaderContext(err *WrappedError, prefix string, head http.Header) {
for key, _ := range head { for key, _ := range head {
contextKey := fmt.Sprintf("%s:%s", prefix, key) contextKey := fmt.Sprintf("%s:%s", prefix, key)

@ -2,7 +2,6 @@ package lfs
import ( import (
"fmt" "fmt"
"github.com/github/git-lfs/git"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -11,6 +10,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"github.com/github/git-lfs/git"
) )
type Configuration struct { type Configuration struct {

@ -1,8 +1,9 @@
package lfs package lfs
import ( import (
"github.com/bmizerany/assert"
"testing" "testing"
"github.com/bmizerany/assert"
) )
func TestEndpointDefaultsToOrigin(t *testing.T) { func TestEndpointDefaultsToOrigin(t *testing.T) {

@ -4,13 +4,14 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"github.com/rubyist/tracerx"
"io" "io"
"net" "net"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/rubyist/tracerx"
) )
func DoHTTP(c *Configuration, req *http.Request) (*http.Response, error) { func DoHTTP(c *Configuration, req *http.Request) (*http.Response, error) {
@ -24,24 +25,29 @@ func DoHTTP(c *Configuration, req *http.Request) (*http.Response, error) {
} }
func (c *Configuration) HttpClient() *http.Client { func (c *Configuration) HttpClient() *http.Client {
if c.httpClient == nil { if c.httpClient != nil {
tr := &http.Transport{ return c.httpClient
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
sslVerify, _ := c.GitConfig("http.sslverify")
if sslVerify == "false" || len(os.Getenv("GIT_SSL_NO_VERIFY")) > 0 {
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
c.httpClient = &http.Client{
Transport: tr,
CheckRedirect: checkRedirect,
}
} }
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
sslVerify, _ := c.GitConfig("http.sslverify")
if sslVerify == "false" || len(os.Getenv("GIT_SSL_NO_VERIFY")) > 0 {
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
c.httpClient = &http.Client{
Transport: tr,
CheckRedirect: checkRedirect,
}
return c.httpClient return c.httpClient
} }

@ -2,13 +2,14 @@ package lfs
import ( import (
"fmt" "fmt"
"github.com/github/git-lfs/git"
"github.com/rubyist/tracerx"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"github.com/github/git-lfs/git"
"github.com/rubyist/tracerx"
) )
const Version = "0.5.1" const Version = "0.5.1"
@ -129,15 +130,16 @@ func recursiveResolveGitDir(dir string) (string, string, error) {
} }
gitDir := filepath.Join(dir, gitExt) gitDir := filepath.Join(dir, gitExt)
if info, err := os.Stat(gitDir); err == nil { info, err := os.Stat(gitDir)
if info.IsDir() { if err != nil {
return dir, gitDir, nil return recursiveResolveGitDir(filepath.Dir(dir))
} else {
return processDotGitFile(gitDir)
}
} }
return recursiveResolveGitDir(filepath.Dir(dir)) if info.IsDir() {
return dir, gitDir, nil
}
return processDotGitFile(gitDir)
} }
func processDotGitFile(file string) (string, string, error) { func processDotGitFile(file string) (string, string, error) {

@ -115,11 +115,11 @@ func decodeKV(data []byte) (*Pointer, error) {
sizeStr, ok := parsed["size"] sizeStr, ok := parsed["size"]
if !ok { if !ok {
return nil, errors.New("Invalid Oid") return nil, errors.New("Invalid Oid")
} else { }
size, err = strconv.ParseInt(sizeStr, 10, 0)
if err != nil { size, err = strconv.ParseInt(sizeStr, 10, 0)
return nil, errors.New("Invalid size: " + sizeStr) if err != nil {
} return nil, errors.New("Invalid size: " + sizeStr)
} }
return NewPointer(oid, size), nil return NewPointer(oid, size), nil

@ -8,7 +8,7 @@ import (
"github.com/cheggaaa/pb" "github.com/cheggaaa/pb"
"github.com/rubyist/tracerx" "github.com/rubyist/tracerx"
"github.com/technoweenie/go-contentaddressable" contentaddressable "github.com/technoweenie/go-contentaddressable"
) )
func PointerSmudge(writer io.Writer, ptr *Pointer, workingfile string, cb CopyCallback) error { func PointerSmudge(writer io.Writer, ptr *Pointer, workingfile string, cb CopyCallback) error {

@ -3,9 +3,10 @@ package lfs
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"github.com/bmizerany/assert"
"strings" "strings"
"testing" "testing"
"github.com/bmizerany/assert"
) )
func TestEncode(t *testing.T) { func TestEncode(t *testing.T) {

@ -3,13 +3,14 @@ package lfs
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"github.com/rubyist/tracerx"
"io" "io"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/rubyist/tracerx"
) )
const ( const (
@ -52,7 +53,11 @@ var z40 = regexp.MustCompile(`\^?0{40}`)
// for all Git LFS pointers it finds for that ref. // for all Git LFS pointers it finds for that ref.
func ScanRefs(refLeft, refRight string) ([]*wrappedPointer, error) { func ScanRefs(refLeft, refRight string) ([]*wrappedPointer, error) {
nameMap := make(map[string]string, 0) nameMap := make(map[string]string, 0)
start := time.Now() start := time.Now()
defer func() {
tracerx.PerformanceSince("scan", start)
}()
revs, err := revListShas(refLeft, refRight, refLeft == "", nameMap) revs, err := revListShas(refLeft, refRight, refLeft == "", nameMap)
if err != nil { if err != nil {
@ -77,8 +82,6 @@ func ScanRefs(refLeft, refRight string) ([]*wrappedPointer, error) {
pointers = append(pointers, p) pointers = append(pointers, p)
} }
tracerx.PerformanceSince("scan", start)
return pointers, nil return pointers, nil
} }
@ -86,7 +89,11 @@ func ScanRefs(refLeft, refRight string) ([]*wrappedPointer, error) {
// Git LFS pointers it finds in the index. // Git LFS pointers it finds in the index.
func ScanIndex() ([]*wrappedPointer, error) { func ScanIndex() ([]*wrappedPointer, error) {
nameMap := make(map[string]*indexFile, 0) nameMap := make(map[string]*indexFile, 0)
start := time.Now() start := time.Now()
defer func() {
tracerx.PerformanceSince("scan-staging", start)
}()
revs, err := revListIndex(false, nameMap) revs, err := revListIndex(false, nameMap)
if err != nil { if err != nil {
@ -135,8 +142,6 @@ func ScanIndex() ([]*wrappedPointer, error) {
pointers = append(pointers, p) pointers = append(pointers, p)
} }
tracerx.PerformanceSince("scan-staging", start)
return pointers, nil return pointers, nil
} }

@ -3,13 +3,14 @@ package lfs
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/github/git-lfs/git"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/github/git-lfs/git"
) )
var ( var (
@ -45,11 +46,9 @@ func InstallHooks(force bool) error {
hookPath := filepath.Join(LocalGitDir, "hooks", "pre-push") hookPath := filepath.Join(LocalGitDir, "hooks", "pre-push")
if _, err := os.Stat(hookPath); err == nil && !force { if _, err := os.Stat(hookPath); err == nil && !force {
return upgradeHookOrError(hookPath, "pre-push", prePushHook, prePushUpgrades) return upgradeHookOrError(hookPath, "pre-push", prePushHook, prePushUpgrades)
} else {
return ioutil.WriteFile(hookPath, []byte(prePushHook+"\n"), 0755)
} }
return nil return ioutil.WriteFile(hookPath, []byte(prePushHook+"\n"), 0755)
} }
func upgradeHookOrError(hookPath, hookName, hook string, upgrades map[string]bool) error { func upgradeHookOrError(hookPath, hookName, hook string, upgrades map[string]bool) error {
@ -77,15 +76,16 @@ func upgradeHookOrError(hookPath, hookName, hook string, upgrades map[string]boo
} }
func InstallFilters() error { func InstallFilters() error {
var err error if err := setFilter("clean"); err != nil {
err = setFilter("clean") return err
if err == nil {
err = setFilter("smudge")
} }
if err == nil { if err := setFilter("smudge"); err != nil {
err = requireFilters() return err
} }
return err if err := requireFilters(); err != nil {
return err
}
return nil
} }
func setFilter(filterName string) error { func setFilter(filterName string) error {

@ -2,8 +2,9 @@ package lfs
import ( import (
"encoding/json" "encoding/json"
"github.com/rubyist/tracerx"
"os/exec" "os/exec"
"github.com/rubyist/tracerx"
) )
type sshAuthResponse struct { type sshAuthResponse struct {

@ -2,9 +2,10 @@ package lfs
import ( import (
"fmt" "fmt"
"github.com/cheggaaa/pb"
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/cheggaaa/pb"
) )
type Transferable interface { type Transferable interface {

@ -80,9 +80,9 @@ func CopyCallbackFile(event, filename string, index, totalFiles int) (CopyCallba
} }
func wrapProgressError(err error, event, filename string) error { func wrapProgressError(err error, event, filename string) error {
if err == nil { if err != nil {
return nil return fmt.Errorf("Error writing Git LFS %s progress to %s: %s", event, filename, err.Error())
} }
return fmt.Errorf("Error writing Git LFS %s progress to %s: %s", event, filename, err.Error()) return nil
} }

@ -2,9 +2,10 @@ package lfs
import ( import (
"bytes" "bytes"
"github.com/bmizerany/assert"
"io/ioutil" "io/ioutil"
"testing" "testing"
"github.com/bmizerany/assert"
) )
func TestWriterWithCallback(t *testing.T) { func TestWriterWithCallback(t *testing.T) {

@ -4,11 +4,13 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"github.com/github/git-lfs/lfs" "log"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/github/git-lfs/lfs"
) )
var ( var (
@ -61,27 +63,23 @@ func mainBuild() {
if !errored { if !errored {
by, err := json.Marshal(buildMatrix) by, err := json.Marshal(buildMatrix)
if err != nil { if err != nil {
fmt.Println("Error encoding build matrix to json:", err) log.Fatalln("Error encoding build matrix to json:", err)
os.Exit(1)
} }
file, err := os.Create("bin/releases/build_matrix.json") file, err := os.Create("bin/releases/build_matrix.json")
if err != nil { if err != nil {
fmt.Println("Error creating build_matrix.json:", err) log.Fatalln("Error creating build_matrix.json:", err)
os.Exit(1)
} }
written, err := file.Write(by) written, err := file.Write(by)
file.Close() file.Close()
if err != nil { if err != nil {
fmt.Println("Error writing build_matrix.json", err) log.Fatalln("Error writing build_matrix.json", err)
os.Exit(1)
} }
if jsonSize := len(by); written != jsonSize { if jsonSize := len(by); written != jsonSize {
fmt.Printf("Expected to write %d bytes, actually wrote %d.\n", jsonSize, written) log.Fatalf("Expected to write %d bytes, actually wrote %d.\n", jsonSize, written)
os.Exit(1)
} }
} }
} }
@ -109,13 +107,13 @@ func build(buildos, buildarch string, buildMatrix map[string]Release) error {
if addenv { if addenv {
err := os.MkdirAll(dir, 0755) err := os.MkdirAll(dir, 0755)
if err != nil { if err != nil {
fmt.Println("Error setting up installer:\n", err.Error()) log.Println("Error setting up installer:\n", err.Error())
return err return err
} }
err = setupInstaller(buildos, buildarch, dir, buildMatrix) err = setupInstaller(buildos, buildarch, dir, buildMatrix)
if err != nil { if err != nil {
fmt.Println("Error setting up installer:\n", err.Error()) log.Println("Error setting up installer:\n", err.Error())
return err return err
} }
} }

29
script/centos-build Executable file

@ -0,0 +1,29 @@
#!/bin/bash -eu
#
# This script works with CentOS 6 or 7
# The CentOS 5 kernel is too old for go's liking.
trap 'echo FAIL' ERR
if grep -q ' 6' /etc/redhat-release; then
rpm -q epel-release || rpm -Uvh http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
fi
yum install -y bison git golang make man
cd /tmp
[ -d git-lfs ] || git clone https://github.com/github/git-lfs
cd git-lfs
./script/bootstrap
install -D bin/git-lfs /usr/local/bin
git lfs init
# I don't know how to install ruby2.0 on CentOS6 yet
if grep -q ' 7' /etc/redhat-release; then
yum install ruby ruby-devel
gem install ronn
./script/man
install -D man/*.1 /usr/local/share/man/man1
git help lfs > /dev/null
fi
echo SUCCESS

@ -1,6 +1,8 @@
#!/bin/sh #!/bin/sh
set -e
git config user.name || git config --global user.name "Git LFS Tests" git config user.name || git config --global user.name "Git LFS Tests"
git config user.email || git config --global user.email "git-lfs@example.com" git config user.email || git config --global user.email "git-lfs@example.com"
script/test script/test
script/integration

49
script/debian-build Executable file

@ -0,0 +1,49 @@
#!/bin/bash -eux
#
# This script works with Debian 8 and Ubuntu 14.04 (LTS)
# Go seems to build poorly on Ubuntu 12.04 (LTS)
trap 'echo FAIL' ERR
apt-get -y update
apt-get -y install binutils bison curl gcc git golang-go make
go_version=$(go version | sed 's/go version go//; s/ .*//')
if [[ $go_version < 1.3.1 ]]; then
[[ -r ~/.gvm/scripts/gvm ]] ||
bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
set +u
source ~/.gvm/scripts/gvm
gvm install go1.4.2
gvm use go1.4.2
set -u
fi
cd /tmp
[ -d git-lfs ] || git clone https://github.com/github/git-lfs
cd git-lfs
./script/bootstrap
install -D bin/git-lfs /usr/local/bin
git lfs init
apt-get -y install build-essential groff man
if grep -q Debian /etc/issue; then
apt-get install -y ruby ruby-dev
elif grep -q Ubuntu /etc/issue; then
apt-get -y install ruby2.0 ruby2.0-dev
rm /usr/bin/ruby && sudo ln -s /usr/bin/ruby2.0 /usr/bin/ruby
rm -fr /usr/bin/gem && sudo ln -s /usr/bin/gem2.0 /usr/bin/gem
else
{
echo "unknown Debian release"
cat /etc/issue
} >&2
exit 1
fi
gem install ronn
./script/man
install -d /usr/local/share/man/man1
install man/*.1 /usr/local/share/man/man1
git help lfs > /dev/null
echo SUCCESS

@ -1,10 +1,15 @@
#!/bin/bash #!/bin/bash
formatter=gofmt
hash goimports 2>/dev/null && {
formatter=goimports
}
# don't run gofmt in these directories # don't run gofmt in these directories
ignored=(/bin/ /docs/ /log/ /man/ /tmp/ .vendor) ignored=(/bin/ /docs/ /log/ /man/ /tmp/ .vendor)
for i in */ ; do for i in */ ; do
if [[ ! ${ignored[*]} =~ "/$i" ]]; then if [[ ! ${ignored[*]} =~ "/$i" ]]; then
gofmt -w -l "$@" "${i%?}" $formatter -w -l "$@" "${i%?}"
fi fi
done done

@ -1,16 +1,17 @@
#!/bin/sh -eu
prefix="/usr/local" prefix="/usr/local"
if [ "$PREFIX" != "" ] ; then if [ "${PREFIX:-}" != "" ] ; then
prefix=$PREFIX prefix=${PREFIX:-}
elif [ "$BOXEN_HOME" != "" ] ; then elif [ "${BOXEN_HOME:-}" != "" ] ; then
prefix=$BOXEN_HOME prefix=${BOXEN_HOME:-}
fi fi
mkdir -p $prefix/bin
rm -rf $prefix/bin/git-lfs* rm -rf $prefix/bin/git-lfs*
for g in git*; do for g in git*; do
cp $g "$prefix/bin/$g" install -D $g "$prefix/bin/$g"
done done
PATH+=:$prefix/bin
git lfs init git lfs init

46
script/integration Executable file

@ -0,0 +1,46 @@
#!/bin/sh
. "test/testenv.sh"
set -e
SHUTDOWN_LFS=no
SHOW_LOGS=yes
atexit() {
res=${1:-$?}
SHUTDOWN_LFS=yes
if [ "$res" = "0" ]; then
SHOW_LOGS=no
fi
if [ "$SHOW_LOGS" = "yes" ]; then
if [ -s "$REMOTEDIR/gitserver.log" ]; then
echo ""
echo "gitserver.log:"
cat "$REMOTEDIR/gitserver.log"
fi
echo ""
echo "env:"
env
fi
shutdown
exit $res
}
trap "atexit" EXIT
if [ -s "$LFS_URL_FILE" ]; then
SHOW_LOGS=no
echo "$LFS_URL_FILE still exists!"
echo "Confirm other tests are done, and run:"
echo " $ curl $(cat "$LFS_URL_FILE")/shutdown"
exit 1
fi
setup
for file in test/test-*.sh; do
echo "0$(cat .$(basename $file).time 2>/dev/null || true) $file"
done | sort -rnk1 | awk '{ print $2 }' | xargs -I % -P 4 -n 1 /bin/sh -c % --batch

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"log"
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
@ -17,24 +18,21 @@ var (
func mainRelease() { func mainRelease() {
if *ReleaseId < 1 { if *ReleaseId < 1 {
fmt.Println("Need a valid github/git-lfs release id.") log.Println("Need a valid github/git-lfs release id.")
fmt.Println("usage: script/release -id") log.Fatalln("usage: script/release -id")
os.Exit(1)
} }
file, err := os.Open("bin/releases/build_matrix.json") file, err := os.Open("bin/releases/build_matrix.json")
if err != nil { if err != nil {
fmt.Println("Error opening build_matrix.json:", err) log.Println("Error opening build_matrix.json:", err)
fmt.Println("Ensure `script/bootstrap -all` has completed successfully") log.Fatalln("Ensure `script/bootstrap -all` has completed successfully")
os.Exit(1)
} }
defer file.Close() defer file.Close()
buildMatrix := make(map[string]Release) buildMatrix := make(map[string]Release)
if err := json.NewDecoder(file).Decode(&buildMatrix); err != nil { if err := json.NewDecoder(file).Decode(&buildMatrix); err != nil {
fmt.Println("Error reading build_matrix.json:", err) log.Fatalln("Error reading build_matrix.json:", err)
os.Exit(1)
} }
for _, rel := range buildMatrix { for _, rel := range buildMatrix {
@ -62,8 +60,7 @@ func release(rel Release) {
by, err := cmd.Output() by, err := cmd.Output()
if err != nil { if err != nil {
fmt.Println("Error running curl:", err) log.Fatalln("Error running curl:", err)
os.Exit(1)
} }
fmt.Println(string(by)) fmt.Println(string(by))

@ -2,8 +2,7 @@ package main
import ( import (
"flag" "flag"
"fmt" "log"
"os"
) )
type Release struct { type Release struct {
@ -21,7 +20,6 @@ func main() {
case "release": case "release":
mainRelease() mainRelease()
default: default:
fmt.Println("Unknown command:", *SubCommand) log.Fatalln("Unknown command:", *SubCommand)
os.Exit(1)
} }
} }

@ -11,7 +11,7 @@ rm -f $LOCALSRCDIR
ln -s `pwd` $LOCALSRCDIR ln -s `pwd` $LOCALSRCDIR
GOPATH="`pwd`/.vendor" GOPATH="`pwd`/.vendor"
SUITE="./${1:-"..."}" SUITE="./${1:-"lfs ./commands"}"
if [ $# -gt 0 ]; then if [ $# -gt 0 ]; then
shift shift
fi fi

123
test/README.md Normal file

@ -0,0 +1,123 @@
# Git LFS Tests
Git LFS uses two form of tests: unit tests for the internals written in Go, and
integration tests that run `git` and `git-lfs` in a real shell environment.
You can run them separately:
```
$ script/test # Tests the Go packages.
$ script/integration # Tests the commands in shell scripts.
```
CI servers should always run both:
```
$ script/cibuild
```
## Internal Package Tests
The internal tests use Go's builtin [testing][t] package.
You can run individual tests by passing arguments to `script/test`:
```
# test a specific Go package
$ script/test lfs
# pass other `go test` arguments
$ script/test lfs -run TestSuccessStatus -v
github.com/kr/text
github.com/cheggaaa/pb
github.com/rubyist/tracerx
github.com/technoweenie/go-contentaddressable
github.com/kr/pretty
github.com/github/git-lfs/git
github.com/bmizerany/assert
=== RUN TestSuccessStatus
--- PASS: TestSuccessStatus (0.00 seconds)
PASS
ok _/Users/rick/github/git-lfs/lfs 0.011s
```
[t]: http://golang.org/pkg/testing/
## Integration Tests
Git LFS integration tests are shell scripts that test the `git-lfs` command from
the shell. Each test file can be run individually, or in parallel through
`script/integration`. Some tests will change the `pwd`, so it's important that
they run in separate OS processes.
```
$ test/test-happy-path.sh
compile git-lfs for test/test-happy-path.sh
LFSTEST_URL=/Users/rick/github/git-lfs/test/remote/url LFSTEST_DIR=/Users/rick/github/git-lfs/test/remote lfstest-gitserver
test: happy path ... OK
```
1. The integration tests should not rely on global system or git config.
2. The tests should be cross platform (Linux, Mac, Windows).
3. Tests should bootstrap an isolated, clean environment. See the Test Suite
section.
4. Successful test runs should have minimal output.
5. Failing test runs should dump enough information to diagnose the bug. This
includes stdout, stderr, any log files, and even the OS environment.
### Test Suite
The `testenv.sh` script includes some global variables used in tests. This
should be automatically included in every `test/test-*.sh` script and
`script/integration`.
`testhelpers.sh` defines some shell functions. Most are only used in the test
scripts themselves. `script/integration` uses the `setup()` and `shutdown()`
functions.
`testlib.sh` is a [fork of a lightweight shell testing lib][testlib] that is
used internally at GitHub. Only the `test/test-*.sh` scripts should include
this.
Tests live in this `./test` directory, and must have a unique name like:
`test-{name}.sh`. All tests should start with a basic template. See
`test/test-happy-path.sh` for an example.
```
#!/bin/sh
. "test/testlib.sh"
begin_test "template"
(
set -e
echo "your tests go here"
)
end_test
```
The `set -e` command will bail on the test at the first command that returns a
non zero exit status. Use simple shell commands like `grep` as assertions.
The test suite has standard `setup` and `shutdown` functions that should be
run only once. If a test script is run by `script/integration`, it will skip
the functions. Setup does the following:
* Resets temporary test directories.
* Compiles git-lfs with the latest code changes.
* Compiles Go files in `test/cmd` to `bin`, and adds them the PATH.
* Spins up a test Git and Git LFS server so the entire push/pull flow can be
exercised.
* Sets up a git credential helper that always returns a set username and
password.
The test Git server writes a `test/remote/url` file when it's complete. This
file is how individual test scripts detect if `script/integration` is being
run. You can fake this by manually spinning up the Git server using the
`lfstest-gitserver` line that is output after Git LFS is compiled.
By default, temporary directories in `tmp` and the `test/remote` directory are
cleared after test runs. Send the "KEEPTRASH" if you want to keep these files
around for debugging failed tests.
[testlib]: https://gist3.github.com/rtomayko/3877539

@ -0,0 +1,77 @@
package main
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)
var (
commands = map[string]func(){
"get": fill,
"store": noop,
"erase": noop,
}
delim = '\n'
hostRE = regexp.MustCompile(`\A127.0.0.1:\d+\z`)
)
func main() {
if argsize := len(os.Args); argsize != 2 {
fmt.Fprintf(os.Stderr, "wrong number of args: %d\n", argsize)
os.Exit(1)
}
arg := os.Args[1]
cmd := commands[arg]
if cmd == nil {
fmt.Fprintf(os.Stderr, "bad cmd: %s\n", arg)
os.Exit(1)
}
cmd()
}
func fill() {
scanner := bufio.NewScanner(os.Stdin)
creds := map[string]string{}
for scanner.Scan() {
line := scanner.Text()
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
fmt.Fprintf(os.Stderr, "bad line: %s\n", line)
os.Exit(1)
}
creds[parts[0]] = strings.TrimSpace(parts[1])
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "reading standard input: %v", err)
os.Exit(1)
}
if _, ok := creds["username"]; !ok {
creds["username"] = "user"
}
if _, ok := creds["password"]; !ok {
creds["password"] = "pass"
}
if host := creds["host"]; !hostRE.MatchString(host) {
fmt.Fprintf(os.Stderr, "invalid host: %s, should be '127.0.0.1:\\d+'\n", host)
os.Exit(1)
}
for key, value := range creds {
fmt.Fprintf(os.Stdout, "%s=%s\n", key, value)
}
}
func noop() {}

@ -0,0 +1,238 @@
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"net/textproto"
"os"
"os/exec"
"strings"
)
var (
repoDir string
largeObjects = make(map[string][]byte)
server *httptest.Server
)
func main() {
repoDir = os.Getenv("LFSTEST_DIR")
mux := http.NewServeMux()
server = httptest.NewServer(mux)
stopch := make(chan bool)
mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
stopch <- true
})
mux.HandleFunc("/storage/", storageHandler)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/info/lfs") {
log.Printf("git lfs %s %s\n", r.Method, r.URL)
lfsHandler(w, r)
return
}
log.Printf("git http-backend %s %s\n", r.Method, r.URL)
gitHandler(w, r)
})
urlname := os.Getenv("LFSTEST_URL")
if len(urlname) == 0 {
urlname = "lfstest-gitserver"
}
file, err := os.Create(urlname)
if err != nil {
log.Fatalln(err)
}
file.Write([]byte(server.URL))
file.Close()
log.Println(server.URL)
defer func() {
os.RemoveAll(urlname)
}()
<-stopch
log.Println("git server done")
}
type lfsObject struct {
Oid string `json:"oid,omitempty"`
Size int64 `json:"size,omitempty"`
Links map[string]lfsLink `json:"_links,omitempty"`
}
type lfsLink struct {
Href string `json:"href"`
Header map[string]string `json:"header,omitempty"`
}
// handles any requests with "{name}.server.git/info/lfs" in the path
func lfsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/vnd.git-lfs+json")
switch r.Method {
case "POST":
lfsPostHandler(w, r)
case "GET":
lfsGetHandler(w, r)
default:
w.WriteHeader(405)
}
}
func lfsPostHandler(w http.ResponseWriter, r *http.Request) {
buf := &bytes.Buffer{}
tee := io.TeeReader(r.Body, buf)
obj := &lfsObject{}
err := json.NewDecoder(tee).Decode(obj)
io.Copy(ioutil.Discard, r.Body)
r.Body.Close()
log.Println("REQUEST")
log.Println(buf.String())
log.Printf("OID: %s\n", obj.Oid)
log.Printf("Size: %d\n", obj.Size)
if err != nil {
log.Fatal(err)
}
res := &lfsObject{
Links: map[string]lfsLink{
"upload": lfsLink{
Href: server.URL + "/storage/" + obj.Oid,
},
},
}
by, err := json.Marshal(res)
if err != nil {
log.Fatal(err)
}
log.Println("RESPONSE: 202")
log.Println(string(by))
w.WriteHeader(202)
w.Write(by)
}
func lfsGetHandler(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, "/")
oid := parts[len(parts)-1]
by, ok := largeObjects[oid]
if !ok {
w.WriteHeader(404)
return
}
obj := &lfsObject{
Oid: oid,
Size: int64(len(by)),
Links: map[string]lfsLink{
"download": lfsLink{
Href: server.URL + "/storage/" + oid,
},
},
}
by, err := json.Marshal(obj)
if err != nil {
log.Fatal(err)
}
log.Println("RESPONSE: 200")
log.Println(string(by))
w.WriteHeader(200)
w.Write(by)
}
// handles any /storage/{oid} requests
func storageHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("storage %s %s\n", r.Method, r.URL)
switch r.Method {
case "PUT":
hash := sha256.New()
buf := &bytes.Buffer{}
io.Copy(io.MultiWriter(hash, buf), r.Body)
oid := hex.EncodeToString(hash.Sum(nil))
if !strings.HasSuffix(r.URL.Path, "/"+oid) {
w.WriteHeader(403)
return
}
largeObjects[oid] = buf.Bytes()
case "GET":
parts := strings.Split(r.URL.Path, "/")
oid := parts[len(parts)-1]
if by, ok := largeObjects[oid]; ok {
w.Write(by)
return
}
w.WriteHeader(404)
default:
w.WriteHeader(405)
}
}
func gitHandler(w http.ResponseWriter, r *http.Request) {
defer func() {
io.Copy(ioutil.Discard, r.Body)
r.Body.Close()
}()
cmd := exec.Command("git", "http-backend")
cmd.Env = []string{
fmt.Sprintf("GIT_PROJECT_ROOT=%s", repoDir),
fmt.Sprintf("GIT_HTTP_EXPORT_ALL="),
fmt.Sprintf("PATH_INFO=%s", r.URL.Path),
fmt.Sprintf("QUERY_STRING=%s", r.URL.RawQuery),
fmt.Sprintf("REQUEST_METHOD=%s", r.Method),
fmt.Sprintf("CONTENT_TYPE=%s", r.Header.Get("Content-Type")),
}
buffer := &bytes.Buffer{}
cmd.Stdin = r.Body
cmd.Stdout = buffer
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
text := textproto.NewReader(bufio.NewReader(buffer))
code, _, _ := text.ReadCodeLine(-1)
if code != 0 {
w.WriteHeader(code)
}
headers, _ := text.ReadMIMEHeader()
head := w.Header()
for key, values := range headers {
for _, value := range values {
head.Add(key, value)
}
}
io.Copy(w, text.R)
}

71
test/test-happy-path.sh Executable file

@ -0,0 +1,71 @@
#!/bin/sh
# This is a sample Git LFS test. See test/README.md and testhelpers.sh for
# more documentation.
. "test/testlib.sh"
begin_test "happy path"
(
set -e
# This initializes a new bare git repository in test/remote.
# These remote repositories are global to every test, so keep the names
# unique.
reponame="$(basename "$0" ".sh")"
setup_remote_repo "$reponame"
# Clone the repository from the test Git server. This is empty, and will be
# used to test a "git pull" below. The repo is cloned to $TRASHDIR/clone
clone_repo "$reponame" clone
# Clone the repository again to $TRASHDIR/repo. This will be used to commit
# and push objects.
clone_repo "$reponame" repo
# This executes Git LFS from the local repo that was just cloned.
out=$($GITLFS track "*.dat" 2>&1)
echo "$out" | grep "Tracking \*.dat"
contents=$(printf "a")
contents_oid=$(printf "$contents" | shasum -a 256 | cut -f 1 -d " ")
# Regular Git commands can be used.
printf "$contents" > a.dat
git add a.dat
git add .gitattributes
out=$(git commit -m "add a.dat" 2>&1)
echo "$out" | grep "master (root-commit)"
echo "$out" | grep "2 files changed"
echo "$out" | grep "create mode 100644 a.dat"
echo "$out" | grep "create mode 100644 .gitattributes"
out=$(cat a.dat)
if [ "$out" != "a" ]; then
exit 1
fi
# This is a small shell function that runs several git commands together.
assert_pointer "master" "a.dat" "$contents_oid" 1
refute_server_object "$contents_oid"
# This pushes to the remote repository set up at the top of the test.
out=$(git push origin master 2>&1)
echo "$out" | grep "(1 of 1 files) 1 B / 1 B 100.00 %"
echo "$out" | grep "master -> master"
assert_server_object "$contents_oid" "$contents"
# change to the clone's working directory
cd ../clone
git pull 2>&1 | grep "Downloading a.dat (1 B)"
out=$(cat a.dat)
if [ "$out" != "a" ]; then
exit 1
fi
assert_pointer "master" "a.dat" "$contents_oid" 1
)
end_test

38
test/testenv.sh Normal file

@ -0,0 +1,38 @@
#!/bin/sh
# Including in script/integration and every test/test-*.sh file.
set -e
# The root directory for the git-lfs repository
ROOTDIR=$(cd $(dirname "$0")/.. && pwd)
# Where Git LFS outputs the compiled binaries
BINPATH="$ROOTDIR/bin"
# Put bin path on PATH
PATH="$BINPATH:$PATH"
# create a temporary work space
TMPDIR="$(cd $(dirname "$0")/.. && pwd)"/tmp
# This is unique to every test file, and cleared after every test run.
TRASHDIR="$TMPDIR/$(basename "$0")-$$"
# Points to the git-lfs binary compiled just for the tests
GITLFS="$BINPATH/git-lfs"
# The directory that the test Git server works from. This cleared at the
# beginning of every test run.
REMOTEDIR="$ROOTDIR/test/remote"
# This is the prefix for Git config files. See the "Test Suite" section in
# test/README.md
LFS_CONFIG="$REMOTEDIR/config"
# This file contains the URL of the test Git server. See the "Test Suite"
# section in test/README.md
LFS_URL_FILE="$REMOTEDIR/url"
mkdir -p "$TRASHDIR"
. "test/testhelpers.sh"

144
test/testhelpers.sh Normal file

@ -0,0 +1,144 @@
#!/bin/sh
# assert_pointer confirms that the pointer in the repository for $path in the
# given $ref matches the given $oid and $size.
#
# $ assert_pointer "master" "path/to/file" "some-oid" 123
assert_pointer() {
local ref=$1
local path=$2
local oid=$3
local size=$4
gitblob=$(git ls-tree -l $ref | grep $path | cut -f 3 -d " ")
actual=$(git cat-file -p $gitblob)
expected=$(pointer $oid $size)
if [ "$expected" != "$actual" ]; then
exit 1
fi
}
# no-op. check that the object does not exist in the git lfs server
refute_server_object() {
echo "refute server object: no-op"
}
# no-op. check that the object does exist in the git lfs server
assert_server_object() {
echo "assert server object: no-op"
}
# pointer returns a string Git LFS pointer file.
#
# $ pointer abc-some-oid 123
# > version ...
pointer() {
local oid=$1
local size=$2
printf "version https://git-lfs.github.com/spec/v1
oid sha256:%s
size %s
" "$oid" "$size"
}
# wait_for_file simply sleeps until a file exists.
#
# $ wait_for_file "path/to/upcoming/file"
wait_for_file() {
local filename=$1
n=0
while [ $n -lt 10 ]; do
if [ -s $filename ]; then
return 0
fi
sleep 0.5
n=`expr $n + 1`
done
return 1
}
# setup_remote_repo intializes a bare Git repository that is accessible through
# the test Git server. The `pwd` is set to the repository's directory, in case
# further commands need to be run. This server is running for every test in a
# script/integration run, so every test file should setup its own remote
# repository to avoid conflicts.
#
# $ setup_remote_repo "some-name"
#
setup_remote_repo() {
local reponame=$1
echo "set up remote git repository: $reponame"
repodir="$REMOTEDIR/$reponame.git"
mkdir -p "$repodir"
cd "$repodir"
git init --bare
git config http.receivepack true
git config receive.denyCurrentBranch ignore
# dump a simple git config file so clones use this test's Git LFS command
# and the custom credential helper. This overrides any Git config that is
# already setup on the system.
printf "[filter \"lfs\"]
required = true
smudge = %s smudge %%f
clean = %s clean %%f
[credential]
helper = %s
[remote \"origin\"]
url = %s/%s
fetch = +refs/heads/*:refs/remotes/origin/*
" "$GITLFS" "$GITLFS" lfstest "$GITSERVER" "$reponame" > "$LFS_CONFIG-$reponame"
}
# clone_repo clones a repository from the test Git server to the subdirectory
# $dir under $TRASHDIR. setup_remote_repo() needs to be run first.
clone_repo() {
cd "$TRASHDIR"
local reponame=$1
local dir=$2
echo "clone local git repository $reponame to $dir"
out=$(GIT_CONFIG="$LFS_CONFIG-$reponame" git clone "$GITSERVER/$reponame" "$dir" 2>&1)
cd "$dir"
git config credential.helper lfstest
echo "$out"
}
# setup initializes the clean, isolated environment for integration tests.
setup() {
cd "$ROOTDIR"
rm -rf "test/remote"
mkdir "test/remote"
echo "compile git-lfs for $0"
script/bootstrap
$GITLFS version
for go in test/cmd/*.go; do
go build -o "$BINPATH/$(basename $go .go)" "$go"
done
echo "LFSTEST_URL=$LFS_URL_FILE LFSTEST_DIR=$REMOTEDIR lfstest-gitserver"
LFSTEST_URL="$LFS_URL_FILE" LFSTEST_DIR="$REMOTEDIR" lfstest-gitserver > "$REMOTEDIR/gitserver.log" 2>&1 &
wait_for_file "$LFS_URL_FILE"
}
# shutdown cleans the $TRASHDIR and shuts the test Git server down.
shutdown() {
# every test/test-*.sh file should cleanup its trashdir
[ -z "$KEEPTRASH" ] && rm -rf "$TRASHDIR"
if [ "$SHUTDOWN_LFS" != "no" ]; then
# only cleanup test/remote after script/integration done OR a single
# test/test-*.sh file is run manually.
[ -z "$KEEPTRASH" ] && rm -rf "$REMOTEDIR"
if [ -s "$LFS_URL_FILE" ]; then
curl "$(cat "$LFS_URL_FILE")/shutdown"
fi
fi
}

97
test/testlib.sh Normal file

@ -0,0 +1,97 @@
#!/bin/sh
# Usage: . testlib.sh
# Simple shell command language test library.
#
# Tests must follow the basic form:
#
# begin_test "the thing"
# (
# set -e
# echo "hello"
# false
# )
# end_test
#
# When a test fails its stdout and stderr are shown.
#
# Note that tests must `set -e' within the subshell block or failed assertions
# will not cause the test to fail and the result may be misreported.
#
# Copyright (c) 2011-13 by Ryan Tomayko <http://tomayko.com>
# License: MIT
. "test/testenv.sh"
set -e
# keep track of num tests and failures
tests=0
failures=0
# this runs at process exit
atexit () {
shutdown
if [ $failures -gt 0 ]; then
exit 1
else
exit 0
fi
}
# create the trash dir
trap "atexit" EXIT
SHUTDOWN_LFS=yes
GITSERVER=undefined
# if the file exists, assume another process started it, and will clean it up
# when it's done
if [ -s $LFS_URL_FILE ]; then
SHUTDOWN_LFS=no
else
setup
fi
GITSERVER=$(cat "$LFS_URL_FILE")
cd "$TRASHDIR"
# Mark the beginning of a test. A subshell should immediately follow this
# statement.
begin_test () {
test_status=$?
[ -n "$test_description" ] && end_test $test_status
unset test_status
tests=$(( tests + 1 ))
test_description="$1"
exec 3>&1 4>&2
out="$TRASHDIR/out"
err="$TRASHDIR/err"
exec 1>"$out" 2>"$err"
# allow the subshell to exit non-zero without exiting this process
set -x +e
}
# Mark the end of a test.
end_test () {
test_status="${1:-$?}"
set +x -e
exec 1>&3 2>&4
if [ "$test_status" -eq 0 ]; then
printf "test: %-60s OK\n" "$test_description ..."
else
failures=$(( failures + 1 ))
printf "test: %-60s FAILED\n" "$test_description ..."
(
echo "-- stdout --"
sed 's/^/ /' <"$TRASHDIR/out"
echo "-- stderr --"
grep -v -e '^\+ end_test' -e '^+ set +x' <"$TRASHDIR/err" |
sed 's/^/ /'
) 1>&2
fi
unset test_description
}