Skip to content
This repository was archived by the owner on Sep 9, 2020. It is now read-only.

Commit e3f6766

Browse files
committed
internal/gps: implement Prune and the related functions
Signed-off-by: Ibrahim AshShohail <[email protected]>
1 parent 0cf7d49 commit e3f6766

File tree

5 files changed

+545
-51
lines changed

5 files changed

+545
-51
lines changed

internal/gps/prune.go

Lines changed: 343 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44

55
package gps
66

7+
import (
8+
"io/ioutil"
9+
"log"
10+
"os"
11+
"path/filepath"
12+
"sort"
13+
"strings"
14+
15+
"github.com/golang/dep/internal/fs"
16+
"github.com/pkg/errors"
17+
)
18+
719
// PruneOptions represents the pruning options used to write the dependecy tree.
820
type PruneOptions uint8
921

@@ -20,8 +32,336 @@ const (
2032
)
2133

2234
var (
23-
preservedNonGoFiles = []string{
24-
"LICENSE",
25-
"COPYING",
35+
// licenseFilePrefixes is a list of name prefixes for licesnse files.
36+
licenseFilePrefixes = []string{
37+
"license",
38+
"licence",
39+
"copying",
40+
"unlicense",
41+
"copyright",
42+
"copyleft",
43+
}
44+
// legalFileSubstrings contains substrings that are likey part of a legal
45+
// declaration file.
46+
legalFileSubstrings = []string{
47+
"legal",
48+
"notice",
49+
"disclaimer",
50+
"patent",
51+
"third-party",
52+
"thirdparty",
2653
}
2754
)
55+
56+
// Prune removes excess files from the dep tree whose root is baseDir based
57+
// on the PruneOptions passed.
58+
//
59+
// A Lock must be passed if PruneUnusedPackages is toggled on.
60+
func Prune(baseDir string, options PruneOptions, l Lock, logger *log.Logger) error {
61+
if (options & PruneNestedVendorDirs) != 0 {
62+
if err := pruneNestedVendorDirs(baseDir); err != nil {
63+
return err
64+
}
65+
}
66+
67+
if err := pruneEmptyDirs(baseDir, logger); err != nil {
68+
return errors.Wrap(err, "failed to prune empty dirs")
69+
}
70+
71+
if (options & PruneUnusedPackages) != 0 {
72+
if l == nil {
73+
return errors.New("pruning unused packages requires passing a non-nil Lock")
74+
}
75+
if err := pruneUnusedPackages(baseDir, l, logger); err != nil {
76+
return errors.Wrap(err, "failed to prune unused packages")
77+
}
78+
}
79+
80+
if (options & PruneNonGoFiles) != 0 {
81+
if err := pruneNonGoFiles(baseDir, logger); err != nil {
82+
return errors.Wrap(err, "failed to prune non-Go files")
83+
}
84+
}
85+
86+
if (options & PruneGoTestFiles) != 0 {
87+
if err := pruneGoTestFiles(baseDir, logger); err != nil {
88+
return errors.Wrap(err, "failed to prune Go test files")
89+
}
90+
}
91+
92+
// Delete all empty directories.
93+
if err := pruneEmptyDirs(baseDir, logger); err != nil {
94+
return errors.Wrap(err, "failed to prune empty dirs")
95+
}
96+
97+
return nil
98+
}
99+
100+
// pruneNestedVendorDirs deletes all nested vendor directories within baseDir.
101+
func pruneNestedVendorDirs(baseDir string) error {
102+
return filepath.Walk(baseDir, stripNestedVendorDirs(baseDir))
103+
}
104+
105+
// pruneUnusedPackages deletes unimported packages found within baseDir.
106+
// Determining whether packages are imported or not is based on the passed Lock.
107+
func pruneUnusedPackages(baseDir string, l Lock, logger *log.Logger) error {
108+
unused, err := calculateUnusedPackages(baseDir, l, logger)
109+
if err != nil {
110+
return err
111+
}
112+
113+
for _, pkg := range unused {
114+
pkgPath := filepath.Join(baseDir, pkg)
115+
116+
files, err := ioutil.ReadDir(pkgPath)
117+
if err != nil {
118+
// TODO(ibrasho) Handle this error properly.
119+
// It happens when attempting to ioutil.ReadDir a submodule.
120+
continue
121+
}
122+
123+
// Delete *.go files in the package directory.
124+
for _, file := range files {
125+
// Skip directories and files that don't have a .go suffix.
126+
if file.IsDir() || !strings.HasSuffix(file.Name(), ".go") {
127+
continue
128+
}
129+
130+
if err := os.Remove(filepath.Join(pkgPath, file.Name())); err != nil {
131+
return err
132+
}
133+
}
134+
}
135+
136+
return nil
137+
}
138+
139+
// calculateUnusedPackages generates a list of unused packages existing within
140+
// baseDir depending on the imported packages found in the passed Lock.
141+
func calculateUnusedPackages(baseDir string, l Lock, logger *log.Logger) ([]string, error) {
142+
imported := calculateImportedPackages(l)
143+
sort.Strings(imported)
144+
145+
var unused []string
146+
147+
if logger != nil {
148+
logger.Println("Calculating unused packages to prune.")
149+
logger.Println("Checking the following packages:")
150+
}
151+
152+
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
153+
if err != nil {
154+
return err
155+
}
156+
157+
// Ignore baseDir and anything that's not a directory.
158+
if path == baseDir || !info.IsDir() {
159+
return nil
160+
}
161+
162+
pkg := strings.TrimPrefix(path, baseDir+string(filepath.Separator))
163+
if logger != nil {
164+
logger.Printf(" %s", pkg)
165+
}
166+
167+
// If pkg is not a parent of an imported package, add it to the
168+
// unused list.
169+
i := sort.Search(len(imported), func(i int) bool {
170+
return pkg <= imported[i]
171+
})
172+
if i >= len(imported) || !strings.HasPrefix(imported[i], pkg) {
173+
unused = append(unused, path)
174+
}
175+
176+
return nil
177+
})
178+
179+
return unused, err
180+
}
181+
182+
// calculateImportedPackages generates a list of imported packages from
183+
// the passed Lock.
184+
func calculateImportedPackages(l Lock) []string {
185+
var imported []string
186+
187+
for _, project := range l.Projects() {
188+
projectRoot := string(project.Ident().ProjectRoot)
189+
for _, pkg := range project.Packages() {
190+
imported = append(imported, filepath.Join(projectRoot, pkg))
191+
}
192+
}
193+
return imported
194+
}
195+
196+
// pruneNonGoFiles delete all non-Go files existing within baseDir.
197+
// Files with names that are prefixed by any entry in preservedNonGoFiles
198+
// are not deleted.
199+
func pruneNonGoFiles(baseDir string, logger *log.Logger) error {
200+
files, err := calculateNonGoFiles(baseDir)
201+
if err != nil {
202+
return errors.Wrap(err, "could not prune non-Go files")
203+
}
204+
205+
if err := deleteFiles(files); err != nil {
206+
return err
207+
}
208+
209+
return nil
210+
}
211+
212+
// calculateNonGoFiles returns a list of all non-Go files within baseDir.
213+
// Files with names that are prefixed by any entry in preservedNonGoFiles
214+
// are not deleted.
215+
func calculateNonGoFiles(baseDir string) ([]string, error) {
216+
var files []string
217+
218+
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
219+
if err != nil {
220+
return err
221+
}
222+
223+
// Ignore directories.
224+
if info.IsDir() {
225+
return nil
226+
}
227+
228+
// Ignore all Go files.
229+
if strings.HasSuffix(info.Name(), ".go") {
230+
return nil
231+
}
232+
233+
if !isPreservedNonGoFile(info.Name()) {
234+
files = append(files, path)
235+
}
236+
237+
return nil
238+
})
239+
240+
return files, err
241+
}
242+
243+
// isPreservedNonGoFile checks if the file name idicates that the file should be
244+
// preserved. It assumes the file is not a Go file (doesn't have a .go suffix).
245+
func isPreservedNonGoFile(name string) bool {
246+
name = strings.ToLower(name)
247+
248+
for _, prefix := range licenseFilePrefixes {
249+
if strings.HasPrefix(name, prefix) {
250+
return true
251+
}
252+
}
253+
254+
for _, substring := range legalFileSubstrings {
255+
if strings.Contains(name, substring) {
256+
return true
257+
}
258+
}
259+
260+
return false
261+
}
262+
263+
// pruneGoTestFiles deletes all Go test files (*_test.go) within baseDirr.
264+
func pruneGoTestFiles(baseDir string, logger *log.Logger) error {
265+
files, err := calculateGoTestFiles(baseDir)
266+
if err != nil {
267+
return errors.Wrap(err, "could not prune Go test files")
268+
}
269+
270+
if err := deleteFiles(files); err != nil {
271+
return err
272+
}
273+
274+
return nil
275+
}
276+
277+
// calculateGoTestFiles walks over baseDir and returns a list of all
278+
// Go test files (any file that has the name *_test.go).
279+
func calculateGoTestFiles(baseDir string) ([]string, error) {
280+
var files []string
281+
282+
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
283+
if err != nil {
284+
return err
285+
}
286+
287+
// Ignore directories.
288+
if info.IsDir() {
289+
return nil
290+
}
291+
292+
// Ignore any files that is not a Go test file.
293+
if !strings.HasSuffix(info.Name(), "_test.go") {
294+
return nil
295+
}
296+
297+
files = append(files, path)
298+
299+
return nil
300+
})
301+
302+
return files, err
303+
}
304+
305+
// pruneEmptyDirs delete all empty directories within baseDir.
306+
func pruneEmptyDirs(baseDir string, logger *log.Logger) error {
307+
empty, err := calculateEmptyDirs(baseDir)
308+
if err != nil {
309+
return err
310+
}
311+
312+
if logger != nil {
313+
logger.Println("Deleting empty directories:")
314+
}
315+
316+
for _, dir := range empty {
317+
if logger != nil {
318+
logger.Printf(" %s\n", strings.TrimPrefix(dir, baseDir+string(os.PathSeparator)))
319+
}
320+
}
321+
for _, dir := range empty {
322+
if err := os.Remove(dir); err != nil {
323+
return err
324+
}
325+
}
326+
327+
return nil
328+
}
329+
330+
// calculateEmptyDirs walks over baseDir and returns a slice of empty directory paths.
331+
func calculateEmptyDirs(baseDir string) ([]string, error) {
332+
var empty []string
333+
334+
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
335+
if err != nil {
336+
return nil
337+
}
338+
339+
if baseDir == path {
340+
return nil
341+
}
342+
343+
if !info.IsDir() {
344+
return nil
345+
}
346+
347+
nonEmpty, err := fs.IsNonEmptyDir(path)
348+
if err != nil {
349+
return err
350+
} else if !nonEmpty {
351+
empty = append(empty, path)
352+
}
353+
354+
return nil
355+
})
356+
357+
return empty, err
358+
}
359+
360+
func deleteFiles(paths []string) error {
361+
for _, path := range paths {
362+
if err := os.Remove(path); err != nil {
363+
return err
364+
}
365+
}
366+
return nil
367+
}

0 commit comments

Comments
 (0)