synapsecns/sanguine

View on GitHub
contrib/git-changes-action/detector/tree/tree.go

Summary

Maintainability
A
0 mins
Test Coverage
// Package tree implements a tree data structure for representing a directory structure.
//
// It provides methods for adding paths, checking for the presence of a path, generating
// a string representation of the tree, getting all paths in the tree, and retrieving a random path.
//
// It leverages the github.com/xlab/treeprint package to generate the string representation.
// The implementation is concurrent-safe through the use of a sync.RWMutex.
package tree

import (
    "github.com/xlab/treeprint"
    "strings"
    "sync"
)

// Tree is a tree data structure for representing a directory structure.
type Tree interface {
    // Add adds a path to the tree
    Add(paths ...string)
    // HasPath checks if any of the paths in the tree contain the given path
    HasPath(path string) bool
    // ToString gets a string representation of the tree
    ToString() string
    // AllPaths gets all paths in the tree. This will include paths that might not have been explicitly added
    // e.g. if you Add() a/b/c/d.go, AllPaths() will return a/b/c/d.go, a/b/c a/b/ and a/
    AllPaths() []string
}

type treeImpl struct {
    nodes []Node
    mux   sync.RWMutex
}

// NewTree creates a new tree.
func NewTree() Tree {
    return &treeImpl{}
}

// Node is a node in the tree.
type Node struct {
    Name     string `json:"name"`
    Children []Node `json:"children,omitempty"`
}

// Add adds a path to the tree and all of it's component paths to the tree.
func (t *treeImpl) Add(paths ...string) {
    t.mux.Lock()
    defer t.mux.Unlock()

    for _, path := range paths {
        path = strings.TrimPrefix(path, "./")

        t.nodes = addToTree(t.nodes, strings.Split(path, "/"))
    }
}

// addToTree adds a path to the tree. It takes the current root node and an array of names (the names of nodes in the path).
// If a node with a given name already exists in the tree, it is used. If not, a new node with that name is added.
// Returns the updated tree root.
func addToTree(root []Node, names []string) []Node {
    if len(names) > 0 {
        var i int
        for i = 0; i < len(root); i++ {
            if root[i].Name == names[0] { // already in tree
                break
            }
        }
        if i == len(root) {
            root = append(root, Node{Name: names[0]})
        }
        root[i].Children = addToTree(root[i].Children, names[1:])
    }
    return root
}

func (t *treeImpl) HasPath(path string) bool {
    t.mux.RLock()
    defer t.mux.RUnlock()
    // trim a prefix slash if it exists
    path = strings.TrimPrefix(path, "./")
    path = strings.TrimPrefix(path, "/")
    for i := 0; i < len(t.nodes); i++ {
        if has(t.nodes[i], path) {
            return true
        }
    }
    return false
}

// has checks wether a path is in the tree recursively
// path is the path to check and nodePath is any previosus paths that belong to the node
// nodePath is passed in as an array so that it can be left blank, there should never be more than 1 node path.
func has(node Node, path string, nodePath ...string) bool {
    newNodePath := append(nodePath, node.Name)
    newNodePathStr := strings.Join(newNodePath, "/")

    if !strings.HasPrefix(path, newNodePathStr) {
        return false
    }

    if newNodePathStr == path {
        return true
    }
    for i := 0; i < len(node.Children); i++ {
        if has(node.Children[i], path, newNodePath...) {
            return true
        }
    }
    return false
}

func (t *treeImpl) AllPaths() []string {
    t.mux.RLock()
    defer t.mux.RUnlock()

    var allPaths []string
    for i := 0; i < len(t.nodes); i++ {
        allPaths = append(allPaths, allPathsFromNode(t.nodes[i])...)
    }
    return allPaths
}

func allPathsFromNode(node Node, nodePaths ...string) []string {
    var allPaths []string
    newNodePaths := append(nodePaths, node.Name)
    newNodePathsStr := strings.Join(newNodePaths, "/")
    allPaths = append(allPaths, newNodePathsStr)
    for i := 0; i < len(node.Children); i++ {
        allPaths = append(allPaths, allPathsFromNode(node.Children[i], newNodePaths...)...)
    }
    return allPaths
}

func (t *treeImpl) ToString() string {
    t.mux.RLock()
    defer t.mux.RUnlock()
    printer := treeprint.New()
    for _, node := range t.nodes {
        printer = addPrinterNode(printer, node)
    }
    return printer.String()
}

// addPrinterNode adds a node to the printer tree.
func addPrinterNode(tree treeprint.Tree, node Node) treeprint.Tree {
    newNode := tree.AddBranch(node.Name)
    for _, child := range node.Children {
        addPrinterNode(newNode, child)
    }
    return tree
}