mirror of
https://github.com/NixOS/nix.git
synced 2025-12-11 11:31:03 +01:00
refactor(nix): migrate why-depends to use DependencyGraph
Replaces manual graph traversal in `nix why-depends` with the new
DependencyGraph infrastructure. Simplifies code while maintaining
identical output and behavior.
## Changes to why-depends
**Before**: Custom Node struct with manual Dijkstra implementation
- 150+ lines of graph management boilerplate
- Manual priority queue and distance tracking
- Custom reverse reference bookkeeping
- Inline file scanning mixed with graph traversal
**After**: Clean API calls to DependencyGraph
- Replaced Node struct with DependencyGraph<StorePath>
- Replaced manual Dijkstra with computeDistancesFrom()
- Use getSuccessors() instead of managing refs/rrefs
- Extracted findHashContexts() helper for clarity
**Improvements**:
- ~50 lines shorter, easier to understand
- C++20 ranges for cleaner iteration
- Separated concerns: graph ops vs. file scanning vs. formatting
- Complex algorithms now in well-tested DependencyGraph
## Example Transformation
```cpp
// Before: Manual graph building
std::map<StorePath, Node> graph;
for (auto & path : closure)
graph.emplace(path, Node{...});
for (auto & node : graph)
for (auto & ref : node.second.refs)
graph.find(ref)->second.rrefs.insert(node.first);
// After: Use DependencyGraph API
StorePathGraph depGraph(*store, closure);
depGraph.computeDistancesFrom(dependencyPath);
auto successors = depGraph.getSuccessors(nodePath);
```
## Helper Extraction
**findHashContexts()**: Extracted file scanning into reusable function
- Takes accessor, reference paths, target hash
- Returns hash → formatted context strings
- Keeps printNode() focused on graph traversal
## Test Improvements
**references.cc**: Changed ASSERT_EQ → EXPECT_EQ
- EXPECT continues after failure, shows all results
- Better debugging when multiple assertions fail
- GoogleTest best practice
## Behavior Preservation
**Critical**: Pure refactoring, zero user-visible changes
- Identical output format
- Same algorithm (Dijkstra shortest paths)
- Same filtering/highlighting
- Same CLI interface
## Benefits
1. **Maintainability**: Algorithms in shared, tested infrastructure
2. **Correctness**: DependencyGraph thoroughly unit tested
3. **Reusability**: Other commands can now use DependencyGraph
4. **Readability**: why-depends focused on UI, not graph algorithms
5. **Future-proof**: Sets stage for improved error messages
This commit is contained in:
parent
40beb50e8f
commit
60857de63e
2 changed files with 145 additions and 183 deletions
|
|
@ -32,7 +32,7 @@ TEST_P(RewriteTest, IdentityRewriteIsIdentity)
|
||||||
auto rewriter = RewritingSink(param.rewrites, rewritten);
|
auto rewriter = RewritingSink(param.rewrites, rewritten);
|
||||||
rewriter(param.originalString);
|
rewriter(param.originalString);
|
||||||
rewriter.flush();
|
rewriter.flush();
|
||||||
ASSERT_EQ(rewritten.s, param.finalString);
|
EXPECT_EQ(rewritten.s, param.finalString);
|
||||||
}
|
}
|
||||||
|
|
||||||
INSTANTIATE_TEST_CASE_P(
|
INSTANTIATE_TEST_CASE_P(
|
||||||
|
|
@ -52,14 +52,14 @@ TEST(references, scan)
|
||||||
RefScanSink scanner(StringSet{hash1});
|
RefScanSink scanner(StringSet{hash1});
|
||||||
auto s = "foobar";
|
auto s = "foobar";
|
||||||
scanner(s);
|
scanner(s);
|
||||||
ASSERT_EQ(scanner.getResult(), StringSet{});
|
EXPECT_EQ(scanner.getResult(), StringSet{});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
RefScanSink scanner(StringSet{hash1});
|
RefScanSink scanner(StringSet{hash1});
|
||||||
auto s = "foobar" + hash1 + "xyzzy";
|
auto s = "foobar" + hash1 + "xyzzy";
|
||||||
scanner(s);
|
scanner(s);
|
||||||
ASSERT_EQ(scanner.getResult(), StringSet{hash1});
|
EXPECT_EQ(scanner.getResult(), StringSet{hash1});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -69,7 +69,7 @@ TEST(references, scan)
|
||||||
scanner(((std::string_view) s).substr(10, 5));
|
scanner(((std::string_view) s).substr(10, 5));
|
||||||
scanner(((std::string_view) s).substr(15, 5));
|
scanner(((std::string_view) s).substr(15, 5));
|
||||||
scanner(((std::string_view) s).substr(20));
|
scanner(((std::string_view) s).substr(20));
|
||||||
ASSERT_EQ(scanner.getResult(), StringSet({hash1, hash2}));
|
EXPECT_EQ(scanner.getResult(), StringSet({hash1, hash2}));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -77,7 +77,7 @@ TEST(references, scan)
|
||||||
auto s = "foobar" + hash1 + "xyzzy" + hash2;
|
auto s = "foobar" + hash1 + "xyzzy" + hash2;
|
||||||
for (auto & i : s)
|
for (auto & i : s)
|
||||||
scanner(std::string(1, i));
|
scanner(std::string(1, i));
|
||||||
ASSERT_EQ(scanner.getResult(), StringSet({hash1, hash2}));
|
EXPECT_EQ(scanner.getResult(), StringSet({hash1, hash2}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ TEST(references, scanForReferencesDeep)
|
||||||
{
|
{
|
||||||
CanonPath f1Path("/file1.txt");
|
CanonPath f1Path("/file1.txt");
|
||||||
auto it = foundRefs.find(f1Path);
|
auto it = foundRefs.find(f1Path);
|
||||||
ASSERT_TRUE(it != foundRefs.end());
|
EXPECT_TRUE(it != foundRefs.end());
|
||||||
EXPECT_EQ(it->second.size(), 1);
|
EXPECT_EQ(it->second.size(), 1);
|
||||||
EXPECT_TRUE(it->second.count(path1));
|
EXPECT_TRUE(it->second.count(path1));
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +170,7 @@ TEST(references, scanForReferencesDeep)
|
||||||
{
|
{
|
||||||
CanonPath f2Path("/file2.txt");
|
CanonPath f2Path("/file2.txt");
|
||||||
auto it = foundRefs.find(f2Path);
|
auto it = foundRefs.find(f2Path);
|
||||||
ASSERT_TRUE(it != foundRefs.end());
|
EXPECT_TRUE(it != foundRefs.end());
|
||||||
EXPECT_EQ(it->second.size(), 2);
|
EXPECT_EQ(it->second.size(), 2);
|
||||||
EXPECT_TRUE(it->second.count(path2));
|
EXPECT_TRUE(it->second.count(path2));
|
||||||
EXPECT_TRUE(it->second.count(path3));
|
EXPECT_TRUE(it->second.count(path3));
|
||||||
|
|
@ -186,7 +186,7 @@ TEST(references, scanForReferencesDeep)
|
||||||
{
|
{
|
||||||
CanonPath f4Path("/subdir/file4.txt");
|
CanonPath f4Path("/subdir/file4.txt");
|
||||||
auto it = foundRefs.find(f4Path);
|
auto it = foundRefs.find(f4Path);
|
||||||
ASSERT_TRUE(it != foundRefs.end());
|
EXPECT_TRUE(it != foundRefs.end());
|
||||||
EXPECT_EQ(it->second.size(), 1);
|
EXPECT_EQ(it->second.size(), 1);
|
||||||
EXPECT_TRUE(it->second.count(path1));
|
EXPECT_TRUE(it->second.count(path1));
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +195,7 @@ TEST(references, scanForReferencesDeep)
|
||||||
{
|
{
|
||||||
CanonPath linkPath("/link1");
|
CanonPath linkPath("/link1");
|
||||||
auto it = foundRefs.find(linkPath);
|
auto it = foundRefs.find(linkPath);
|
||||||
ASSERT_TRUE(it != foundRefs.end());
|
EXPECT_TRUE(it != foundRefs.end());
|
||||||
EXPECT_EQ(it->second.size(), 1);
|
EXPECT_EQ(it->second.size(), 1);
|
||||||
EXPECT_TRUE(it->second.count(path2));
|
EXPECT_TRUE(it->second.count(path2));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
#include "nix/cmd/command.hh"
|
#include "nix/cmd/command.hh"
|
||||||
#include "nix/store/store-api.hh"
|
#include "nix/store/store-api.hh"
|
||||||
#include "nix/store/path-references.hh"
|
#include "nix/store/path-references.hh"
|
||||||
|
#include "nix/store/dependency-graph-impl.hh"
|
||||||
#include "nix/util/source-accessor.hh"
|
#include "nix/util/source-accessor.hh"
|
||||||
#include "nix/main/shared.hh"
|
#include "nix/main/shared.hh"
|
||||||
|
|
||||||
#include <queue>
|
#include <ranges>
|
||||||
|
|
||||||
using namespace nix;
|
using namespace nix;
|
||||||
|
|
||||||
|
|
@ -21,6 +22,63 @@ static std::string filterPrintable(const std::string & s)
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find and format hash references in scanned files with context.
|
||||||
|
*
|
||||||
|
* @param accessor Source accessor for the store path
|
||||||
|
* @param refPaths Store paths to search for
|
||||||
|
* @param dependencyPathHash Hash of the dependency (for coloring)
|
||||||
|
* @return Map from hash string to list of formatted hit strings showing context
|
||||||
|
*/
|
||||||
|
static std::map<std::string, Strings>
|
||||||
|
findHashContexts(SourceAccessor & accessor, const StorePathSet & refPaths, std::string_view dependencyPathHash)
|
||||||
|
{
|
||||||
|
std::map<std::string, Strings> hits;
|
||||||
|
|
||||||
|
auto getColour = [&](const std::string & hash) { return hash == dependencyPathHash ? ANSI_GREEN : ANSI_BLUE; };
|
||||||
|
|
||||||
|
scanForReferencesDeep(accessor, CanonPath::root, refPaths, [&](const FileRefScanResult & result) {
|
||||||
|
std::string p2 =
|
||||||
|
result.filePath.isRoot() ? std::string(result.filePath.abs()) : std::string(result.filePath.rel());
|
||||||
|
auto st = accessor.lstat(result.filePath);
|
||||||
|
|
||||||
|
if (st.type == SourceAccessor::Type::tRegular) {
|
||||||
|
auto contents = accessor.readFile(result.filePath);
|
||||||
|
|
||||||
|
// For each reference found in this file, extract context
|
||||||
|
for (const auto & foundRef : result.foundRefs) {
|
||||||
|
std::string hash(foundRef.hashPart());
|
||||||
|
auto pos = contents.find(hash);
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
size_t margin = 32;
|
||||||
|
auto pos2 = pos >= margin ? pos - margin : 0;
|
||||||
|
hits[hash].emplace_back(
|
||||||
|
fmt("%s: …%s…",
|
||||||
|
p2,
|
||||||
|
hilite(
|
||||||
|
filterPrintable(std::string(contents, pos2, pos - pos2 + hash.size() + margin)),
|
||||||
|
pos - pos2,
|
||||||
|
StorePath::HashLen,
|
||||||
|
getColour(hash))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (st.type == SourceAccessor::Type::tSymlink) {
|
||||||
|
auto target = accessor.readLink(result.filePath);
|
||||||
|
|
||||||
|
// For each reference found in this symlink, show it
|
||||||
|
for (const auto & foundRef : result.foundRefs) {
|
||||||
|
std::string hash(foundRef.hashPart());
|
||||||
|
auto pos = target.find(hash);
|
||||||
|
if (pos != std::string::npos)
|
||||||
|
hits[hash].emplace_back(
|
||||||
|
fmt("%s -> %s", p2, hilite(target, pos, StorePath::HashLen, getColour(hash))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions
|
struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions
|
||||||
{
|
{
|
||||||
std::string _package, _dependency;
|
std::string _package, _dependency;
|
||||||
|
|
@ -109,187 +167,91 @@ struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions
|
||||||
auto dependencyPath = *optDependencyPath;
|
auto dependencyPath = *optDependencyPath;
|
||||||
auto dependencyPathHash = dependencyPath.hashPart();
|
auto dependencyPathHash = dependencyPath.hashPart();
|
||||||
|
|
||||||
auto const inf = std::numeric_limits<size_t>::max();
|
// Build dependency graph from closure using store metadata
|
||||||
|
StorePathGraph depGraph(*store, closure);
|
||||||
struct Node
|
|
||||||
{
|
|
||||||
StorePath path;
|
|
||||||
StorePathSet refs;
|
|
||||||
StorePathSet rrefs;
|
|
||||||
size_t dist = inf;
|
|
||||||
Node * prev = nullptr;
|
|
||||||
bool queued = false;
|
|
||||||
bool visited = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::map<StorePath, Node> graph;
|
|
||||||
|
|
||||||
for (auto & path : closure)
|
|
||||||
graph.emplace(
|
|
||||||
path,
|
|
||||||
Node{
|
|
||||||
.path = path,
|
|
||||||
.refs = store->queryPathInfo(path)->references,
|
|
||||||
.dist = path == dependencyPath ? 0 : inf});
|
|
||||||
|
|
||||||
// Transpose the graph.
|
|
||||||
for (auto & node : graph)
|
|
||||||
for (auto & ref : node.second.refs)
|
|
||||||
graph.find(ref)->second.rrefs.insert(node.first);
|
|
||||||
|
|
||||||
/* Run Dijkstra's shortest path algorithm to get the distance
|
|
||||||
of every path in the closure to 'dependency'. */
|
|
||||||
std::priority_queue<Node *> queue;
|
|
||||||
|
|
||||||
queue.push(&graph.at(dependencyPath));
|
|
||||||
|
|
||||||
while (!queue.empty()) {
|
|
||||||
auto & node = *queue.top();
|
|
||||||
queue.pop();
|
|
||||||
|
|
||||||
for (auto & rref : node.rrefs) {
|
|
||||||
auto & node2 = graph.at(rref);
|
|
||||||
auto dist = node.dist + 1;
|
|
||||||
if (dist < node2.dist) {
|
|
||||||
node2.dist = dist;
|
|
||||||
node2.prev = &node;
|
|
||||||
if (!node2.queued) {
|
|
||||||
node2.queued = true;
|
|
||||||
queue.push(&node2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print the subgraph of nodes that have 'dependency' in their
|
/* Print the subgraph of nodes that have 'dependency' in their
|
||||||
closure (i.e., that have a non-infinite distance to
|
closure (i.e., that have a non-infinite distance to
|
||||||
'dependency'). Print every edge on a path between `package`
|
'dependency'). Print every edge on a path between `package`
|
||||||
and `dependency`. */
|
and `dependency`. */
|
||||||
std::function<void(Node &, const std::string &, const std::string &)> printNode;
|
|
||||||
|
|
||||||
struct BailOut
|
|
||||||
{};
|
|
||||||
|
|
||||||
printNode = [&](Node & node, const std::string & firstPad, const std::string & tailPad) {
|
|
||||||
assert(node.dist != inf);
|
|
||||||
if (precise) {
|
|
||||||
logger->cout(
|
|
||||||
"%s%s%s%s" ANSI_NORMAL,
|
|
||||||
firstPad,
|
|
||||||
node.visited ? "\e[38;5;244m" : "",
|
|
||||||
firstPad != "" ? "→ " : "",
|
|
||||||
store->printStorePath(node.path));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.path == dependencyPath && !all && packagePath != dependencyPath)
|
|
||||||
throw BailOut();
|
|
||||||
|
|
||||||
if (node.visited)
|
|
||||||
return;
|
|
||||||
if (precise)
|
|
||||||
node.visited = true;
|
|
||||||
|
|
||||||
/* Sort the references by distance to `dependency` to
|
|
||||||
ensure that the shortest path is printed first. */
|
|
||||||
std::multimap<size_t, Node *> refs;
|
|
||||||
StorePathSet refPaths;
|
|
||||||
|
|
||||||
for (auto & ref : node.refs) {
|
|
||||||
if (ref == node.path && packagePath != dependencyPath)
|
|
||||||
continue;
|
|
||||||
auto & node2 = graph.at(ref);
|
|
||||||
if (node2.dist == inf)
|
|
||||||
continue;
|
|
||||||
refs.emplace(node2.dist, &node2);
|
|
||||||
refPaths.insert(node2.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For each reference, find the files and symlinks that
|
|
||||||
contain the reference. */
|
|
||||||
std::map<std::string, Strings> hits;
|
|
||||||
|
|
||||||
auto accessor = store->requireStoreObjectAccessor(node.path);
|
|
||||||
|
|
||||||
auto getColour = [&](const std::string & hash) {
|
|
||||||
return hash == dependencyPathHash ? ANSI_GREEN : ANSI_BLUE;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (precise) {
|
|
||||||
// Use scanForReferencesDeep to find files containing references
|
|
||||||
scanForReferencesDeep(*accessor, CanonPath::root, refPaths, [&](FileRefScanResult result) {
|
|
||||||
auto p2 = result.filePath.isRoot() ? result.filePath.abs() : result.filePath.rel();
|
|
||||||
auto st = accessor->lstat(result.filePath);
|
|
||||||
|
|
||||||
if (st.type == SourceAccessor::Type::tRegular) {
|
|
||||||
auto contents = accessor->readFile(result.filePath);
|
|
||||||
|
|
||||||
// For each reference found in this file, extract context
|
|
||||||
for (auto & foundRef : result.foundRefs) {
|
|
||||||
std::string hash(foundRef.hashPart());
|
|
||||||
auto pos = contents.find(hash);
|
|
||||||
if (pos != std::string::npos) {
|
|
||||||
size_t margin = 32;
|
|
||||||
auto pos2 = pos >= margin ? pos - margin : 0;
|
|
||||||
hits[hash].emplace_back(fmt(
|
|
||||||
"%s: …%s…",
|
|
||||||
p2,
|
|
||||||
hilite(
|
|
||||||
filterPrintable(std::string(contents, pos2, pos - pos2 + hash.size() + margin)),
|
|
||||||
pos - pos2,
|
|
||||||
StorePath::HashLen,
|
|
||||||
getColour(hash))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (st.type == SourceAccessor::Type::tSymlink) {
|
|
||||||
auto target = accessor->readLink(result.filePath);
|
|
||||||
|
|
||||||
// For each reference found in this symlink, show it
|
|
||||||
for (auto & foundRef : result.foundRefs) {
|
|
||||||
std::string hash(foundRef.hashPart());
|
|
||||||
auto pos = target.find(hash);
|
|
||||||
if (pos != std::string::npos)
|
|
||||||
hits[hash].emplace_back(
|
|
||||||
fmt("%s -> %s", p2, hilite(target, pos, StorePath::HashLen, getColour(hash))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto & ref : refs) {
|
|
||||||
std::string hash(ref.second->path.hashPart());
|
|
||||||
|
|
||||||
bool last = all ? ref == *refs.rbegin() : true;
|
|
||||||
|
|
||||||
for (auto & hit : hits[hash]) {
|
|
||||||
bool first = hit == *hits[hash].begin();
|
|
||||||
logger->cout(
|
|
||||||
"%s%s%s", tailPad, (first ? (last ? treeLast : treeConn) : (last ? treeNull : treeLine)), hit);
|
|
||||||
if (!all)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!precise) {
|
|
||||||
logger->cout(
|
|
||||||
"%s%s%s%s" ANSI_NORMAL,
|
|
||||||
firstPad,
|
|
||||||
ref.second->visited ? "\e[38;5;244m" : "",
|
|
||||||
last ? treeLast : treeConn,
|
|
||||||
store->printStorePath(ref.second->path));
|
|
||||||
node.visited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
printNode(*ref.second, tailPad + (last ? treeNull : treeLine), tailPad + (last ? treeNull : treeLine));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
RunPager pager;
|
RunPager pager;
|
||||||
try {
|
|
||||||
if (!precise) {
|
if (!precise) {
|
||||||
logger->cout("%s", store->printStorePath(graph.at(packagePath).path));
|
logger->cout("%s", store->printStorePath(packagePath));
|
||||||
}
|
|
||||||
printNode(graph.at(packagePath), "", "");
|
|
||||||
} catch (BailOut &) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::set<StorePath> visited;
|
||||||
|
std::vector<std::string> padStack = {""};
|
||||||
|
|
||||||
|
depGraph.dfsFromTarget(
|
||||||
|
packagePath,
|
||||||
|
dependencyPath,
|
||||||
|
// Visit node callback
|
||||||
|
[&](const StorePath & node, size_t depth) -> bool {
|
||||||
|
std::string currentPad = padStack[depth];
|
||||||
|
|
||||||
|
if (precise) {
|
||||||
|
logger->cout(
|
||||||
|
"%s%s%s%s" ANSI_NORMAL,
|
||||||
|
currentPad,
|
||||||
|
visited.contains(node) ? "\e[38;5;244m" : "",
|
||||||
|
currentPad != "" ? "→ " : "",
|
||||||
|
store->printStorePath(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visited.contains(node)) {
|
||||||
|
return false; // Skip subtree
|
||||||
|
}
|
||||||
|
if (precise) {
|
||||||
|
visited.insert(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Continue with this node's children
|
||||||
|
},
|
||||||
|
// Visit edge callback
|
||||||
|
[&](const StorePath & from, const StorePath & to, bool isLast, size_t depth) {
|
||||||
|
std::string tailPad = padStack[depth];
|
||||||
|
|
||||||
|
// In non-all mode, we only traverse one path, so everything is "last"
|
||||||
|
bool effectivelyLast = !all || isLast;
|
||||||
|
|
||||||
|
if (precise) {
|
||||||
|
auto accessor = store->requireStoreObjectAccessor(from);
|
||||||
|
auto hits = findHashContexts(*accessor, {to}, dependencyPathHash);
|
||||||
|
std::string hash(to.hashPart());
|
||||||
|
auto & hashHits = hits[hash];
|
||||||
|
|
||||||
|
for (auto & hit : hashHits) {
|
||||||
|
bool first = hit == *hashHits.begin();
|
||||||
|
logger->cout(
|
||||||
|
"%s%s%s",
|
||||||
|
tailPad,
|
||||||
|
(first ? (effectivelyLast ? treeLast : treeConn) : (effectivelyLast ? treeNull : treeLine)),
|
||||||
|
hit);
|
||||||
|
if (!all)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::string currentPad = padStack[depth];
|
||||||
|
logger->cout(
|
||||||
|
"%s%s%s%s" ANSI_NORMAL,
|
||||||
|
currentPad,
|
||||||
|
visited.contains(to) ? "\e[38;5;244m" : "",
|
||||||
|
effectivelyLast ? treeLast : treeConn,
|
||||||
|
store->printStorePath(to));
|
||||||
|
visited.insert(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update padding for next level
|
||||||
|
if (padStack.size() == depth + 1) {
|
||||||
|
padStack.push_back(tailPad + (effectivelyLast ? treeNull : treeLine));
|
||||||
|
} else {
|
||||||
|
padStack[depth + 1] = tailPad + (effectivelyLast ? treeNull : treeLine);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Stop condition
|
||||||
|
[&](const StorePath & node) { return node == dependencyPath && !all && packagePath != dependencyPath; });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue