Skip to content

Commit d542f07

Browse files
author
Seamus Connor
committed
Use git-ls-files to enumerate repo contents.
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
1 parent b6e2779 commit d542f07

File tree

9 files changed

+139
-290
lines changed

9 files changed

+139
-290
lines changed

fs/cleanup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func (f *Filesystem) cleanupTmp() error {
1717
}
1818

1919
var walkErr error
20-
tools.FastWalkGitRepo(tmpdir, func(parentDir string, info os.FileInfo, err error) {
20+
tools.FastWalkDir(tmpdir, func(parentDir string, info os.FileInfo, err error) {
2121
if err != nil {
2222
walkErr = err
2323
}

fs/fs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type Filesystem struct {
4545

4646
func (f *Filesystem) EachObject(fn func(Object) error) error {
4747
var eachErr error
48-
tools.FastWalkGitRepo(f.LFSObjectDir(), func(parentDir string, info os.FileInfo, err error) {
48+
tools.FastWalkDir(f.LFSObjectDir(), func(parentDir string, info os.FileInfo, err error) {
4949
if err != nil {
5050
eachErr = err
5151
return

git/attribs.go

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package git
33
import (
44
"os"
55
"path/filepath"
6+
"sort"
67

78
"github.com/git-lfs/git-lfs/filepathfilter"
89
"github.com/git-lfs/git-lfs/git/gitattr"
@@ -172,27 +173,27 @@ func findAttributeFiles(workingDir, gitDir string) []attrFile {
172173
paths = append(paths, attrFile{path: repoAttributes, readMacros: true})
173174
}
174175

175-
tools.FastWalkGitRepo(workingDir, func(parentDir string, info os.FileInfo, err error) {
176-
if err != nil {
177-
tracerx.Printf("Error finding .gitattributes: %v", err)
178-
return
179-
}
176+
lsFiles, err := NewLsFiles(workingDir, true)
177+
if err != nil {
178+
tracerx.Printf("Error finding .gitattributes: %v", err)
179+
return paths
180+
}
180181

181-
if info.IsDir() || info.Name() != ".gitattributes" {
182-
return
182+
if gitattributesFiles, present := lsFiles.FilesByName[".gitattributes"]; present {
183+
for _, f := range gitattributesFiles {
184+
tracerx.Printf("findAttributeFiles: located %s", f.FullPath)
185+
paths = append(paths, attrFile{
186+
path: filepath.Join(workingDir, f.FullPath),
187+
readMacros: f.FullPath == ".gitattributes", // Read macros from the top-level attributes
188+
})
183189
}
184-
185-
paths = append(paths, attrFile{
186-
path: filepath.Join(parentDir, info.Name()),
187-
readMacros: parentDir == workingDir,
188-
})
189-
})
190+
}
190191

191192
// reverse the order of the files so more specific entries are found first
192193
// when iterating from the front (respects precedence)
193-
for i, j := 0, len(paths)-1; i < j; i, j = i+1, j-1 {
194-
paths[i], paths[j] = paths[j], paths[i]
195-
}
194+
sort.Slice(paths[:], func(i, j int) bool {
195+
return len(paths[i].path) > len(paths[j].path)
196+
})
196197

197198
return paths
198199
}

git/ls_files.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package git
2+
3+
import (
4+
"bufio"
5+
"strings"
6+
"path"
7+
"io/ioutil"
8+
9+
"github.com/git-lfs/git-lfs/errors"
10+
"github.com/git-lfs/git-lfs/tools"
11+
"github.com/rubyist/tracerx"
12+
)
13+
14+
type lsFileInfo struct {
15+
BaseName string
16+
FullPath string
17+
}
18+
type LsFiles struct {
19+
Files map[string]*lsFileInfo
20+
FilesByName map[string][]*lsFileInfo
21+
}
22+
23+
func NewLsFiles(workingDir string, standardExclude bool) (*LsFiles, error) {
24+
25+
args := []string{
26+
"ls-files",
27+
"-z", // Use a NUL separator. This also disables the escaping of special characters.
28+
"--others",
29+
"--cached",
30+
}
31+
32+
if standardExclude {
33+
args = append(args, "--exclude-standard")
34+
}
35+
cmd := gitNoLFS(args...)
36+
cmd.Dir = workingDir
37+
38+
tracerx.Printf("NewLsFiles: running in %s git %s",
39+
workingDir, strings.Join(args, " "))
40+
41+
// Capture stdout and stderr
42+
stdout, err := cmd.StdoutPipe()
43+
if err != nil {
44+
return nil, err
45+
}
46+
stderr, err := cmd.StderrPipe()
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
scanner := bufio.NewScanner(stdout)
52+
scanner.Split(tools.SplitOnNul)
53+
54+
if err := cmd.Start(); err != nil {
55+
return nil, err
56+
}
57+
58+
rv := &LsFiles {
59+
Files: make(map[string]*lsFileInfo),
60+
FilesByName: make(map[string][]*lsFileInfo),
61+
}
62+
63+
// Setup a goroutine to drain stderr as large amounts of error output may cause
64+
// the subprocess to block.
65+
errorMessages := make(chan []byte)
66+
go func() {
67+
msg, _ := ioutil.ReadAll(stderr)
68+
errorMessages <- msg
69+
}()
70+
71+
// Read all files
72+
for scanner.Scan() {
73+
base := path.Base(scanner.Text())
74+
finfo := &lsFileInfo {
75+
BaseName: base,
76+
FullPath: scanner.Text(),
77+
}
78+
rv.Files[scanner.Text()] = finfo
79+
rv.FilesByName[base] = append(rv.FilesByName[base], finfo)
80+
}
81+
82+
// Check the output of the subprocess, output stderr if the command failed.
83+
msg := <-errorMessages
84+
if err := cmd.Wait(); err != nil {
85+
return nil, errors.Errorf("Error in git %s: %v %s",
86+
strings.Join(args, " "), err, msg)
87+
}
88+
89+
return rv, nil
90+
}

locking/lockable.go

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"os"
66
"path/filepath"
77
"strings"
8-
"sync"
98

109
"github.com/git-lfs/git-lfs/errors"
1110
"github.com/git-lfs/git-lfs/filepathfilter"
@@ -113,46 +112,20 @@ func (c *Client) FixFileWriteFlagsInDir(dir string, lockablePatterns, unlockable
113112
// Internal implementation of fixing file write flags with precompiled filters
114113
func (c *Client) fixFileWriteFlags(absPath, workingDir string, lockable, unlockable *filepathfilter.Filter) error {
115114

116-
var errs []error
117-
var errMux sync.Mutex
118-
119-
addErr := func(err error) {
120-
errMux.Lock()
121-
defer errMux.Unlock()
122-
123-
errs = append(errs, err)
124-
}
125-
126-
recursor := tools.FastWalkGitRepo
127-
if c.ModifyIgnoredFiles {
128-
recursor = tools.FastWalkGitRepoAll
115+
// Build a list of files
116+
lsFiles, err := git.NewLsFiles(workingDir, !c.ModifyIgnoredFiles)
117+
if err != nil {
118+
return err
129119
}
130120

131-
recursor(absPath, func(parentDir string, fi os.FileInfo, err error) {
132-
if err != nil {
133-
addErr(err)
134-
return
135-
}
136-
// Skip dirs, we only need to check files
137-
if fi.IsDir() {
138-
return
139-
}
140-
abschild := filepath.Join(parentDir, fi.Name())
141-
142-
// This is a file, get relative to repo root
143-
relpath, err := filepath.Rel(workingDir, abschild)
144-
if err != nil {
145-
addErr(err)
146-
return
147-
}
148-
149-
err = c.fixSingleFileWriteFlags(relpath, lockable, unlockable)
121+
for f := range lsFiles.Files {
122+
err = c.fixSingleFileWriteFlags(f, lockable, unlockable)
150123
if err != nil {
151-
addErr(err)
124+
return err
152125
}
126+
}
153127

154-
})
155-
return errors.Combine(errs)
128+
return nil
156129
}
157130

158131
// FixLockableFileWriteFlags checks each file in the provided list, and for

t/t-lock.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ begin_test "lock with .gitignore"
250250
git add .gitignore
251251
git commit -m ".gitignore: ignore 'a.txt'"
252252
rm -f a.txt && git checkout a.txt
253-
assert_file_writeable a.txt
253+
refute_file_writeable a.txt
254254
)
255255
end_test
256256

tools/filetools.go

Lines changed: 8 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
package tools
44

55
import (
6-
"bufio"
76
"encoding/hex"
87
"fmt"
98
"io"
@@ -239,7 +238,7 @@ func VerifyFileHash(oid, path string) error {
239238
// FastWalkCallback is the signature for the callback given to FastWalkGitRepo()
240239
type FastWalkCallback func(parentDir string, info os.FileInfo, err error)
241240

242-
// FastWalkGitRepo is a more optimal implementation of filepath.Walk for a Git
241+
// FastWalkDir is a more optimal implementation of filepath.Walk for a Git
243242
// repo. The callback guaranteed to be called sequentially. The function returns
244243
// once all files and errors have triggered callbacks.
245244
// It differs in the following ways:
@@ -248,18 +247,10 @@ type FastWalkCallback func(parentDir string, info os.FileInfo, err error)
248247
// there are no other guarantees. Use parentDir argument in the callback to
249248
// determine absolute path rather than tracking it yourself
250249
// * Automatically ignores any .git directories
251-
// * Respects .gitignore contents and skips ignored files/dirs
252250
//
253251
// rootDir - Absolute path to the top of the repository working directory
254-
func FastWalkGitRepo(rootDir string, cb FastWalkCallback) {
255-
fastWalkCallback(fastWalkWithExcludeFiles(rootDir, ".gitignore"), cb)
256-
}
257-
258-
// FastWalkGitRepoAll behaves as FastWalkGitRepo, with the additional caveat
259-
// that it does not ignore paths and directories found in .gitignore file(s)
260-
// throughout the repository.
261-
func FastWalkGitRepoAll(rootDir string, cb FastWalkCallback) {
262-
fastWalkCallback(fastWalkWithExcludeFiles(rootDir, ""), cb)
252+
func FastWalkDir(rootDir string, cb FastWalkCallback) {
253+
fastWalkCallback(fastWalkWithExcludeFiles(rootDir), cb)
263254
}
264255

265256
// fastWalkCallback calls the FastWalkCallback "cb" for all files found by the
@@ -281,19 +272,17 @@ type fastWalkInfo struct {
281272

282273
type fastWalker struct {
283274
rootDir string
284-
excludeFilename string
285275
ch chan fastWalkInfo
286276
limit int32
287277
cur *int32
288278
wg *sync.WaitGroup
289279
}
290280

291281
// fastWalkWithExcludeFiles walks the contents of a dir, respecting
292-
// include/exclude patterns and also loading new exlude patterns from files
293-
// named excludeFilename in directories walked
282+
// include/exclude patterns.
294283
//
295284
// rootDir - Absolute path to the top of the repository working directory
296-
func fastWalkWithExcludeFiles(rootDir, excludeFilename string) *fastWalker {
285+
func fastWalkWithExcludeFiles(rootDir string) *fastWalker {
297286
excludePaths := []filepathfilter.Pattern{
298287
filepathfilter.NewPattern(".git"),
299288
filepathfilter.NewPattern("**/.git"),
@@ -307,7 +296,6 @@ func fastWalkWithExcludeFiles(rootDir, excludeFilename string) *fastWalker {
307296
c := int32(0)
308297
w := &fastWalker{
309298
rootDir: rootDir,
310-
excludeFilename: excludeFilename,
311299
limit: int32(limit),
312300
cur: &c,
313301
ch: make(chan fastWalkInfo, 256),
@@ -327,12 +315,9 @@ func fastWalkWithExcludeFiles(rootDir, excludeFilename string) *fastWalker {
327315
return w
328316
}
329317

330-
// Walk is the main recursive implementation of fast walk.
331-
// Sends the file/dir and any contents to the channel so long as it passes the
332-
// include/exclude filter. If a dir, parses any excludeFilename found and updates
333-
// the excludePaths with its content before (parallel) recursing into contents
334-
// Also splits large directories into multiple goroutines.
335-
// Increments waitg.Add(1) for each new goroutine launched internally
318+
// Walk is the main recursive implementation of fast walk. Sends the file/dir
319+
// and any contents to the channel so long as it passes the include/exclude
320+
// filter. Increments waitg.Add(1) for each new goroutine launched internally
336321
//
337322
// workDir - Relative path inside the repository
338323
func (w *fastWalker) Walk(isRoot bool, workDir string, itemFi os.FileInfo,
@@ -373,15 +358,6 @@ func (w *fastWalker) Walk(isRoot bool, workDir string, itemFi os.FileInfo,
373358
childWorkDir = join(workDir, itemFi.Name())
374359
}
375360

376-
if len(w.excludeFilename) > 0 {
377-
possibleExcludeFile := join(fullPath, w.excludeFilename)
378-
var err error
379-
excludePaths, err = loadExcludeFilename(possibleExcludeFile, childWorkDir, excludePaths)
380-
if err != nil {
381-
w.ch <- fastWalkInfo{Err: err}
382-
}
383-
}
384-
385361
// The absolute optimal way to scan would be File.Readdirnames but we
386362
// still need the Stat() to know whether something is a dir, so use
387363
// File.Readdir instead. Means we can provide os.FileInfo to callers like
@@ -430,51 +406,6 @@ func (w *fastWalker) Wait() {
430406
close(w.ch)
431407
}
432408

433-
// loadExcludeFilename reads the given file in gitignore format and returns a
434-
// revised array of exclude paths if there are any changes.
435-
// If any changes are made a copy of the array is taken so the original is not
436-
// modified
437-
func loadExcludeFilename(filename, workDir string, excludePaths []filepathfilter.Pattern) ([]filepathfilter.Pattern, error) {
438-
f, err := os.OpenFile(filename, os.O_RDONLY, 0644)
439-
if err != nil {
440-
if os.IsNotExist(err) {
441-
return excludePaths, nil
442-
}
443-
return excludePaths, err
444-
}
445-
defer f.Close()
446-
447-
retPaths := excludePaths
448-
modified := false
449-
450-
scanner := bufio.NewScanner(f)
451-
for scanner.Scan() {
452-
line := strings.TrimSpace(scanner.Text())
453-
// Skip blanks, comments and negations (not supported right now)
454-
if len(line) == 0 || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "!") {
455-
continue
456-
}
457-
458-
if !modified {
459-
// copy on write
460-
retPaths = make([]filepathfilter.Pattern, len(excludePaths))
461-
copy(retPaths, excludePaths)
462-
modified = true
463-
}
464-
465-
path := line
466-
// Add pattern in context if exclude has separator, or no wildcard
467-
// Allow for both styles of separator at this point
468-
if strings.ContainsAny(path, "/\\") ||
469-
!strings.Contains(path, "*") {
470-
path = join(workDir, line)
471-
}
472-
retPaths = append(retPaths, filepathfilter.NewPattern(path))
473-
}
474-
475-
return retPaths, nil
476-
}
477-
478409
func join(paths ...string) string {
479410
ne := make([]string, 0, len(paths))
480411

0 commit comments

Comments
 (0)