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

Commit 3e69c4e

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

File tree

4 files changed

+366
-48
lines changed

4 files changed

+366
-48
lines changed

internal/gps/prune.go

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
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/pkg/errors"
16+
)
17+
718
// PruneOptions represents the pruning options used to write the dependecy tree.
819
type PruneOptions uint8
920

@@ -25,3 +36,280 @@ var (
2536
"COPYING",
2637
}
2738
)
39+
40+
// Prune removes excess files from the dep tree whose root is baseDir based
41+
// on the PruneOptions passed.
42+
//
43+
// A Lock must be passed if PruneUnusedPackages is toggled on.
44+
func Prune(baseDir string, options PruneOptions, l Lock, logger *log.Logger) error {
45+
if (options & PruneNestedVendorDirs) != 0 {
46+
if err := pruneNestedVendorDirs(baseDir); err != nil {
47+
return err
48+
}
49+
}
50+
51+
if err := pruneEmptyDirs(baseDir, logger); err != nil {
52+
return errors.Wrap(err, "failed to prune empty dirs")
53+
}
54+
55+
if (options & PruneUnusedPackages) != 0 {
56+
if l == nil {
57+
return errors.New("pruning unused packages requires passing a non-nil Lock")
58+
}
59+
if err := pruneUnusedPackages(baseDir, l, logger); err != nil {
60+
return errors.Wrap(err, "failed to prune unused packages")
61+
}
62+
}
63+
64+
if (options & PruneNonGoFiles) != 0 {
65+
if err := pruneNonGoFiles(baseDir, logger); err != nil {
66+
return errors.Wrap(err, "failed to prune non-Go files")
67+
}
68+
}
69+
70+
if (options & PruneGoTestFiles) != 0 {
71+
if err := pruneGoTestFiles(baseDir, logger); err != nil {
72+
return errors.Wrap(err, "failed to prune Go test files")
73+
}
74+
}
75+
76+
// Delete all empty directories.
77+
if err := pruneEmptyDirs(baseDir, logger); err != nil {
78+
return errors.Wrap(err, "failed to prune empty dirs")
79+
}
80+
81+
return nil
82+
}
83+
84+
func pruneNestedVendorDirs(baseDir string) error {
85+
return filepath.Walk(baseDir, stripNestedVendorDirs(baseDir))
86+
}
87+
88+
func pruneUnusedPackages(baseDir string, l Lock, logger *log.Logger) error {
89+
unused, err := calculateUnusedPackages(baseDir, l, logger)
90+
if err != nil {
91+
return err
92+
}
93+
94+
for _, pkg := range unused {
95+
pkgPath := filepath.Join(baseDir, pkg)
96+
97+
files, err := ioutil.ReadDir(pkgPath)
98+
if err != nil {
99+
// TODO(ibrasho) Handle this error properly.
100+
// It happens when attempting to ioutil.ReadDir a submodule.
101+
continue
102+
}
103+
104+
// Delete *.go files in the package directory.
105+
for _, file := range files {
106+
// Skip directories and files that don't have a .go suffix.
107+
if file.IsDir() || !strings.HasSuffix(file.Name(), ".go") {
108+
continue
109+
}
110+
111+
if err := os.Remove(filepath.Join(pkgPath, file.Name())); err != nil {
112+
return err
113+
}
114+
}
115+
}
116+
117+
return nil
118+
}
119+
120+
func calculateUnusedPackages(baseDir string, l Lock, logger *log.Logger) ([]string, error) {
121+
imported := calculateImportedPackages(l)
122+
sort.Strings(imported)
123+
124+
var unused []string
125+
126+
if logger != nil {
127+
logger.Println("Calculating unused packages to prune. Checking the following packages:")
128+
}
129+
130+
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
131+
if err != nil {
132+
return err
133+
}
134+
135+
// Ignore baseDir and anything that's not a directory.
136+
if path == baseDir || !info.IsDir() {
137+
return nil
138+
}
139+
140+
pkg := strings.TrimPrefix(path, baseDir+string(filepath.Separator))
141+
if logger != nil {
142+
logger.Printf(" %s", pkg)
143+
}
144+
145+
// If pkg is not a parent of an imported package, add it to the unused list.
146+
i := sort.Search(len(imported), func(i int) bool {
147+
return pkg <= imported[i]
148+
})
149+
if i >= len(imported) || !strings.HasPrefix(imported[i], pkg) {
150+
unused = append(unused, path)
151+
}
152+
153+
return nil
154+
})
155+
156+
return unused, err
157+
}
158+
159+
func calculateImportedPackages(l Lock) []string {
160+
var imported []string
161+
162+
for _, project := range l.Projects() {
163+
projectRoot := string(project.Ident().ProjectRoot)
164+
for _, pkg := range project.Packages() {
165+
imported = append(imported, filepath.Join(projectRoot, pkg))
166+
}
167+
}
168+
return imported
169+
}
170+
171+
func pruneNonGoFiles(baseDir string, logger *log.Logger) error {
172+
files, err := calculateNonGoFiles(baseDir)
173+
if err != nil {
174+
return errors.Wrap(err, "could not prune non-Go files")
175+
}
176+
177+
if err := deleteFiles(files); err != nil {
178+
return err
179+
}
180+
181+
return nil
182+
}
183+
184+
func calculateNonGoFiles(baseDir string) ([]string, error) {
185+
var files []string
186+
187+
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
188+
if err != nil {
189+
return err
190+
}
191+
192+
// Ignore directories.
193+
if info.IsDir() {
194+
return nil
195+
}
196+
197+
// Ignore all Go files.
198+
if strings.HasSuffix(info.Name(), ".go") {
199+
return nil
200+
}
201+
202+
// Ignore preserved non-Go files. We check for prefix incase the file
203+
// has an extension. For example: LICENSE.md.
204+
for _, prefix := range preservedNonGoFiles {
205+
if strings.HasPrefix(info.Name(), prefix) {
206+
return nil
207+
}
208+
}
209+
210+
files = append(files, path)
211+
212+
return nil
213+
})
214+
215+
return files, err
216+
}
217+
218+
func pruneGoTestFiles(baseDir string, logger *log.Logger) error {
219+
files, err := calculateGoTestFiles(baseDir)
220+
if err != nil {
221+
return errors.Wrap(err, "could not prune Go test files")
222+
}
223+
224+
if err := deleteFiles(files); err != nil {
225+
return err
226+
}
227+
228+
return nil
229+
}
230+
231+
func calculateGoTestFiles(baseDir string) ([]string, error) {
232+
var files []string
233+
234+
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
235+
if err != nil {
236+
return err
237+
}
238+
239+
// Ignore directories.
240+
if info.IsDir() {
241+
return nil
242+
}
243+
244+
// Ignore any files that is not a Go test file.
245+
if !strings.HasSuffix(info.Name(), "_test.go") {
246+
return nil
247+
}
248+
249+
files = append(files, path)
250+
251+
return nil
252+
})
253+
254+
return files, err
255+
}
256+
257+
func deleteFiles(paths []string) error {
258+
for _, path := range paths {
259+
if err := os.Remove(path); err != nil {
260+
return err
261+
}
262+
}
263+
return nil
264+
}
265+
266+
func pruneEmptyDirs(baseDir string, logger *log.Logger) error {
267+
empty, err := calculateEmptyDirs(baseDir)
268+
if err != nil {
269+
return err
270+
}
271+
272+
if logger != nil {
273+
logger.Println("Deleting empty directories:")
274+
}
275+
276+
for _, dir := range empty {
277+
if logger != nil {
278+
logger.Printf(" %s\n", strings.TrimPrefix(dir, baseDir+string(os.PathSeparator)))
279+
}
280+
}
281+
for _, dir := range empty {
282+
if err := os.Remove(dir); err != nil {
283+
return err
284+
}
285+
}
286+
287+
return nil
288+
}
289+
func calculateEmptyDirs(baseDir string) ([]string, error) {
290+
var empty []string
291+
292+
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
293+
if err != nil {
294+
return nil
295+
}
296+
297+
if !info.IsDir() {
298+
return nil
299+
}
300+
301+
// TODO(ibrasho) should we use fs.IsNonEmptyDir instead?
302+
files, err := ioutil.ReadDir(path)
303+
if err != nil {
304+
return err
305+
}
306+
307+
if len(files) == 0 {
308+
empty = append(empty, path)
309+
}
310+
311+
return nil
312+
})
313+
314+
return empty, err
315+
}

