Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions xff/engine/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ cc_library(
visibility = ["//xff:__subpackages__"],
deps = [
"//xff/vfs:vfs_cc",
"@abseil-cpp//absl/algorithm:container",
"@abseil-cpp//absl/functional:function_ref",
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
Expand Down
16 changes: 16 additions & 0 deletions xff/engine/run.cc
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ SymlinkMode ResolveSymlinkMode(const std::vector<std::string>& globals) {
return mode;
}

// xff --sort[=name|none]: order siblings by name within each directory for
// deterministic output, or keep readdir order (none / absent). Bare --sort means
// --sort=name. Leading global, last occurrence wins.
SortOrder ResolveSort(const std::vector<std::string>& globals) {
SortOrder sort = SortOrder::kNone;
for (const std::string& global : globals) {
if (global == "--sort" || global == "--sort=name") {
sort = SortOrder::kName;
} else if (global == "--sort=none") {
sort = SortOrder::kNone;
}
}
return sort;
}

// xff's modern output selector (leading globals, last wins, default plain):
// --format=plain|nul|jsonl, with -0 a shorthand for NUL. find's -print/-print0
// keep their fixed formats; this drives only the implicit (default) print.
Expand Down Expand Up @@ -368,6 +383,7 @@ int RunFind(const parser::Command& command, const vfs::FileSystem& fs, EmitFn em
}
WalkOptions options;
options.symlinks = ResolveSymlinkMode(command.globals);
options.sort = ResolveSort(command.globals);
const render::Format format = ResolveFormat(command.globals);
const std::optional<std::string> tmpl = ResolveTemplate(command.globals);
// A -capture whose {capture.NAME} is never referenced ran a subprocess for
Expand Down
20 changes: 20 additions & 0 deletions xff/engine/run_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ TEST_F(RunTest, NoExpressionPrintsEverything) {
EXPECT_THAT(last_errors_, 0);
}

TEST_F(RunTest, SortNameVisitsSiblingsInDeterministicOrder) {
// --sort=name orders each directory's entries by name, so the whole walk is
// deterministic: root first, then a.txt < b.md < sub, then sub/c.txt. ElementsAre
// (not UnorderedElementsAre) asserts the exact sequence.
const auto command = parser::Parse({"--sort", root_.string()});
ASSERT_THAT(command, IsOk());
std::vector<std::string> records;
RunFind(
*command, fs_,
[&](std::string_view record) {
std::string text(record);
if (!text.empty() && text.back() == '\n') {
text.pop_back();
}
records.push_back(std::move(text));
},
[](std::string_view, absl::Status) {});
EXPECT_THAT(records, ElementsAre(root_.string(), Path("a.txt"), Path("b.md"), Path("sub"), Path("sub/c.txt")));
}

TEST_F(RunTest, NameGlobImplicitPrint) {
EXPECT_THAT(RunExpr({"-name", "*.txt"}), UnorderedElementsAre(Path("a.txt"), Path("sub/c.txt")));
}
Expand Down
8 changes: 7 additions & 1 deletion xff/engine/walk.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <utility>
#include <vector>

#include "absl/algorithm/container.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/types/span.h"
Expand Down Expand Up @@ -114,11 +115,16 @@ class Walker {
}

void Descend(const std::string& dir, int depth) {
const absl::StatusOr<std::vector<vfs::Entry>> children = fs_.ReadDir(dir);
absl::StatusOr<std::vector<vfs::Entry>> children = fs_.ReadDir(dir);
if (!children.ok()) {
on_error_(dir, children.status());
return;
}
// --sort=name: order siblings by path before visiting. They share `dir` as a
// prefix, so path order is name order; the result is a deterministic walk.
if (options_.sort == SortOrder::kName) {
absl::c_sort(*children, [](const vfs::Entry& a, const vfs::Entry& b) { return a.path < b.path; });
}
for (const vfs::Entry& child : *children) {
if (stopped_) {
return;
Expand Down
7 changes: 7 additions & 0 deletions xff/engine/walk.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ namespace xff::engine {
// (`-L`). Following enables filesystem-loop detection.
enum class SymlinkMode { kNever, kRoots, kAll };

// Sibling ordering within each directory (xff's --sort). kNone keeps the
// filesystem's readdir order (find's default, fastest, non-reproducible); kName
// sorts each directory's entries by path so output is deterministic and diffable.
enum class SortOrder { kNone, kName };

// Traversal limits. Parallelism is layered on in a follow-up (design.md
// "Determinism").
struct WalkOptions {
Expand All @@ -52,6 +57,8 @@ struct WalkOptions {
// symlink is followed, its target's metadata is reported and a directory target
// is descended into, with loop detection.
SymlinkMode symlinks = SymlinkMode::kNever;
// Sibling ordering within each directory (xff `--sort`); kNone is readdir order.
SortOrder sort = SortOrder::kNone;
};

// One visited entry handed to the `Visitor`. `path`/`name` reference storage
Expand Down
Loading