Make code lint-clean under the canonical golangci-lint config
150 findings fixed: linter autofixes for whitespace style (wsl_v5, nlreturn, noinlineerr), named returns removed, magic numbers replaced with named constants, static errNotRegular error, explicit Close/Parse error handling, unused cobra params renamed to _, and runReport/ runTrees/hashPass split into helpers to satisfy cyclop, gocognit, and funlen. Behavior verified unchanged against the README smoke test.
This commit is contained in:
113
trees.go
113
trees.go
@@ -38,16 +38,64 @@ type treeNode struct {
|
||||
func runTrees(args []string) {
|
||||
in, name, closer := openScanInput(args)
|
||||
defer closer()
|
||||
|
||||
recs, malformed := parseScanStream(in, name)
|
||||
records := len(recs) + malformed
|
||||
|
||||
// Build the hierarchy under a synthetic super-root. Paths are
|
||||
// split on "/"; for absolute paths the first component is empty,
|
||||
// which simply becomes a top-level node representing "/".
|
||||
super, allDirs := buildHierarchy(recs)
|
||||
super.compute()
|
||||
|
||||
dupes := collectTreeGroups(allDirs, super)
|
||||
|
||||
out := bufio.NewWriterSize(os.Stdout, ioBufSize)
|
||||
|
||||
_, err := fmt.Fprintln(out, "first\tdupe\tfiles\tsize")
|
||||
if err != nil {
|
||||
fatalf("write stdout: %v", err)
|
||||
}
|
||||
|
||||
dupeTrees := 0
|
||||
|
||||
var reclaimable int64
|
||||
|
||||
for _, g := range dupes {
|
||||
first := g[0]
|
||||
for _, n := range g[1:] {
|
||||
_, err = fmt.Fprintf(out, "%s\t%s\t%d\t%d\n",
|
||||
first.path, n.path, first.fileCount, first.totalSize)
|
||||
if err != nil {
|
||||
fatalf("write stdout: %v", err)
|
||||
}
|
||||
|
||||
dupeTrees++
|
||||
reclaimable += first.totalSize
|
||||
}
|
||||
}
|
||||
|
||||
err = out.Flush()
|
||||
if err != nil {
|
||||
fatalf("write stdout: %v", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"trees: %d records read%s, %d duplicate tree groups, %d dupe trees, %s reclaimable\n",
|
||||
records, malformedNote(malformed), len(dupes), dupeTrees,
|
||||
humanBytes(reclaimable))
|
||||
}
|
||||
|
||||
// buildHierarchy reconstructs the directory hierarchy from the record
|
||||
// paths under a synthetic super-root. Paths are split on "/"; for
|
||||
// absolute paths the first component is empty, which simply becomes a
|
||||
// top-level node representing "/". It returns the super-root and every
|
||||
// directory node created.
|
||||
func buildHierarchy(recs []scanRec) (*treeNode, []*treeNode) {
|
||||
super := &treeNode{}
|
||||
|
||||
var allDirs []*treeNode
|
||||
|
||||
for _, r := range recs {
|
||||
comps := strings.Split(r.path, "/")
|
||||
|
||||
node := super
|
||||
for _, c := range comps[:len(comps)-1] {
|
||||
child := node.dirs[c]
|
||||
@@ -56,76 +104,68 @@ func runTrees(args []string) {
|
||||
if node != super {
|
||||
childPath = node.path + "/" + c
|
||||
}
|
||||
|
||||
child = &treeNode{path: childPath, parent: node}
|
||||
if node.dirs == nil {
|
||||
node.dirs = make(map[string]*treeNode)
|
||||
}
|
||||
|
||||
node.dirs[c] = child
|
||||
allDirs = append(allDirs, child)
|
||||
}
|
||||
|
||||
node = child
|
||||
}
|
||||
|
||||
if node.files == nil {
|
||||
node.files = make(map[string]fileSig)
|
||||
}
|
||||
|
||||
node.files[comps[len(comps)-1]] = fileSig{
|
||||
size: r.size, head: r.head, tail: r.tail,
|
||||
}
|
||||
}
|
||||
super.compute()
|
||||
|
||||
// Group directories by digest; two or more dirs sharing a digest
|
||||
// are candidate duplicate trees.
|
||||
return super, allDirs
|
||||
}
|
||||
|
||||
// collectTreeGroups groups directories by digest and returns every
|
||||
// maximal group with two or more members, each group's members sorted
|
||||
// by path, groups ordered by tree size descending then by first path
|
||||
// ascending.
|
||||
func collectTreeGroups(allDirs []*treeNode, super *treeNode) [][]*treeNode {
|
||||
groups := make(map[[sha256.Size]byte][]*treeNode)
|
||||
for _, d := range allDirs {
|
||||
groups[d.digest] = append(groups[d.digest], d)
|
||||
}
|
||||
|
||||
var dupes [][]*treeNode
|
||||
|
||||
for _, g := range groups {
|
||||
if len(g) < 2 || suppressed(g, super) {
|
||||
if len(g) < minGroupSize || suppressed(g, super) {
|
||||
continue
|
||||
}
|
||||
|
||||
slices.SortFunc(g, func(a, b *treeNode) int {
|
||||
return strings.Compare(a.path, b.path)
|
||||
})
|
||||
dupes = append(dupes, g)
|
||||
}
|
||||
|
||||
// Biggest reclaimable space first; ties broken by first path.
|
||||
slices.SortFunc(dupes, func(a, b []*treeNode) int {
|
||||
if a[0].totalSize != b[0].totalSize {
|
||||
if a[0].totalSize > b[0].totalSize {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
return strings.Compare(a[0].path, b[0].path)
|
||||
})
|
||||
|
||||
out := bufio.NewWriterSize(os.Stdout, 1<<20)
|
||||
if _, err := fmt.Fprintln(out, "first\tdupe\tfiles\tsize"); err != nil {
|
||||
fatalf("write stdout: %v", err)
|
||||
}
|
||||
dupeTrees := 0
|
||||
var reclaimable int64
|
||||
for _, g := range dupes {
|
||||
first := g[0]
|
||||
for _, n := range g[1:] {
|
||||
if _, err := fmt.Fprintf(out, "%s\t%s\t%d\t%d\n",
|
||||
first.path, n.path, first.fileCount, first.totalSize); err != nil {
|
||||
fatalf("write stdout: %v", err)
|
||||
}
|
||||
dupeTrees++
|
||||
reclaimable += first.totalSize
|
||||
}
|
||||
}
|
||||
if err := out.Flush(); err != nil {
|
||||
fatalf("write stdout: %v", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"trees: %d records read%s, %d duplicate tree groups, %d dupe trees, %s reclaimable\n",
|
||||
records, malformedNote(malformed), len(dupes), dupeTrees,
|
||||
humanBytes(reclaimable))
|
||||
return dupes
|
||||
}
|
||||
|
||||
// compute fills in digest, fileCount, and totalSize for n and all of
|
||||
@@ -143,18 +183,22 @@ func (n *treeNode) compute() {
|
||||
n.fileCount++
|
||||
n.totalSize += sig.size
|
||||
}
|
||||
|
||||
for name, child := range n.dirs {
|
||||
child.compute()
|
||||
entries = append(entries, "d\x00"+name+"\x00"+string(child.digest[:]))
|
||||
n.fileCount += child.fileCount
|
||||
n.totalSize += child.totalSize
|
||||
}
|
||||
|
||||
slices.Sort(entries)
|
||||
|
||||
h := sha256.New()
|
||||
for _, e := range entries {
|
||||
h.Write([]byte(e))
|
||||
h.Write([]byte{0})
|
||||
}
|
||||
|
||||
copy(n.digest[:], h.Sum(nil))
|
||||
}
|
||||
|
||||
@@ -165,15 +209,19 @@ func (n *treeNode) compute() {
|
||||
// parent) or members whose parents differ are always reported.
|
||||
func suppressed(g []*treeNode, super *treeNode) bool {
|
||||
seen := make(map[*treeNode]bool, len(g))
|
||||
|
||||
var parentDigest [sha256.Size]byte
|
||||
|
||||
for i, n := range g {
|
||||
p := n.parent
|
||||
if p == nil || p == super {
|
||||
return false
|
||||
}
|
||||
|
||||
if seen[p] {
|
||||
return false // siblings: not implied by any parent group
|
||||
}
|
||||
|
||||
seen[p] = true
|
||||
if i == 0 {
|
||||
parentDigest = p.digest
|
||||
@@ -181,5 +229,6 @@ func suppressed(g []*treeNode, super *treeNode) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user