83d7f76e76
Due to the complexities of handling .gitignores, rely directly on git-ls-files rather than a custom gitignore-aware directory walking routine. As a side-effect, there has been a minor behaviorial change related to locking. Previously, if a file was locked, then a matching pattern was added to a .gitignore, that file was no longer considered locked and the writeable attribute was set on that file. With this change, files which match a .gitignore, but have been addded to the repository, will still have their write bit cleared. Fixes #3797
336 lines
8.5 KiB
Go
336 lines
8.5 KiB
Go
package tools
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestCleanPathsCleansPaths(t *testing.T) {
|
|
cleaned := CleanPaths("/foo/bar/,/foo/bar/baz", ",")
|
|
assert.Equal(t, []string{"/foo/bar", "/foo/bar/baz"}, cleaned)
|
|
}
|
|
|
|
func TestCleanPathsReturnsNoResultsWhenGivenNoPaths(t *testing.T) {
|
|
cleaned := CleanPaths("", ",")
|
|
assert.Empty(t, cleaned)
|
|
}
|
|
|
|
type ExpandPathTestCase struct {
|
|
Path string
|
|
Expand bool
|
|
|
|
Want string
|
|
WantErr string
|
|
|
|
currentUser func() (*user.User, error)
|
|
lookupUser func(who string) (*user.User, error)
|
|
}
|
|
|
|
func (c *ExpandPathTestCase) Assert(t *testing.T) {
|
|
if c.currentUser != nil {
|
|
oldCurrentUser := currentUser
|
|
currentUser = c.currentUser
|
|
defer func() { currentUser = oldCurrentUser }()
|
|
}
|
|
|
|
if c.lookupUser != nil {
|
|
oldLookupUser := lookupUser
|
|
lookupUser = c.lookupUser
|
|
defer func() { lookupUser = oldLookupUser }()
|
|
}
|
|
|
|
got, err := ExpandPath(c.Path, c.Expand)
|
|
if err != nil || len(c.WantErr) > 0 {
|
|
assert.EqualError(t, err, c.WantErr)
|
|
}
|
|
assert.Equal(t, filepath.ToSlash(c.Want), filepath.ToSlash(got))
|
|
}
|
|
|
|
func TestExpandPath(t *testing.T) {
|
|
for desc, c := range map[string]*ExpandPathTestCase{
|
|
"no expand": {
|
|
Path: "/path/to/hooks",
|
|
Want: "/path/to/hooks",
|
|
},
|
|
"current": {
|
|
Path: "~/path/to/hooks",
|
|
Want: "/home/jane/path/to/hooks",
|
|
currentUser: func() (*user.User, error) {
|
|
return &user.User{
|
|
HomeDir: "/home/jane",
|
|
}, nil
|
|
},
|
|
},
|
|
"current, slash": {
|
|
Path: "~/",
|
|
Want: "/home/jane",
|
|
currentUser: func() (*user.User, error) {
|
|
return &user.User{
|
|
HomeDir: "/home/jane",
|
|
}, nil
|
|
},
|
|
},
|
|
"current, no slash": {
|
|
Path: "~",
|
|
Want: "/home/jane",
|
|
currentUser: func() (*user.User, error) {
|
|
return &user.User{
|
|
HomeDir: "/home/jane",
|
|
}, nil
|
|
},
|
|
},
|
|
"non-current": {
|
|
Path: "~other/path/to/hooks",
|
|
Want: "/home/special/path/to/hooks",
|
|
lookupUser: func(who string) (*user.User, error) {
|
|
assert.Equal(t, "other", who)
|
|
return &user.User{
|
|
HomeDir: "/home/special",
|
|
}, nil
|
|
},
|
|
},
|
|
"non-current, no slash": {
|
|
Path: "~other",
|
|
Want: "/home/special",
|
|
lookupUser: func(who string) (*user.User, error) {
|
|
assert.Equal(t, "other", who)
|
|
return &user.User{
|
|
HomeDir: "/home/special",
|
|
}, nil
|
|
},
|
|
},
|
|
"non-current (missing)": {
|
|
Path: "~other/path/to/hooks",
|
|
WantErr: "could not find user other: missing",
|
|
lookupUser: func(who string) (*user.User, error) {
|
|
assert.Equal(t, "other", who)
|
|
return nil, fmt.Errorf("missing")
|
|
},
|
|
},
|
|
} {
|
|
t.Run(desc, c.Assert)
|
|
}
|
|
}
|
|
|
|
type ExpandConfigPathTestCase struct {
|
|
Path string
|
|
DefaultPath string
|
|
|
|
Want string
|
|
WantErr string
|
|
|
|
currentUser func() (*user.User, error)
|
|
lookupConfigHome func() string
|
|
}
|
|
|
|
func (c *ExpandConfigPathTestCase) Assert(t *testing.T) {
|
|
if c.currentUser != nil {
|
|
oldCurrentUser := currentUser
|
|
currentUser = c.currentUser
|
|
defer func() { currentUser = oldCurrentUser }()
|
|
}
|
|
|
|
if c.lookupConfigHome != nil {
|
|
oldLookupConfigHome := lookupConfigHome
|
|
lookupConfigHome = c.lookupConfigHome
|
|
defer func() { lookupConfigHome = oldLookupConfigHome }()
|
|
}
|
|
|
|
got, err := ExpandConfigPath(c.Path, c.DefaultPath)
|
|
if err != nil || len(c.WantErr) > 0 {
|
|
assert.EqualError(t, err, c.WantErr)
|
|
}
|
|
assert.Equal(t, filepath.ToSlash(c.Want), filepath.ToSlash(got))
|
|
}
|
|
|
|
func TestExpandConfigPath(t *testing.T) {
|
|
for desc, c := range map[string]*ExpandConfigPathTestCase{
|
|
"unexpanded full path": {
|
|
Path: "/path/to/attributes",
|
|
Want: "/path/to/attributes",
|
|
},
|
|
"expanded full path": {
|
|
Path: "~/path/to/attributes",
|
|
Want: "/home/pat/path/to/attributes",
|
|
currentUser: func() (*user.User, error) {
|
|
return &user.User{
|
|
HomeDir: "/home/pat",
|
|
}, nil
|
|
},
|
|
},
|
|
"expanded default path": {
|
|
DefaultPath: "git/attributes",
|
|
Want: "/home/pat/.config/git/attributes",
|
|
currentUser: func() (*user.User, error) {
|
|
return &user.User{
|
|
HomeDir: "/home/pat",
|
|
}, nil
|
|
},
|
|
},
|
|
"XDG_CONFIG_HOME set": {
|
|
DefaultPath: "git/attributes",
|
|
Want: "/home/pat/configpath/git/attributes",
|
|
lookupConfigHome: func() string {
|
|
return "/home/pat/configpath"
|
|
},
|
|
},
|
|
} {
|
|
t.Run(desc, c.Assert)
|
|
}
|
|
}
|
|
|
|
func TestFastWalkBasic(t *testing.T) {
|
|
rootDir, err := ioutil.TempDir(os.TempDir(), "GitLfsTestFastWalkBasic")
|
|
if err != nil {
|
|
assert.FailNow(t, "Unable to get temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(rootDir)
|
|
os.Chdir(rootDir)
|
|
|
|
expectedEntries := createFastWalkInputData(10, 160)
|
|
|
|
walker := fastWalkWithExcludeFiles(expectedEntries[0])
|
|
gotEntries, gotErrors := collectFastWalkResults(walker.ch)
|
|
|
|
assert.Empty(t, gotErrors)
|
|
|
|
sort.Strings(expectedEntries)
|
|
sort.Strings(gotEntries)
|
|
assert.Equal(t, expectedEntries, gotEntries)
|
|
}
|
|
|
|
// Make test data - ensure you've Chdir'ed into a temp dir first
|
|
// Returns list of files/dirs that are created
|
|
// First entry is the parent dir of all others
|
|
func createFastWalkInputData(smallFolder, largeFolder int) []string {
|
|
dirs := []string{
|
|
"testroot",
|
|
"testroot/folder1",
|
|
"testroot/folder2",
|
|
"testroot/folder2/subfolder1",
|
|
"testroot/folder2/subfolder2",
|
|
"testroot/folder2/subfolder3",
|
|
"testroot/folder2/subfolder4",
|
|
"testroot/folder2/subfolder4/subsub",
|
|
}
|
|
expectedEntries := make([]string, 0, 250)
|
|
|
|
for i, dir := range dirs {
|
|
os.MkdirAll(dir, 0755)
|
|
numFiles := smallFolder
|
|
expectedEntries = append(expectedEntries, dir)
|
|
if i >= 3 && i <= 5 {
|
|
// Bulk test to ensure works with > 1 batch
|
|
numFiles = largeFolder
|
|
}
|
|
for f := 0; f < numFiles; f++ {
|
|
filename := join(dir, fmt.Sprintf("file%d.txt", f))
|
|
ioutil.WriteFile(filename, []byte("TEST"), 0644)
|
|
expectedEntries = append(expectedEntries, filename)
|
|
}
|
|
}
|
|
|
|
return expectedEntries
|
|
}
|
|
|
|
func collectFastWalkResults(fchan <-chan fastWalkInfo) ([]string, []error) {
|
|
gotEntries := make([]string, 0, 1000)
|
|
gotErrors := make([]error, 0, 5)
|
|
for o := range fchan {
|
|
if o.Err != nil {
|
|
gotErrors = append(gotErrors, o.Err)
|
|
} else {
|
|
if len(o.ParentDir) == 0 {
|
|
gotEntries = append(gotEntries, o.Info.Name())
|
|
} else {
|
|
gotEntries = append(gotEntries, join(o.ParentDir, o.Info.Name()))
|
|
}
|
|
}
|
|
}
|
|
|
|
return gotEntries, gotErrors
|
|
}
|
|
|
|
func getFileMode(filename string) os.FileMode {
|
|
s, err := os.Stat(filename)
|
|
if err != nil {
|
|
return 0000
|
|
}
|
|
return s.Mode()
|
|
}
|
|
|
|
// uniq creates an element-wise copy of "xs" containing only unique elements in
|
|
// the same order.
|
|
func uniq(xs []string) []string {
|
|
seen := make(map[string]struct{})
|
|
uniq := make([]string, 0, len(xs))
|
|
|
|
for _, x := range xs {
|
|
if _, ok := seen[x]; !ok {
|
|
seen[x] = struct{}{}
|
|
uniq = append(uniq, x)
|
|
}
|
|
}
|
|
|
|
return uniq
|
|
}
|
|
|
|
func TestSetWriteFlag(t *testing.T) {
|
|
f, err := ioutil.TempFile("", "lfstestwriteflag")
|
|
assert.Nil(t, err)
|
|
filename := f.Name()
|
|
defer os.Remove(filename)
|
|
f.Close()
|
|
// Set up with read/write bit for all but no execute
|
|
assert.Nil(t, os.Chmod(filename, 0666))
|
|
|
|
assert.Nil(t, SetFileWriteFlag(filename, false))
|
|
// should turn off all write
|
|
assert.EqualValues(t, 0444, getFileMode(filename))
|
|
assert.Nil(t, SetFileWriteFlag(filename, true))
|
|
// should only add back user write (on Mac/Linux)
|
|
if runtime.GOOS == "windows" {
|
|
assert.EqualValues(t, 0666, getFileMode(filename))
|
|
} else {
|
|
assert.EqualValues(t, 0644, getFileMode(filename))
|
|
}
|
|
|
|
// Can't run selective UGO tests on Windows as doesn't support separate roles
|
|
// Also Golang only supports read/write but not execute on Windows
|
|
if runtime.GOOS != "windows" {
|
|
// Set up with read/write/execute bit for all but no execute
|
|
assert.Nil(t, os.Chmod(filename, 0777))
|
|
assert.Nil(t, SetFileWriteFlag(filename, false))
|
|
// should turn off all write but not execute
|
|
assert.EqualValues(t, 0555, getFileMode(filename))
|
|
assert.Nil(t, SetFileWriteFlag(filename, true))
|
|
// should only add back user write (on Mac/Linux)
|
|
if runtime.GOOS == "windows" {
|
|
assert.EqualValues(t, 0777, getFileMode(filename))
|
|
} else {
|
|
assert.EqualValues(t, 0755, getFileMode(filename))
|
|
}
|
|
|
|
assert.Nil(t, os.Chmod(filename, 0440))
|
|
assert.Nil(t, SetFileWriteFlag(filename, false))
|
|
assert.EqualValues(t, 0440, getFileMode(filename))
|
|
assert.Nil(t, SetFileWriteFlag(filename, true))
|
|
// should only add back user write
|
|
assert.EqualValues(t, 0640, getFileMode(filename))
|
|
}
|
|
}
|
|
|
|
func TestExecutablePermissions(t *testing.T) {
|
|
assert.EqualValues(t, os.FileMode(0755), ExecutablePermissions(0644))
|
|
assert.EqualValues(t, os.FileMode(0750), ExecutablePermissions(0640))
|
|
assert.EqualValues(t, os.FileMode(0700), ExecutablePermissions(0600))
|
|
}
|