internal/gps/strip_vendor.go

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,43 @@
66

77
package gps
88

9-
import "os"
10-
11-
func stripVendor(path string, info os.FileInfo, err error) error {
12-
if info.Name() == "vendor" {
13-
if _, err := os.Lstat(path); err == nil {
14-
if (info.Mode() & os.ModeSymlink) != 0 {
15-
realInfo, err := os.Stat(path)
16-
if err != nil {
17-
return err
18-
}
19-
if realInfo.IsDir() {
20-
return os.Remove(path)
21-
}
9+
import (
10+
"os"
11+
"path/filepath"
12+
)
13+
14+
func stripNestedVendorDirs(baseDir string) filepath.WalkFunc {
15+
return func(path string, info os.FileInfo, err error) error {
16+
// Ignore anything that's not named "vendor".
17+
if info.Name() != "vendor" {
18+
return nil
19+
}
20+
21+
// Ignore the base vendor directory.
22+
if path == baseDir {
23+
return nil
24+
}
25+
26+
// If it's a directory, delete it along with its content.
27+
if info.IsDir() {
28+
return removeAll(path)
29+
}
30+
31+
if _, err := os.Lstat(path); err != nil {
32+
return nil
33+
}
34+
35+
// If it is a symlink, check if the target is a directory and delete that instead.
36+
if (info.Mode() & os.ModeSymlink) != 0 {
37+
realInfo, err := os.Stat(path)
38+
if err != nil {
39+
return err
2240
}
23-
if info.IsDir() {
24-
return removeAll(path)
41+
if realInfo.IsDir() {
42+
return os.Remove(path)
2543
}
2644
}
27-
}
2845

29-
return nil
46+
return nil
47+
}
3048
}

internal/gps/strip_vendor_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func stripVendorTestCase(tc fsTestCase) func(*testing.T) {
2727

2828
tc.before.setup(t)
2929

30-
if err := filepath.Walk(tempDir, stripVendor); err != nil {
30+
if err := filepath.Walk(tempDir, stripNestedVendorDirs(tempDir)); err != nil {
3131
t.Errorf("filepath.Walk err=%q", err)
3232
}
3333

0 commit comments

Comments
 (0)