New Upstream Release - golang-github-a8m-tree

Ready changes

Summary

Merged new upstream version: 0.0~git20230208.36ae24d (was: 0.0~git20210414.ce3525c+ds).

Resulting package

Built on 2023-03-26T22:17 (took 3m45s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-github-a8m-tree-dev

Lintian Result

Diff

diff --git a/.gitignore b/.gitignore
index 793bf2e..6714d1d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 draft
 coverage
+*~
diff --git a/.travis.yml b/.travis.yml
index dbac1c6..d7475be 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,9 +5,9 @@ arch:
 language: go
 sudo: false
 go:
-  - 1.13.x
-  - 1.14.x
-  - 1.15.x
+  - 1.18.x
+  - 1.19.x
+  - 1.20.x
   - tip
 matrix:
   allow_failures:
diff --git a/color.go b/color.go
index 65bdf27..2dffcce 100644
--- a/color.go
+++ b/color.go
@@ -22,6 +22,11 @@ const (
 	White
 )
 
+// ANSIColorFormat
+func ANSIColorFormat(style string, s string) string {
+	return fmt.Sprintf("%s[%sm%s%s[%dm", Escape, style, s, Escape, Reset)
+}
+
 // ANSIColor
 func ANSIColor(node *Node, s string) string {
 	var style string
@@ -58,7 +63,7 @@ func ANSIColor(node *Node, s string) string {
 	default:
 		return s
 	}
-	return fmt.Sprintf("%s[%sm%s%s[%dm", Escape, style, s, Escape, Reset)
+	return ANSIColorFormat(style, s)
 }
 
 // case-insensitive contains helper
diff --git a/csort_bsd.go b/csort_bsd.go
index f5895a6..21af4b1 100644
--- a/csort_bsd.go
+++ b/csort_bsd.go
@@ -8,6 +8,9 @@ import (
 )
 
 func CTimeSort(f1, f2 os.FileInfo) bool {
+	if f1 == nil || f2 == nil {
+		return f2 == nil
+	}
 	s1, ok1 := f1.Sys().(*syscall.Stat_t)
 	s2, ok2 := f2.Sys().(*syscall.Stat_t)
 	// If this type of node isn't an os node then revert to ModSort
diff --git a/csort_unix.go b/csort_unix.go
index 6acd092..e5777b8 100644
--- a/csort_unix.go
+++ b/csort_unix.go
@@ -1,4 +1,5 @@
-//+build linux openbsd dragonfly android solaris
+//go:build linux || openbsd || dragonfly || android || solaris
+// +build linux openbsd dragonfly android solaris
 
 package tree
 
@@ -8,6 +9,9 @@ import (
 )
 
 func CTimeSort(f1, f2 os.FileInfo) bool {
+	if f1 == nil || f2 == nil {
+		return f2 == nil
+	}
 	s1, ok1 := f1.Sys().(*syscall.Stat_t)
 	s2, ok2 := f2.Sys().(*syscall.Stat_t)
 	// If this type of node isn't an os node then revert to ModSort
diff --git a/debian/changelog b/debian/changelog
index 033d018..cda57c5 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+golang-github-a8m-tree (0.0~git20230208.36ae24d-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sun, 26 Mar 2023 22:14:34 -0000
+
 golang-github-a8m-tree (0.0~git20201026.fce18e2-1) unstable; urgency=medium
 
   [ Alexandre Viau ]
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..9cc896d
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/a8m/tree
+
+go 1.15
diff --git a/node.go b/node.go
index 4d15e56..fc36573 100644
--- a/node.go
+++ b/node.go
@@ -49,6 +49,8 @@ type Options struct {
 	DeepLevel  int
 	Pattern    string
 	IPattern   string
+	MatchDirs  bool
+	Prune      bool
 	// File
 	ByteSize bool
 	UnitSize bool
@@ -71,6 +73,16 @@ type Options struct {
 	// Graphics
 	NoIndent bool
 	Colorize bool
+	// Color defaults to ANSIColor()
+	Color func(*Node, string) string
+}
+
+func (opts *Options) color(node *Node, s string) string {
+	f := opts.Color
+	if f == nil {
+		f = ANSIColor
+	}
+	return f(node, s)
 }
 
 // New get path and create new node(root).
@@ -103,6 +115,16 @@ func (node *Node) Visit(opts *Options) (dirs, files int) {
 	if opts.DeepLevel > 0 && opts.DeepLevel <= node.depth {
 		return
 	}
+	// MatchDirs option
+	var dirMatch bool
+	if node.depth != 0 && opts.MatchDirs {
+		// then disable prune and pattern for immediate children
+		if opts.Pattern != "" {
+			dirMatch = node.match(opts.Pattern, opts)
+		} else if opts.IPattern != "" && node.match(opts.IPattern, opts) {
+			return
+		}
+	}
 	names, err := opts.Fs.ReadDir(node.path)
 	if err != nil {
 		node.err = err
@@ -120,26 +142,26 @@ func (node *Node) Visit(opts *Options) (dirs, files int) {
 			vpaths: node.vpaths,
 		}
 		d, f := nnode.Visit(opts)
-		if nnode.err == nil && !nnode.IsDir() {
-			// "dirs only" option
-			if opts.DirsOnly {
-				continue
-			}
-			var rePrefix string
-			if opts.IgnoreCase {
-				rePrefix = "(?i)"
-			}
-			// Pattern matching
-			if opts.Pattern != "" {
-				re, err := regexp.Compile(rePrefix + opts.Pattern)
-				if err == nil && !re.MatchString(name) {
+		if nnode.err == nil {
+			if nnode.IsDir() {
+				// "prune" option, hide empty directories
+				if opts.Prune && f == 0 {
 					continue
 				}
-			}
-			// IPattern matching
-			if opts.IPattern != "" {
-				re, err := regexp.Compile(rePrefix + opts.IPattern)
-				if err == nil && re.MatchString(name) {
+				if opts.MatchDirs && opts.IPattern != "" && nnode.match(opts.IPattern, opts) {
+					continue
+				}
+			} else {
+				// "dirs only" option
+				if opts.DirsOnly {
+					continue
+				}
+				// Pattern matching
+				if !dirMatch && opts.Pattern != "" && !nnode.match(opts.Pattern, opts) {
+					continue
+				}
+				// IPattern matching
+				if opts.IPattern != "" && nnode.match(opts.IPattern, opts) {
 					continue
 				}
 			}
@@ -154,6 +176,19 @@ func (node *Node) Visit(opts *Options) (dirs, files int) {
 	return
 }
 
+func (node *Node) match(pattern string, opt *Options) bool {
+	var prefix string
+	if opt.IgnoreCase {
+		prefix = "(?i)"
+	}
+	search := node.Name()
+	if strings.Contains(pattern, "*") {
+		search = node.path
+	}
+	re, err := regexp.Compile(prefix + pattern)
+	return err == nil && re.FindString(search) != ""
+}
+
 func (node *Node) sort(opts *Options) {
 	var fn SortFunc
 	switch {
@@ -181,6 +216,11 @@ func (node *Node) sort(opts *Options) {
 	}
 }
 
+// Path returns the Node's absolute path
+func (node *Node) Path() string {
+	return node.path
+}
+
 // Print nodes based on the given configuration.
 func (node *Node) Print(opts *Options) { node.print("", opts) }
 
@@ -214,7 +254,11 @@ func (node *Node) print(indent string, opts *Options) {
 		if msgs := strings.Split(err, ": "); len(msgs) > 1 {
 			err = msgs[1]
 		}
-		fmt.Printf("%s [%s]\n", node.path, err)
+		name := node.path
+		if !opts.FullPath {
+			name = filepath.Base(name)
+		}
+		fmt.Fprintf(opts.OutFile, "%s [%s]\n", name, err)
 		return
 	}
 	if !node.IsDir() {
@@ -302,7 +346,7 @@ func (node *Node) print(indent string, opts *Options) {
 	}
 	// Colorize
 	if opts.Colorize {
-		name = ANSIColor(node, name)
+		name = opts.color(node, name)
 	}
 	// IsSymlink
 	if node.Mode()&os.ModeSymlink == os.ModeSymlink {
@@ -316,7 +360,7 @@ func (node *Node) print(indent string, opts *Options) {
 		}
 		fi, err := opts.Fs.Stat(targetPath)
 		if opts.Colorize && fi != nil {
-			vtarget = ANSIColor(&Node{FileInfo: fi, path: vtarget}, vtarget)
+			vtarget = opts.color(&Node{FileInfo: fi, path: vtarget}, vtarget)
 		}
 		name = fmt.Sprintf("%s -> %s", name, vtarget)
 		// Follow symbolic links like directories
diff --git a/node_test.go b/node_test.go
index 5a0a5f1..8ead508 100644
--- a/node_test.go
+++ b/node_test.go
@@ -1,6 +1,7 @@
 package tree
 
 import (
+	"errors"
 	"os"
 	"syscall"
 	"testing"
@@ -63,6 +64,9 @@ func (fs *MockFs) addFile(path string, file *file) *MockFs {
 }
 
 func (fs *MockFs) Stat(path string) (os.FileInfo, error) {
+	if path == "root/bad" {
+		return nil, errors.New("stat failed")
+	}
 	return fs.files[path], nil
 }
 func (fs *MockFs) ReadDir(path string) ([]string, error) {
@@ -109,47 +113,133 @@ var listTests = []treeTest{
 	{"basic", &Options{Fs: fs, OutFile: out}, `root
 ├── a
 ├── b
-└── c
-    ├── d
-    └── e
-`, 1, 4},
+├── c
+│   ├── d
+│   ├── e
+│   ├── g
+│   │   ├── h
+│   │   └── i
+│   └── k
+└── j
+`, 2, 8},
 	{"all", &Options{Fs: fs, OutFile: out, All: true, NoSort: true}, `root
 ├── a
 ├── b
-└── c
-    ├── d
-    ├── e
-    └── .f
-`, 1, 5},
+├── c
+│   ├── d
+│   ├── e
+│   ├── .f
+│   ├── g
+│   │   ├── h
+│   │   └── i
+│   └── k
+└── j
+`, 2, 9},
 	{"dirs", &Options{Fs: fs, OutFile: out, DirsOnly: true}, `root
 └── c
-`, 1, 0},
+    └── g
+`, 2, 0},
 	{"fullPath", &Options{Fs: fs, OutFile: out, FullPath: true}, `root
 ├── root/a
 ├── root/b
-└── root/c
-    ├── root/c/d
-    └── root/c/e
-`, 1, 4},
+├── root/c
+│   ├── root/c/d
+│   ├── root/c/e
+│   ├── root/c/g
+│   │   ├── root/c/g/h
+│   │   └── root/c/g/i
+│   └── root/c/k
+└── root/j
+`, 2, 8},
 	{"deepLevel", &Options{Fs: fs, OutFile: out, DeepLevel: 1}, `root
 ├── a
 ├── b
+├── c
+└── j
+`, 1, 3},
+	{"pattern (a|e|i)", &Options{Fs: fs, OutFile: out, Pattern: "(a|e|i)"}, `root
+├── a
 └── c
-`, 1, 2},
-	{"pattern", &Options{Fs: fs, OutFile: out, Pattern: "(a|e)"}, `root
+    ├── e
+    └── g
+        └── i
+`, 2, 3},
+	{"pattern (x) + 0 files", &Options{Fs: fs, OutFile: out, Pattern: "(x)"}, `root
+└── c
+    └── g
+`, 2, 0},
+	{"ipattern (a|e|i)", &Options{Fs: fs, OutFile: out, IPattern: "(a|e|i)"}, `root
+├── b
+├── c
+│   ├── d
+│   ├── g
+│   │   └── h
+│   └── k
+└── j
+`, 2, 5},
+	{"pattern (A) + ignore-case", &Options{Fs: fs, OutFile: out, Pattern: "(A)", IgnoreCase: true}, `root
 ├── a
 └── c
-    └── e
-`, 1, 2},
-	{"ipattern", &Options{Fs: fs, OutFile: out, IPattern: "(a|e)"}, `root
+    └── g
+`, 2, 1},
+	{"pattern (A) + ignore-case + prune", &Options{Fs: fs, OutFile: out, Pattern: "(A)", Prune: true, IgnoreCase: true}, `root
+└── a
+`, 0, 1},
+	{"pattern (a) + prune", &Options{Fs: fs, OutFile: out, Pattern: "(a)", Prune: true}, `root
+└── a
+`, 0, 1},
+	{"pattern (c) + matchdirs", &Options{Fs: fs, OutFile: out, Pattern: "(c)", MatchDirs: true}, `root
+└── c
+    ├── d
+    ├── e
+    ├── g
+    └── k
+`, 2, 3},
+	{"pattern (c.*) + matchdirs", &Options{Fs: fs, OutFile: out, Pattern: "(c.*)", MatchDirs: true}, `root
+└── c
+    ├── d
+    ├── e
+    ├── g
+    │   ├── h
+    │   └── i
+    └── k
+`, 2, 5},
+	{"ipattern (c) + matchdirs", &Options{Fs: fs, OutFile: out, IPattern: "(c)", MatchDirs: true}, `root
+├── a
+├── b
+└── j
+`, 0, 3},
+	{"ipattern (g) + matchdirs", &Options{Fs: fs, OutFile: out, IPattern: "(g)", MatchDirs: true}, `root
+├── a
 ├── b
+├── c
+│   ├── d
+│   ├── e
+│   └── k
+└── j
+`, 1, 6},
+	{"ipattern (a|e|i|h) + matchdirs + prune", &Options{Fs: fs, OutFile: out, IPattern: "(a|e|i|h)", MatchDirs: true, Prune: true}, `root
+├── b
+├── c
+│   ├── d
+│   └── k
+└── j
+`, 1, 4},
+	{"pattern (d|e) + prune", &Options{Fs: fs, OutFile: out, Pattern: "(d|e)", Prune: true}, `root
 └── c
-    └── d
+    ├── d
+    └── e
 `, 1, 2},
-	{"ignore-case", &Options{Fs: fs, OutFile: out, Pattern: "(A)", IgnoreCase: true}, `root
-├── a
+	{"pattern (c.*) + matchdirs + prune ", &Options{Fs: fs, OutFile: out, Pattern: "(c.*)", Prune: true, MatchDirs: true}, `root
 └── c
-`, 1, 1}}
+    ├── d
+    ├── e
+    ├── g
+    │   ├── h
+    │   └── i
+    └── k
+`, 2, 5},
+}
 
 func TestSimple(t *testing.T) {
 	root := &file{
@@ -165,8 +255,18 @@ func TestSimple(t *testing.T) {
 					{name: "d", size: 50},
 					{name: "e", size: 50},
 					{name: ".f", size: 0},
+					{
+						name: "g",
+						size: 100,
+						files: []*file{
+							{name: "h", size: 50},
+							{name: "i", size: 50},
+						},
+					},
+					{name: "k", size: 50},
 				},
 			},
+			{name: "j", size: 50},
 		},
 	}
 	fs.clean().addFile(root.name, root)
@@ -177,7 +277,7 @@ func TestSimple(t *testing.T) {
 			t.Errorf("wrong dir count for test %q:\ngot:\n%d\nexpected:\n%d", test.name, d, test.dirs)
 		}
 		if f != test.files {
-			t.Errorf("wrong dir count for test %q:\ngot:\n%d\nexpected:\n%d", test.name, d, test.files)
+			t.Errorf("wrong file count for test %q:\ngot:\n%d\nexpected:\n%d", test.name, f, test.files)
 		}
 		inf.Print(test.opts)
 		if !out.equal(test.expected) {
@@ -396,3 +496,158 @@ func TestCount(t *testing.T) {
 		t.Errorf("TestCount - expect (dir, file) count to be equal to (7, 8)\n%s", out.str)
 	}
 }
+
+var errorTests = []treeTest{
+	{"basic", &Options{Fs: fs, OutFile: out}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"all", &Options{Fs: fs, OutFile: out, All: true, NoSort: true}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"dirs", &Options{Fs: fs, OutFile: out, DirsOnly: true}, `root
+└── bad [stat failed]
+`, 0, 0},
+	{"fullPath", &Options{Fs: fs, OutFile: out, FullPath: true}, `root
+├── root/a
+├── root/b
+├── root/j
+└── root/bad [stat failed]
+`, 0, 3},
+	{"deepLevel", &Options{Fs: fs, OutFile: out, DeepLevel: 1}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"pattern (a|e|i)", &Options{Fs: fs, OutFile: out, Pattern: "(a|e|i)"}, `root
+├── a
+└── bad [stat failed]
+`, 0, 1},
+	{"pattern (x) + 0 files", &Options{Fs: fs, OutFile: out, Pattern: "(x)"}, `root
+└── bad [stat failed]
+`, 0, 0},
+	{"ipattern (a|e|i)", &Options{Fs: fs, OutFile: out, IPattern: "(a|e|i)"}, `root
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 2},
+	{"pattern (A) + ignore-case", &Options{Fs: fs, OutFile: out, Pattern: "(A)", IgnoreCase: true}, `root
+├── a
+└── bad [stat failed]
+`, 0, 1},
+	{"pattern (A) + ignore-case + prune", &Options{Fs: fs, OutFile: out, Pattern: "(A)", Prune: true, IgnoreCase: true}, `root
+├── a
+└── bad [stat failed]
+`, 0, 1},
+	{"pattern (a) + prune", &Options{Fs: fs, OutFile: out, Pattern: "(a)", Prune: true}, `root
+├── a
+└── bad [stat failed]
+`, 0, 1},
+	{"pattern (c) + matchdirs", &Options{Fs: fs, OutFile: out, Pattern: "(c)", MatchDirs: true}, `root
+└── bad [stat failed]
+`, 0, 0},
+	{"pattern (c.*) + matchdirs", &Options{Fs: fs, OutFile: out, Pattern: "(c.*)", MatchDirs: true}, `root
+└── bad [stat failed]
+`, 0, 0},
+	{"ipattern (c) + matchdirs", &Options{Fs: fs, OutFile: out, IPattern: "(c)", MatchDirs: true}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"ipattern (g) + matchdirs", &Options{Fs: fs, OutFile: out, IPattern: "(g)", MatchDirs: true}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"ipattern (a|e|i|h) + matchdirs + prune", &Options{Fs: fs, OutFile: out, IPattern: "(a|e|i|h)", MatchDirs: true, Prune: true}, `root
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 2},
+	{"pattern (d|e) + prune", &Options{Fs: fs, OutFile: out, Pattern: "(d|e)", Prune: true}, `root
+└── bad [stat failed]
+`, 0, 0},
+	{"pattern (c.*) + matchdirs + prune ", &Options{Fs: fs, OutFile: out, Pattern: "(c.*)", Prune: true, MatchDirs: true}, `root
+└── bad [stat failed]
+`, 0, 0},
+
+	{"name-sort", &Options{Fs: fs, OutFile: out, NameSort: true}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"dirs-first sort", &Options{Fs: fs, OutFile: out, DirSort: true}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"reverse sort", &Options{Fs: fs, OutFile: out, ReverSort: true, NameSort: true}, `root
+├── bad [stat failed]
+├── j
+├── b
+└── a
+`, 0, 3},
+	{"no-sort", &Options{Fs: fs, OutFile: out, NoSort: true, DirSort: true}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"size-sort", &Options{Fs: fs, OutFile: out, SizeSort: true}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"last-mod-sort", &Options{Fs: fs, OutFile: out, ModSort: true}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+	{"c-time-sort", &Options{Fs: fs, OutFile: out, CTimeSort: true}, `root
+├── a
+├── b
+├── j
+└── bad [stat failed]
+`, 0, 3},
+}
+
+func TestError(t *testing.T) {
+	root := &file{
+		name: "root",
+		size: 200,
+		files: []*file{
+			{name: "a", size: 50},
+			{name: "b", size: 50},
+			{name: "j", size: 50},
+			{name: "bad", size: 50}, // stat fails on this file
+		},
+	}
+	fs.clean().addFile(root.name, root)
+	for _, test := range errorTests {
+		inf := New(root.name)
+		d, f := inf.Visit(test.opts)
+		if d != test.dirs {
+			t.Errorf("wrong dir count for test %q:\ngot:\n%d\nexpected:\n%d", test.name, d, test.dirs)
+		}
+		if f != test.files {
+			t.Errorf("wrong file count for test %q:\ngot:\n%d\nexpected:\n%d", test.name, f, test.files)
+		}
+		inf.Print(test.opts)
+		if !out.equal(test.expected) {
+			t.Errorf("%s:\ngot:\n%+v\nexpected:\n%+v", test.name, out.str, test.expected)
+		}
+		out.clear()
+	}
+}
diff --git a/sort.go b/sort.go
index 5529993..0ee8a07 100644
--- a/sort.go
+++ b/sort.go
@@ -17,22 +17,38 @@ func (b ByFunc) Less(i, j int) bool {
 type SortFunc func(f1, f2 os.FileInfo) bool
 
 func ModSort(f1, f2 os.FileInfo) bool {
+	// This ensures any nil os.FileInfos sort at the end
+	if f1 == nil || f2 == nil {
+		return f2 == nil
+	}
 	return f1.ModTime().Before(f2.ModTime())
 }
 
 func DirSort(f1, f2 os.FileInfo) bool {
+	if f1 == nil || f2 == nil {
+		return f2 == nil
+	}
 	return f1.IsDir() && !f2.IsDir()
 }
 
 func SizeSort(f1, f2 os.FileInfo) bool {
+	if f1 == nil || f2 == nil {
+		return f2 == nil
+	}
 	return f1.Size() < f2.Size()
 }
 
 func NameSort(f1, f2 os.FileInfo) bool {
+	if f1 == nil || f2 == nil {
+		return f2 == nil
+	}
 	return f1.Name() < f2.Name()
 }
 
 func VerSort(f1, f2 os.FileInfo) bool {
+	if f1 == nil || f2 == nil {
+		return f2 == nil
+	}
 	return NaturalLess(f1.Name(), f2.Name())
 }
 

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/gocode/src/github.com/a8m/tree/go.mod

No differences were encountered in the control files

More details

Full run details