From 8226398cbccbb2d8240db53de9f92eb41b77a6b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:44:49 +0000 Subject: [PATCH 01/10] Initial plan From 70d604c00ad10c12ccf035f73a327586809efab8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:50:20 +0000 Subject: [PATCH 02/10] Add unsorted option to ls() to improve performance Co-authored-by: mxcl <58962+mxcl@users.noreply.github.com> --- Sources/Path+ls.swift | 43 +++++++++++++++++++--- Tests/PathTests/PathTests+ls().swift | 53 +++++++++++++++++++++++++++ Tests/PathTests/XCTestManifests.swift | 2 + 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/Sources/Path+ls.swift b/Sources/Path+ls.swift index 30462ae..8a24f2a 100644 --- a/Sources/Path+ls.swift +++ b/Sources/Path+ls.swift @@ -165,23 +165,47 @@ public extension Pathish { //MARK: Directory Listing /** - Same as the `ls` command ∴ output is ”shallow” and unsorted. + Same as the `ls` command ∴ output is "shallow". + - Note: as per `ls`, by default we do *not* return hidden files. Specify `.a` for hidden files. - Parameter options: Configure the listing. - Important: On Linux the listing is always `ls -a` - - WARNING: we actually sort the output :( sorry. Will fix in a major version bump. + - WARNING: ⚠️ **PERFORMANCE**: By default, output is sorted using locale-specific sorting which can be **VERY EXPENSIVE** + for large directories (0.5+ seconds). For better performance, use `.unsorted` or `.a_unsorted` options. + - Note: Sorting will be removed by default in the next major version bump. */ func ls(_ options: ListDirectoryOptions? = nil) -> [Path] { guard let urls = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) else { fputs("warning: could not list: \(self)\n", stderr) return [] } - return urls.compactMap { url in + + let shouldSort: Bool + let includeHidden: Bool + + switch options { + case .a: + shouldSort = true + includeHidden = true + case .a_unsorted: + shouldSort = false + includeHidden = true + case .unsorted: + shouldSort = false + includeHidden = false + case .none: + shouldSort = true + includeHidden = false + } + + let paths = urls.compactMap { url -> Path? in guard let path = Path(url.path) else { return nil } - if options != .a, path.basename().hasPrefix(".") { return nil } - // ^^ we don’t use the Foundation `skipHiddenFiles` because it considers weird things hidden and we are mirroring `ls` + if !includeHidden, path.basename().hasPrefix(".") { return nil } + // ^^ we don't use the Foundation `skipHiddenFiles` because it considers weird things hidden and we are mirroring `ls` return path - }.sorted() + } + + return shouldSort ? paths.sorted() : paths } /// Recursively find files under this path. If the path is a file, no files will be found. @@ -217,4 +241,11 @@ public extension Array where Element == Path { public enum ListDirectoryOptions { /// Lists hidden files also case a + /// Lists hidden files also without sorting + case a_unsorted + /// Disables sorting for better performance + /// - WARNING: Sorting is locale-specific and can be expensive for large directories. + /// Use this option when you don't need sorted output and performance is critical. + /// - Note: In the next major version, sorting will be removed by default. + case unsorted } diff --git a/Tests/PathTests/PathTests+ls().swift b/Tests/PathTests/PathTests+ls().swift index 58072d3..b33b6e3 100644 --- a/Tests/PathTests/PathTests+ls().swift +++ b/Tests/PathTests/PathTests+ls().swift @@ -248,4 +248,57 @@ extension PathTests { XCTAssertNil(tmpdir.a.find().next()) } } + + func testLsUnsortedOption() throws { + try Path.mktemp { tmpdir in + // Create files with names that would be sorted differently + try tmpdir.join("zebra.txt").touch() + try tmpdir.join("apple.txt").touch() + try tmpdir.join("banana.txt").touch() + + // Test default (sorted) behavior + let sortedResults = tmpdir.ls() + XCTAssertEqual(sortedResults.count, 3) + XCTAssertEqual(sortedResults[0].basename(), "apple.txt") + XCTAssertEqual(sortedResults[1].basename(), "banana.txt") + XCTAssertEqual(sortedResults[2].basename(), "zebra.txt") + + // Test unsorted behavior - just verify we get all files, order doesn't matter + let unsortedResults = tmpdir.ls(.unsorted) + XCTAssertEqual(unsortedResults.count, 3) + XCTAssertTrue(unsortedResults.contains(tmpdir.join("apple.txt"))) + XCTAssertTrue(unsortedResults.contains(tmpdir.join("banana.txt"))) + XCTAssertTrue(unsortedResults.contains(tmpdir.join("zebra.txt"))) + } + } + + func testLsUnsortedWithHidden() throws { + try Path.mktemp { tmpdir in + // Create regular and hidden files + try tmpdir.join("visible.txt").touch() + try tmpdir.join(".hidden.txt").touch() + try tmpdir.join("another.txt").touch() + + // Test .a (sorted with hidden) + let sortedWithHidden = tmpdir.ls(.a) + XCTAssertEqual(sortedWithHidden.count, 3) + XCTAssertEqual(sortedWithHidden[0].basename(), ".hidden.txt") + XCTAssertEqual(sortedWithHidden[1].basename(), "another.txt") + XCTAssertEqual(sortedWithHidden[2].basename(), "visible.txt") + + // Test .a_unsorted (unsorted with hidden) + let unsortedWithHidden = tmpdir.ls(.a_unsorted) + XCTAssertEqual(unsortedWithHidden.count, 3) + XCTAssertTrue(unsortedWithHidden.contains(tmpdir.join("visible.txt"))) + XCTAssertTrue(unsortedWithHidden.contains(tmpdir.join(".hidden.txt"))) + XCTAssertTrue(unsortedWithHidden.contains(tmpdir.join("another.txt"))) + + // Test .unsorted (unsorted without hidden) + let unsortedNoHidden = tmpdir.ls(.unsorted) + XCTAssertEqual(unsortedNoHidden.count, 2) + XCTAssertTrue(unsortedNoHidden.contains(tmpdir.join("visible.txt"))) + XCTAssertTrue(unsortedNoHidden.contains(tmpdir.join("another.txt"))) + XCTAssertFalse(unsortedNoHidden.contains(tmpdir.join(".hidden.txt"))) + } + } } diff --git a/Tests/PathTests/XCTestManifests.swift b/Tests/PathTests/XCTestManifests.swift index 74d1eb5..cce6468 100644 --- a/Tests/PathTests/XCTestManifests.swift +++ b/Tests/PathTests/XCTestManifests.swift @@ -42,6 +42,8 @@ extension PathTests { ("testKind", testKind), ("testLock", testLock), ("testLsOnNonexistentDirectoryReturnsEmptyArray", testLsOnNonexistentDirectoryReturnsEmptyArray), + ("testLsUnsortedOption", testLsUnsortedOption), + ("testLsUnsortedWithHidden", testLsUnsortedWithHidden), ("testMkpathIfExists", testMkpathIfExists), ("testMktemp", testMktemp), ("testMoveInto", testMoveInto), From cfdb728b4eee11805ec76091259be5038bd8df04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:51:26 +0000 Subject: [PATCH 03/10] Fix naming convention: a_unsorted -> aUnsorted Co-authored-by: mxcl <58962+mxcl@users.noreply.github.com> --- Sources/Path+ls.swift | 6 +++--- Tests/PathTests/PathTests+ls().swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Path+ls.swift b/Sources/Path+ls.swift index 8a24f2a..8059407 100644 --- a/Sources/Path+ls.swift +++ b/Sources/Path+ls.swift @@ -171,7 +171,7 @@ public extension Pathish { - Parameter options: Configure the listing. - Important: On Linux the listing is always `ls -a` - WARNING: ⚠️ **PERFORMANCE**: By default, output is sorted using locale-specific sorting which can be **VERY EXPENSIVE** - for large directories (0.5+ seconds). For better performance, use `.unsorted` or `.a_unsorted` options. + for large directories (0.5+ seconds). For better performance, use `.unsorted` or `.aUnsorted` options. - Note: Sorting will be removed by default in the next major version bump. */ func ls(_ options: ListDirectoryOptions? = nil) -> [Path] { @@ -187,7 +187,7 @@ public extension Pathish { case .a: shouldSort = true includeHidden = true - case .a_unsorted: + case .aUnsorted: shouldSort = false includeHidden = true case .unsorted: @@ -242,7 +242,7 @@ public enum ListDirectoryOptions { /// Lists hidden files also case a /// Lists hidden files also without sorting - case a_unsorted + case aUnsorted /// Disables sorting for better performance /// - WARNING: Sorting is locale-specific and can be expensive for large directories. /// Use this option when you don't need sorted output and performance is critical. diff --git a/Tests/PathTests/PathTests+ls().swift b/Tests/PathTests/PathTests+ls().swift index b33b6e3..6d5ac3f 100644 --- a/Tests/PathTests/PathTests+ls().swift +++ b/Tests/PathTests/PathTests+ls().swift @@ -286,8 +286,8 @@ extension PathTests { XCTAssertEqual(sortedWithHidden[1].basename(), "another.txt") XCTAssertEqual(sortedWithHidden[2].basename(), "visible.txt") - // Test .a_unsorted (unsorted with hidden) - let unsortedWithHidden = tmpdir.ls(.a_unsorted) + // Test .aUnsorted (unsorted with hidden) + let unsortedWithHidden = tmpdir.ls(.aUnsorted) XCTAssertEqual(unsortedWithHidden.count, 3) XCTAssertTrue(unsortedWithHidden.contains(tmpdir.join("visible.txt"))) XCTAssertTrue(unsortedWithHidden.contains(tmpdir.join(".hidden.txt"))) From 0e66a27a69be052e2c1ab884e4c25e5a1c04d7cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:52:06 +0000 Subject: [PATCH 04/10] Improve documentation placement for enum warnings Co-authored-by: mxcl <58962+mxcl@users.noreply.github.com> --- Sources/Path+ls.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/Path+ls.swift b/Sources/Path+ls.swift index 8059407..fcd868f 100644 --- a/Sources/Path+ls.swift +++ b/Sources/Path+ls.swift @@ -238,14 +238,15 @@ public extension Array where Element == Path { } /// Options for `Path.ls(_:)` +/// +/// - WARNING: Sorting is locale-specific and can be expensive for large directories (0.5+ seconds). +/// Use `.unsorted` or `.aUnsorted` when you don't need sorted output and performance is critical. +/// - Note: In the next major version, sorting will be removed by default. public enum ListDirectoryOptions { - /// Lists hidden files also + /// Lists hidden files also (sorted) case a - /// Lists hidden files also without sorting + /// Lists hidden files also (unsorted for better performance) case aUnsorted /// Disables sorting for better performance - /// - WARNING: Sorting is locale-specific and can be expensive for large directories. - /// Use this option when you don't need sorted output and performance is critical. - /// - Note: In the next major version, sorting will be removed by default. case unsorted } From c1ccda4f7a24c835aaee607defe2b9296753aa6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:19:48 +0000 Subject: [PATCH 05/10] Remove restrictive paths filters to enable tests in CI Co-authored-by: mxcl <58962+mxcl@users.noreply.github.com> --- .github/workflows/checks.yml | 3 --- .github/workflows/ci.yml | 3 --- 2 files changed, 6 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index da4236d..741b4f8 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -2,9 +2,6 @@ on: push: branches: - master - paths: - - '**/*.swift' - - .github/workflows/checks.yml jobs: smoke: runs-on: ubuntu-latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce494ba..2d52aba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,6 @@ name: CI on: pull_request: - paths: - - '**/*.swift' - - .github/workflows/ci.yml schedule: - cron: '3 3 * * 5' # 3:03 AM, every Friday From 58b7dc9b79746df53613fe07092b14a2739b73c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:44:49 +0000 Subject: [PATCH 06/10] Initial plan From 987cfb4416ecbd410ed01f5a4626feeec9cf56c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:53:26 +0000 Subject: [PATCH 07/10] Fix CI: Replace mxcl/get-swift-version with inline script to avoid glibc issues Co-authored-by: mxcl <58962+mxcl@users.noreply.github.com> --- .github/workflows/ci.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d52aba..b526009 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,15 +52,20 @@ jobs: container: image: ${{ matrix.swift }} steps: - - uses: mxcl/get-swift-version@v1 - id: swift - - uses: actions/checkout@v2 - run: useradd -ms /bin/bash mxcl - run: chown -R mxcl . # ^^ we need to be a normal user and not root for the tests to be valid + - name: Get Swift Version + id: swift + run: | + version=$(swift --version | head -n1 | sed 's/.*Swift version \([0-9.]*\).*/\1/') + marketing_version=$(echo $version | cut -d. -f1) + echo "marketing-version=$marketing_version" >> $GITHUB_OUTPUT + echo "Swift version: $version (marketing: $marketing_version)" + - run: echo ARGS=--enable-code-coverage >> $GITHUB_ENV if: ${{ steps.swift.outputs.marketing-version > 5 }} From 369e2a7e8e7eb2c8c9aff37ea1e153ddb2b47b73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:54:14 +0000 Subject: [PATCH 08/10] Update CI matrix: use current macOS runners and extend Linux Swift versions Co-authored-by: mxcl <58962+mxcl@users.noreply.github.com> --- .github/workflows/ci.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b526009..9b759d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ concurrency: jobs: verify-linuxmain: - runs-on: macos-10.15 + runs-on: macos-13 steps: - uses: actions/checkout@v2 - run: swift test --generate-linuxmain @@ -21,8 +21,9 @@ jobs: strategy: matrix: os: - - macos-10.15 - - macos-11 + - macos-13 + - macos-14 + - macos-15 platform: - iOS - tvOS @@ -48,7 +49,12 @@ jobs: - swift:5.2 - swift:5.3 - swift:5.4 - - swiftlang/swift:nightly-5.5 + - swift:5.5 + - swift:5.6 + - swift:5.7 + - swift:5.8 + - swift:5.9 + - swift:5.10 container: image: ${{ matrix.swift }} steps: From d1563301f466c8d944e1871626e4cec427670641 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:18:19 +0000 Subject: [PATCH 09/10] Fix CI: update to actions/checkout@v4, remove macos-15, separate legacy Swift versions Co-authored-by: mxcl <58962+mxcl@users.noreply.github.com> --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b759d6..eb5f0d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: verify-linuxmain: runs-on: macos-13 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: swift test --generate-linuxmain - run: git diff --exit-code @@ -23,14 +23,13 @@ jobs: os: - macos-13 - macos-14 - - macos-15 platform: - iOS - tvOS - macOS - watchOS steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: mxcl/xcodebuild@v1 with: platform: ${{ matrix.platform }} @@ -43,10 +42,6 @@ jobs: strategy: matrix: swift: - - swift:4.2 - - swift:5.0 - - swift:5.1 - - swift:5.2 - swift:5.3 - swift:5.4 - swift:5.5 @@ -58,7 +53,7 @@ jobs: container: image: ${{ matrix.swift }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: useradd -ms /bin/bash mxcl - run: chown -R mxcl . @@ -93,3 +88,25 @@ jobs: if: ${{ steps.swift.outputs.marketing-version > 5 }} with: file: ./info.lcov + + linux-legacy: + runs-on: ubuntu-latest + strategy: + matrix: + swift: + - swift:4.2 + - swift:5.0 + - swift:5.1 + - swift:5.2 + container: + image: ${{ matrix.swift }} + steps: + - uses: actions/checkout@v3 + # Using v3 instead of v4 because v4 requires node20 which needs glibc 2.28+ + # These old Swift containers (Ubuntu 18.04) only have glibc 2.27 + + - run: useradd -ms /bin/bash mxcl + - run: chown -R mxcl . + # ^^ we need to be a normal user and not root for the tests to be valid + + - run: su mxcl -c "swift test --parallel" From be3f1c5b5ca40e52242773d846734d23cd51edd4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:50:30 +0000 Subject: [PATCH 10/10] Fix CI: manual git checkout for legacy Swift, move 5.3 to legacy, remove macOS 14 Co-authored-by: mxcl <58962+mxcl@users.noreply.github.com> --- .github/workflows/ci.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb5f0d6..387b098 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,12 +17,9 @@ jobs: - run: git diff --exit-code apple: - runs-on: ${{ matrix.os }} + runs-on: macos-13 strategy: matrix: - os: - - macos-13 - - macos-14 platform: - iOS - tvOS @@ -42,7 +39,6 @@ jobs: strategy: matrix: swift: - - swift:5.3 - swift:5.4 - swift:5.5 - swift:5.6 @@ -98,11 +94,23 @@ jobs: - swift:5.0 - swift:5.1 - swift:5.2 + - swift:5.3 container: image: ${{ matrix.swift }} steps: - - uses: actions/checkout@v3 - # Using v3 instead of v4 because v4 requires node20 which needs glibc 2.28+ + - name: Checkout code + run: | + apt-get update && apt-get install -y git + git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} https://github.com/$GITHUB_REPOSITORY .git-tmp || \ + git clone --depth 1 https://github.com/$GITHUB_REPOSITORY .git-tmp + cd .git-tmp + git fetch origin $GITHUB_SHA + git checkout $GITHUB_SHA + cd .. + mv .git-tmp/* . + mv .git-tmp/.git . + rm -rf .git-tmp + # Manual checkout because actions/checkout@v3 and v4 both require node20 with glibc 2.28+ # These old Swift containers (Ubuntu 18.04) only have glibc 2.27 - run: useradd -ms /bin/bash mxcl