From 6904097171ec878bdfacc34bd8c8f108592dedc0 Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Sat, 23 Dec 2023 12:50:47 +0000 Subject: [PATCH] feat: initial import --- .envrc | 1 + .gitignore | 12 + LICENSE.md | 21 + flake.lock | 187 ++++++++ flake.nix | 39 ++ go.mod | 35 ++ go.sum | 73 +++ gomod2nix.toml | 81 ++++ internal/cache/cache.go | 144 ++++++ internal/cache/types.go | 8 + internal/cli/cli.go | 29 ++ internal/cli/format.go | 155 ++++++ internal/format/config.go | 12 + internal/format/config_test.go | 122 +++++ internal/format/context.go | 36 ++ internal/format/format.go | 161 +++++++ internal/format/glob.go | 15 + internal/log/writer.go | 21 + main.go | 11 + nix/checks.nix | 5 + nix/default.nix | 10 + nix/devshell.nix | 58 +++ nix/nixpkgs.nix | 16 + nix/packages.nix | 42 ++ nix/treefmt.nix | 32 ++ test/echo.toml | 3 + test/examples/elm/elm.json | 22 + test/examples/elm/src/Main.elm | 31 ++ test/examples/go/go.mod | 3 + test/examples/go/main.go | 7 + test/examples/haskell-frontend/CHANGELOG.md | 5 + test/examples/haskell-frontend/Main.hs | 4 + test/examples/haskell-frontend/Setup.hs | 3 + .../haskell-frontend/haskell-frontend.cabal | 25 + test/examples/haskell/CHANGELOG.md | 5 + test/examples/haskell/Foo.hs | 4 + test/examples/haskell/Main.hs | 4 + test/examples/haskell/Nested/Foo.hs | 4 + test/examples/haskell/Setup.hs | 3 + test/examples/haskell/haskell.cabal | 25 + test/examples/haskell/treefmt.toml | 10 + test/examples/html/index.html | 10 + test/examples/html/scripts/.gitkeep | 0 test/examples/javascript/source/hello.js | 65 +++ test/examples/nix/sources.nix | 242 ++++++++++ test/examples/python/main.py | 12 + test/examples/python/requirements.txt | 1 + test/examples/python/virtualenv_proxy.py | 104 ++++ test/examples/ruby/bundler.rb | 452 ++++++++++++++++++ test/examples/rust/Cargo.toml | 9 + test/examples/rust/src/main.rs | 3 + test/examples/shell/foo.sh | 8 + test/examples/terraform/main.tf | 4 + test/examples/terraform/two.tf | 4 + test/treefmt.toml | 82 ++++ 55 files changed, 2480 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gomod2nix.toml create mode 100644 internal/cache/cache.go create mode 100644 internal/cache/types.go create mode 100644 internal/cli/cli.go create mode 100644 internal/cli/format.go create mode 100644 internal/format/config.go create mode 100644 internal/format/config_test.go create mode 100644 internal/format/context.go create mode 100644 internal/format/format.go create mode 100644 internal/format/glob.go create mode 100644 internal/log/writer.go create mode 100644 main.go create mode 100644 nix/checks.nix create mode 100644 nix/default.nix create mode 100644 nix/devshell.nix create mode 100644 nix/nixpkgs.nix create mode 100644 nix/packages.nix create mode 100644 nix/treefmt.nix create mode 100644 test/echo.toml create mode 100644 test/examples/elm/elm.json create mode 100644 test/examples/elm/src/Main.elm create mode 100644 test/examples/go/go.mod create mode 100644 test/examples/go/main.go create mode 100644 test/examples/haskell-frontend/CHANGELOG.md create mode 100644 test/examples/haskell-frontend/Main.hs create mode 100644 test/examples/haskell-frontend/Setup.hs create mode 100644 test/examples/haskell-frontend/haskell-frontend.cabal create mode 100644 test/examples/haskell/CHANGELOG.md create mode 100644 test/examples/haskell/Foo.hs create mode 100644 test/examples/haskell/Main.hs create mode 100644 test/examples/haskell/Nested/Foo.hs create mode 100644 test/examples/haskell/Setup.hs create mode 100644 test/examples/haskell/haskell.cabal create mode 100644 test/examples/haskell/treefmt.toml create mode 100644 test/examples/html/index.html create mode 100644 test/examples/html/scripts/.gitkeep create mode 100644 test/examples/javascript/source/hello.js create mode 100644 test/examples/nix/sources.nix create mode 100644 test/examples/python/main.py create mode 100644 test/examples/python/requirements.txt create mode 100644 test/examples/python/virtualenv_proxy.py create mode 100644 test/examples/ruby/bundler.rb create mode 100644 test/examples/rust/Cargo.toml create mode 100644 test/examples/rust/src/main.rs create mode 100755 test/examples/shell/foo.sh create mode 100644 test/examples/terraform/main.tf create mode 100644 test/examples/terraform/two.tf create mode 100644 test/treefmt.toml diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..631de79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# editors +.idea + +# nix +result* +repl-result-* + +# direnv +/.direnv + +# devshell +/.data diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fd2155d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Nits Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6a851c1 --- /dev/null +++ b/flake.lock @@ -0,0 +1,187 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "nixpkgs": ["nixpkgs"], + "systems": "systems" + }, + "locked": { + "lastModified": 1700815693, + "narHash": "sha256-JtKZEQUzosrCwDsLgm+g6aqbP1aseUl1334OShEAS3s=", + "owner": "numtide", + "repo": "devshell", + "rev": "7ad1c417c87e98e56dcef7ecd0e0a2f2e5669d51", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1698882062, + "narHash": "sha256-HkhafUayIqxXyHH1X8d9RDl1M2CkFgZLjKD3MzabiEo=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "8c9fa2545007b49a5db5f650ae91f227672c3877", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-root": { + "locked": { + "lastModified": 1692742795, + "narHash": "sha256-f+Y0YhVCIJ06LemO+3Xx00lIcqQxSKJHXT/yk1RTKxw=", + "owner": "srid", + "repo": "flake-root", + "rev": "d9a70d9c7a5fd7f3258ccf48da9335e9b47c3937", + "type": "github" + }, + "original": { + "owner": "srid", + "repo": "flake-root", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gomod2nix": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": ["nixpkgs"] + }, + "locked": { + "lastModified": 1699950847, + "narHash": "sha256-xN/yVtqHb7kimHA/WvQFrEG5WS38t0K+A/W+j/WhQWM=", + "owner": "nix-community", + "repo": "gomod2nix", + "rev": "05c993c9a5bd55a629cd45ed49951557b7e9c61a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "gomod2nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1702312524, + "narHash": "sha256-gkZJRDBUCpTPBvQk25G0B7vfbpEYM5s5OZqghkjZsnE=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "a9bf124c46ef298113270b1f84a164865987a91c", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1698611440, + "narHash": "sha256-jPjHjrerhYDy3q9+s5EAsuhyhuknNfowY6yt6pjn9pc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0cbe9f69c234a7700596e943bfae7ef27a31b735", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-parts": "flake-parts", + "flake-root": "flake-root", + "gomod2nix": "gomod2nix", + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": ["nixpkgs"] + }, + "locked": { + "lastModified": 1699786194, + "narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..8a4ad1a --- /dev/null +++ b/flake.nix @@ -0,0 +1,39 @@ +{ + description = "Treefmt"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + flake-parts.url = "github:hercules-ci/flake-parts"; + flake-root.url = "github:srid/flake-root"; + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + devshell = { + url = "github:numtide/devshell"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + gomod2nix = { + url = "github:nix-community/gomod2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs @ {flake-parts, ...}: + flake-parts.lib.mkFlake + { + inherit inputs; + } + { + imports = [ + ./nix + ]; + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5b427b7 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module github.com/numtide/treefmt + +go 1.21 + +require ( + github.com/BurntSushi/toml v1.3.2 + github.com/adrg/xdg v0.4.0 + github.com/alecthomas/kong v0.8.1 + github.com/charmbracelet/log v0.3.1 + github.com/gobwas/glob v0.2.3 + github.com/juju/errors v1.0.0 + github.com/stretchr/testify v1.8.4 + github.com/vmihailenco/msgpack/v5 v5.4.1 + github.com/ztrue/shutdown v0.1.1 + go.etcd.io/bbolt v1.3.8 + golang.org/x/sync v0.5.0 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v0.9.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sys v0.13.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8346b8b --- /dev/null +++ b/go.sum @@ -0,0 +1,73 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= +github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= +github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw= +github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= +github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/ztrue/shutdown v0.1.1 h1:GKR2ye2OSQlq1GNVE/s2NbrIMsFdmL+NdR6z6t1k+Tg= +github.com/ztrue/shutdown v0.1.1/go.mod h1:hcMWcM2SwIsQk7Wb49aYme4tX66x6iLzs07w1OYAQLw= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gomod2nix.toml b/gomod2nix.toml new file mode 100644 index 0000000..3dbee60 --- /dev/null +++ b/gomod2nix.toml @@ -0,0 +1,81 @@ +schema = 3 + +[mod] + [mod."github.com/BurntSushi/toml"] + version = "v1.3.2" + hash = "sha256-FIwyH67KryRWI9Bk4R8s1zFP0IgKR4L66wNQJYQZLeg=" + [mod."github.com/adrg/xdg"] + version = "v0.4.0" + hash = "sha256-zGjkdUQmrVqD6rMO9oDY+TeJCpuqnHyvkPCaXDlac/U=" + [mod."github.com/alecthomas/kong"] + version = "v0.8.1" + hash = "sha256-170mjSrLNC+0W1KhXltaa+YWYgt5gJQEcfssepcyh4E=" + [mod."github.com/aymanbagabas/go-osc52/v2"] + version = "v2.0.1" + hash = "sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg=" + [mod."github.com/charmbracelet/lipgloss"] + version = "v0.9.1" + hash = "sha256-AHbabOymgDRIXsMBgJHS25/GgBWT54oGbd15EBWKeZc=" + [mod."github.com/charmbracelet/log"] + version = "v0.3.1" + hash = "sha256-Er60POPID2eNrRZnBHxoI4yHn0mIKnXYftGKSslbXx0=" + [mod."github.com/davecgh/go-spew"] + version = "v1.1.1" + hash = "sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI=" + [mod."github.com/go-logfmt/logfmt"] + version = "v0.6.0" + hash = "sha256-RtIG2qARd5sT10WQ7F3LR8YJhS8exs+KiuUiVf75bWg=" + [mod."github.com/gobwas/glob"] + version = "v0.2.3" + hash = "sha256-hYHMUdwxVkMOjSKjR7UWO0D0juHdI4wL8JEy5plu/Jc=" + [mod."github.com/juju/errors"] + version = "v1.0.0" + hash = "sha256-9uZ0wNf44ilzLsvXqOsmFUpNOBFAVadj6+ZH8+QMDMk=" + [mod."github.com/lucasb-eyer/go-colorful"] + version = "v1.2.0" + hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE=" + [mod."github.com/mattn/go-isatty"] + version = "v0.0.18" + hash = "sha256-QpIn0DSggtBn2ocyj0RlXDKLK5F5KZG1/ogzrqBCjF8=" + [mod."github.com/mattn/go-runewidth"] + version = "v0.0.15" + hash = "sha256-WP39EU2UrQbByYfnwrkBDoKN7xzXsBssDq3pNryBGm0=" + [mod."github.com/muesli/reflow"] + version = "v0.3.0" + hash = "sha256-Pou2ybE9SFSZG6YfZLVV1Eyfm+X4FuVpDPLxhpn47Cc=" + [mod."github.com/muesli/termenv"] + version = "v0.15.2" + hash = "sha256-Eum/SpyytcNIchANPkG4bYGBgcezLgej7j/+6IhqoMU=" + [mod."github.com/pmezard/go-difflib"] + version = "v1.0.0" + hash = "sha256-/FtmHnaGjdvEIKAJtrUfEhV7EVo5A/eYrtdnUkuxLDA=" + [mod."github.com/rivo/uniseg"] + version = "v0.2.0" + hash = "sha256-GLj0jiGrT03Ept4V6FXCN1yeZ/b6PpS3MEXK6rYQ8Eg=" + [mod."github.com/stretchr/testify"] + version = "v1.8.4" + hash = "sha256-MoOmRzbz9QgiJ+OOBo5h5/LbilhJfRUryvzHJmXAWjo=" + [mod."github.com/vmihailenco/msgpack/v5"] + version = "v5.4.1" + hash = "sha256-pDplX6xU6UpNLcFbO1pRREW5vCnSPvSU+ojAwFDv3Hk=" + [mod."github.com/vmihailenco/tagparser/v2"] + version = "v2.0.0" + hash = "sha256-M9QyaKhSmmYwsJk7gkjtqu9PuiqZHSmTkous8VWkWY0=" + [mod."github.com/ztrue/shutdown"] + version = "v0.1.1" + hash = "sha256-+ygx5THHu9g+vBAn6b63tV35bvQGdRyto4pLhkontJI=" + [mod."go.etcd.io/bbolt"] + version = "v1.3.8" + hash = "sha256-ekKy8198B2GfPldHLYZnvNjID6x07dUPYKgFx84TgVs=" + [mod."golang.org/x/exp"] + version = "v0.0.0-20231006140011-7918f672742d" + hash = "sha256-2SO1etTQ6UCUhADR5sgvDEDLHcj77pJKCIa/8mGDbAo=" + [mod."golang.org/x/sync"] + version = "v0.5.0" + hash = "sha256-EAKeODSsct5HhXPmpWJfulKSCkuUu6kkDttnjyZMNcI=" + [mod."golang.org/x/sys"] + version = "v0.13.0" + hash = "sha256-/+RDZ0a0oEfJ0k304VqpJpdrl2ZXa3yFlOxy4mjW7w0=" + [mod."gopkg.in/yaml.v3"] + version = "v3.0.1" + hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU=" diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..d10469e --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,144 @@ +package cache + +import ( + "context" + "crypto/sha1" + "encoding/base32" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/adrg/xdg" + "github.com/juju/errors" + "github.com/vmihailenco/msgpack/v5" + bolt "go.etcd.io/bbolt" +) + +const ( + modifiedBucket = "modified" +) + +var db *bolt.DB + +func Open(treeRoot string, clean bool) (err error) { + // determine a unique and consistent db name for the tree root + h := sha1.New() + h.Write([]byte(treeRoot)) + digest := h.Sum(nil) + + name := base32.StdEncoding.EncodeToString(digest) + path, err := xdg.CacheFile(fmt.Sprintf("treefmt/eval-cache/%v.db", name)) + + // bust the cache if specified + if clean { + err := os.Remove(path) + if errors.Is(err, os.ErrNotExist) { + err = nil + } else if err != nil { + return errors.Annotate(err, "failed to clear cache") + } + } + + if err != nil { + return errors.Annotate(err, "could not resolve local path for the cache") + } + + db, err = bolt.Open(path, 0o600, nil) + if err != nil { + return errors.Annotate(err, "failed to open cache") + } + + err = db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucket([]byte(modifiedBucket)) + if errors.Is(err, bolt.ErrBucketExists) { + return nil + } + return err + }) + + return +} + +func Close() error { + return db.Close() +} + +func ChangeSet(ctx context.Context, root string, pathsCh chan<- string) error { + return db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(modifiedBucket)) + + return filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return errors.Annotate(err, "failed to walk path") + } else if ctx.Err() != nil { + return ctx.Err() + } else if info.IsDir() { + // todo what about symlinks? + return nil + } + + if info.Mode()&os.ModeSymlink == os.ModeSymlink { + // skip symlinks + return nil + } + + b := bucket.Get([]byte(path)) + + var cached FileInfo + + if b != nil { + if err = msgpack.Unmarshal(b, &cached); err != nil { + return errors.Annotatef(err, "failed to unmarshal cache info for path '%v'", path) + } + } + + changedOrNew := !(cached.Modified == info.ModTime() && cached.Size == info.Size()) + + if !changedOrNew { + // no change + return nil + } + + // pass on the path + pathsCh <- path + return nil + }) + }) +} + +func WriteModTime(paths []string) error { + if len(paths) == 0 { + return nil + } + + return db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(modifiedBucket)) + + for _, path := range paths { + if path == "" { + continue + } + pathInfo, err := os.Stat(path) + if err != nil { + return err + } + + cacheInfo := FileInfo{ + Size: pathInfo.Size(), + Modified: pathInfo.ModTime(), + } + + bytes, err := msgpack.Marshal(cacheInfo) + if err != nil { + return errors.Annotate(err, "failed to marshal mod time") + } + + if err = bucket.Put([]byte(path), bytes); err != nil { + return errors.Annotate(err, "failed to put mode time") + } + } + + return nil + }) +} diff --git a/internal/cache/types.go b/internal/cache/types.go new file mode 100644 index 0000000..d78e953 --- /dev/null +++ b/internal/cache/types.go @@ -0,0 +1,8 @@ +package cache + +import "time" + +type FileInfo struct { + Size int64 + Modified time.Time +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..8007a31 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,29 @@ +package cli + +import "github.com/charmbracelet/log" + +var Cli struct { + Log LogOptions `embed:""` + + ConfigFile string `type:"existingfile" default:"./treefmt.toml"` + TreeRoot string `type:"existingdir" default:"."` + ClearCache bool `short:"c" help:"Reset the evaluation cache. Use in case the cache is not precise enough"` + + Format Format `cmd:"" default:"."` +} + +type LogOptions struct { + Verbosity int `name:"verbose" short:"v" type:"counter" default:"0" env:"LOG_LEVEL" help:"Set the verbosity of logs e.g. -vv"` +} + +func (lo *LogOptions) ConfigureLogger() { + log.SetReportTimestamp(false) + + if lo.Verbosity == 0 { + log.SetLevel(log.WarnLevel) + } else if lo.Verbosity == 1 { + log.SetLevel(log.InfoLevel) + } else if lo.Verbosity >= 2 { + log.SetLevel(log.DebugLevel) + } +} diff --git a/internal/cli/format.go b/internal/cli/format.go new file mode 100644 index 0000000..bae2eb4 --- /dev/null +++ b/internal/cli/format.go @@ -0,0 +1,155 @@ +package cli + +import ( + "context" + "fmt" + "time" + + "github.com/numtide/treefmt/internal/cache" + "github.com/numtide/treefmt/internal/format" + + "github.com/charmbracelet/log" + "github.com/juju/errors" + "github.com/ztrue/shutdown" + "golang.org/x/sync/errgroup" +) + +type Format struct{} + +func (f *Format) Run() error { + start := time.Now() + + Cli.Log.ConfigureLogger() + + l := log.WithPrefix("format") + + defer func() { + if err := cache.Close(); err != nil { + l.Errorf("failed to close cache: %v", err) + } + }() + + // create an overall context + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // register shutdown hook + shutdown.Add(cancel) + + // read config + cfg, err := format.ReadConfigFile(Cli.ConfigFile) + if err != nil { + return errors.Annotate(err, "failed to read config file") + } + + // init formatters + for name, formatter := range cfg.Formatters { + if err = formatter.Init(name); err != nil { + return errors.Annotatef(err, "failed to initialise formatter: %v", name) + } + } + + ctx = format.RegisterFormatters(ctx, cfg.Formatters) + + if err = cache.Open(Cli.TreeRoot, Cli.ClearCache); err != nil { + return err + } + + // + pendingCh := make(chan string, 1024) + completedCh := make(chan string, 1024) + + ctx = format.SetCompletedChannel(ctx, completedCh) + + // + eg, ctx := errgroup.WithContext(ctx) + + // start the formatters + for name := range cfg.Formatters { + formatter := cfg.Formatters[name] + eg.Go(func() error { + return formatter.Run(ctx) + }) + } + + // determine paths to be formatted + pathsCh := make(chan string, 1024) + + // update cache as paths are completed + eg.Go(func() error { + batchSize := 1024 + batch := make([]string, batchSize) + + var pending, completed int + + LOOP: + for { + select { + case _, ok := <-pendingCh: + if ok { + pending += 1 + } else if pending == completed { + break LOOP + } + + case path, ok := <-completedCh: + if !ok { + break LOOP + } + batch = append(batch, path) + if len(batch) == batchSize { + if err := cache.WriteModTime(batch); err != nil { + return err + } + batch = batch[:0] + } + + completed += 1 + + if completed == pending { + close(completedCh) + } + } + } + + // final flush + if err := cache.WriteModTime(batch); err != nil { + return err + } + + println(fmt.Sprintf("%v files changed in %v", completed, time.Now().Sub(start))) + return nil + }) + + eg.Go(func() error { + count := 0 + + for path := range pathsCh { + // todo cycle detection in Befores + for _, formatter := range cfg.Formatters { + if formatter.Wants(path) { + pendingCh <- path + count += 1 + formatter.Put(path) + } + } + } + + for _, formatter := range cfg.Formatters { + formatter.Close() + } + + if count == 0 { + close(completedCh) + } + + return nil + }) + + eg.Go(func() error { + defer close(pathsCh) + return cache.ChangeSet(ctx, Cli.TreeRoot, pathsCh) + }) + + return eg.Wait() +} diff --git a/internal/format/config.go b/internal/format/config.go new file mode 100644 index 0000000..6b786c3 --- /dev/null +++ b/internal/format/config.go @@ -0,0 +1,12 @@ +package format + +import "github.com/BurntSushi/toml" + +type Config struct { + Formatters map[string]*Formatter `toml:"formatter"` +} + +func ReadConfigFile(path string) (cfg *Config, err error) { + _, err = toml.DecodeFile(path, &cfg) + return +} diff --git a/internal/format/config_test.go b/internal/format/config_test.go new file mode 100644 index 0000000..56c0b43 --- /dev/null +++ b/internal/format/config_test.go @@ -0,0 +1,122 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConfig(t *testing.T) { + as := require.New(t) + + cfg, err := ReadConfigFile("../../test/treefmt.toml") + as.NoError(err, "failed to read config file") + + as.NotNil(cfg) + + // python + python, ok := cfg.Formatters["python"] + as.True(ok, "python formatter not found") + as.Equal("black", python.Command) + as.Nil(python.Options) + as.Equal([]string{"*.py"}, python.Includes) + as.Nil(python.Excludes) + + // elm + elm, ok := cfg.Formatters["elm"] + as.True(ok, "elm formatter not found") + as.Equal("elm-format", elm.Command) + as.Equal([]string{"--yes"}, elm.Options) + as.Equal([]string{"*.elm"}, elm.Includes) + as.Nil(elm.Excludes) + + // go + golang, ok := cfg.Formatters["go"] + as.True(ok, "go formatter not found") + as.Equal("gofmt", golang.Command) + as.Equal([]string{"-w"}, golang.Options) + as.Equal([]string{"*.go"}, golang.Includes) + as.Nil(golang.Excludes) + + // haskell + haskell, ok := cfg.Formatters["haskell"] + as.True(ok, "haskell formatter not found") + as.Equal("ormolu", haskell.Command) + as.Equal([]string{ + "--ghc-opt", "-XBangPatterns", + "--ghc-opt", "-XPatternSynonyms", + "--ghc-opt", "-XTypeApplications", + "--mode", "inplace", + "--check-idempotence", + }, haskell.Options) + as.Equal([]string{"*.hs"}, haskell.Includes) + as.Equal([]string{"examples/haskell/"}, haskell.Excludes) + + // nix + nix, ok := cfg.Formatters["nix"] + as.True(ok, "nix formatter not found") + as.Equal("nixpkgs-fmt", nix.Command) + as.Nil(nix.Options) + as.Equal([]string{"*.nix"}, nix.Includes) + as.Equal([]string{"examples/nix/sources.nix"}, nix.Excludes) + + // ruby + ruby, ok := cfg.Formatters["ruby"] + as.True(ok, "ruby formatter not found") + as.Equal("rufo", ruby.Command) + as.Equal([]string{"-x"}, ruby.Options) + as.Equal([]string{"*.rb"}, ruby.Includes) + as.Nil(ruby.Excludes) + + // prettier + prettier, ok := cfg.Formatters["prettier"] + as.True(ok, "prettier formatter not found") + as.Equal("prettier", prettier.Command) + as.Equal([]string{"--write"}, prettier.Options) + as.Equal([]string{ + "*.css", + "*.html", + "*.js", + "*.json", + "*.jsx", + "*.md", + "*.mdx", + "*.scss", + "*.ts", + "*.yaml", + }, prettier.Includes) + as.Equal([]string{"CHANGELOG.md"}, prettier.Excludes) + + // rust + // rust, ok := cfg.Formatters["rust"] + // as.True(ok, "rust formatter not found") + // as.Equal("rustfmt", rust.Command) + // as.Equal([]string{"--edition", "2018"}, rust.Options) + // as.Equal([]string{"*.rs"}, rust.Includes) + // as.Nil(rust.Excludes) + + // shell + shell, ok := cfg.Formatters["shell"] + as.True(ok, "shell formatter not found") + as.Equal("/bin/sh", shell.Command) + as.Equal([]string{ + "-euc", + `# First lint all the scripts +shellcheck "$@" + +# Then format them +shfmt -i 2 -s -w "$@" + `, + "--", + }, shell.Options) + as.Equal([]string{"*.sh"}, shell.Includes) + as.Nil(shell.Excludes) + + // terraform + terraform, ok := cfg.Formatters["terraform"] + as.True(ok, "terraform formatter not found") + as.Equal("terraform", terraform.Command) + as.Equal([]string{"fmt"}, terraform.Options) + as.Equal([]string{"*.tf"}, terraform.Includes) + as.Nil(terraform.Excludes) +} diff --git a/internal/format/context.go b/internal/format/context.go new file mode 100644 index 0000000..207ca4a --- /dev/null +++ b/internal/format/context.go @@ -0,0 +1,36 @@ +package format + +import ( + "context" +) + +const ( + formattersKey = "formatters" + completedChKey = "completedCh" +) + +func RegisterFormatters(ctx context.Context, formatters map[string]*Formatter) context.Context { + return context.WithValue(ctx, formattersKey, formatters) +} + +func GetFormatters(ctx context.Context) map[string]*Formatter { + return ctx.Value(formattersKey).(map[string]*Formatter) +} + +func SetCompletedChannel(ctx context.Context, completedCh chan string) context.Context { + return context.WithValue(ctx, completedChKey, completedCh) +} + +func MarkFormatComplete(ctx context.Context, path string) { + ctx.Value(completedChKey).(chan string) <- path +} + +func ForwardPath(ctx context.Context, path string, names []string) { + if len(names) == 0 { + return + } + formatters := GetFormatters(ctx) + for _, name := range names { + formatters[name].Put(path) + } +} diff --git a/internal/format/format.go b/internal/format/format.go new file mode 100644 index 0000000..8d446b3 --- /dev/null +++ b/internal/format/format.go @@ -0,0 +1,161 @@ +package format + +import ( + "context" + "os/exec" + "strings" + "time" + + "github.com/charmbracelet/log" + "github.com/gobwas/glob" + "github.com/juju/errors" +) + +type Formatter struct { + Name string + Command string + Options []string + Includes []string + Excludes []string + Before []string + + log *log.Logger + + // globs for matching against paths + includes []glob.Glob + excludes []glob.Glob + + inbox chan string + + batch []string + batchSize int +} + +func (f *Formatter) Init(name string) error { + f.Name = name + f.log = log.WithPrefix("format | " + name) + + f.inbox = make(chan string, 1024) + + f.batchSize = 1024 + f.batch = make([]string, f.batchSize) + f.batch = f.batch[:0] + + // todo refactor common code below + if len(f.Includes) > 0 { + for _, pattern := range f.Includes { + if !strings.Contains(pattern, "/") { + pattern = "**/" + pattern + } + g, err := glob.Compile(pattern) + if err != nil { + return errors.Annotatef(err, "failed to compile include pattern '%v' for formatter '%v'", pattern, f.Name) + } + f.includes = append(f.includes, g) + } + } + + if len(f.Excludes) > 0 { + for _, pattern := range f.Excludes { + if !strings.Contains(pattern, "/") { + pattern = "**/" + pattern + } + g, err := glob.Compile(pattern) + if err != nil { + return errors.Annotatef(err, "failed to compile exclude pattern '%v' for formatter '%v'", pattern, f.Name) + } + f.excludes = append(f.excludes, g) + } + } + + return nil +} + +func (f *Formatter) Wants(path string) bool { + if PathMatches(path, f.excludes) { + return false + } + return PathMatches(path, f.includes) +} + +func (f *Formatter) Put(path string) { + f.inbox <- path +} + +func (f *Formatter) Run(ctx context.Context) (err error) { +LOOP: + for { + select { + case <-ctx.Done(): + err = ctx.Err() + break LOOP + + case path, ok := <-f.inbox: + if !ok { + break LOOP + } + + // add to the current batch + f.batch = append(f.batch, path) + + if len(f.batch) == f.batchSize { + // drain immediately + if err := f.apply(ctx); err != nil { + break LOOP + } + } + } + } + + if err != nil { + return + } + + // final flush + return f.apply(ctx) +} + +func (f *Formatter) apply(ctx context.Context) error { + // empty check + if len(f.batch) == 0 { + return nil + } + + // construct args, starting with config + args := f.Options + + // append each file path + for _, path := range f.batch { + args = append(args, path) + } + + start := time.Now() + cmd := exec.CommandContext(ctx, f.Command, args...) + + if _, err := cmd.CombinedOutput(); err != nil { + // todo log output + return err + } + + f.log.Infof("%v files processed in %v", len(f.batch), time.Now().Sub(start)) + + // mark completed or forward on + if len(f.Before) == 0 { + for _, path := range f.batch { + MarkFormatComplete(ctx, path) + } + } else { + for _, path := range f.batch { + ForwardPath(ctx, path, f.Before) + } + } + + // reset batch + f.batch = f.batch[:0] + + return nil +} + +func (f *Formatter) Close() { + close(f.inbox) +} diff --git a/internal/format/glob.go b/internal/format/glob.go new file mode 100644 index 0000000..e883fb6 --- /dev/null +++ b/internal/format/glob.go @@ -0,0 +1,15 @@ +package format + +import ( + "github.com/gobwas/glob" +) + +func PathMatches(path string, globs []glob.Glob) bool { + for idx := range globs { + if globs[idx].Match(path) { + return true + } + } + + return false +} diff --git a/internal/log/writer.go b/internal/log/writer.go new file mode 100644 index 0000000..25daa88 --- /dev/null +++ b/internal/log/writer.go @@ -0,0 +1,21 @@ +package log + +import ( + "bufio" + "bytes" + + "github.com/charmbracelet/log" +) + +type Writer struct { + Log *log.Logger +} + +func (l *Writer) Write(p []byte) (n int, err error) { + scanner := bufio.NewScanner(bytes.NewReader(p)) + for scanner.Scan() { + line := scanner.Text() + l.Log.Debug(line) + } + return len(p), nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ca6d3f9 --- /dev/null +++ b/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/alecthomas/kong" + "github.com/numtide/treefmt/internal/cli" +) + +func main() { + ctx := kong.Parse(&cli.Cli) + ctx.FatalIfErrorf(ctx.Run()) +} diff --git a/nix/checks.nix b/nix/checks.nix new file mode 100644 index 0000000..8216515 --- /dev/null +++ b/nix/checks.nix @@ -0,0 +1,5 @@ +{lib, ...}: { + perSystem = {self', ...}: { + checks = with lib; mapAttrs' (n: nameValuePair "package-${n}") self'.packages; + }; +} diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 0000000..8d2322e --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,10 @@ +{inputs, ...}: { + imports = [ + inputs.flake-root.flakeModule + ./checks.nix + ./devshell.nix + ./nixpkgs.nix + ./packages.nix + ./treefmt.nix + ]; +} diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 0000000..391bd14 --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,58 @@ +{inputs, ...}: { + imports = [ + inputs.devshell.flakeModule + ]; + + config.perSystem = { + pkgs, + config, + ... + }: { + config.devshells.default = { + env = [ + { + name = "GOROOT"; + value = pkgs.go + "/share/go"; + } + { + name = "LD_LIBRARY_PATH"; + value = "$DEVSHELL_DIR/lib"; + } + ]; + + packages = with pkgs; [ + # golang + go + go-tools + delve + golangci-lint + + # formatters for testing + + elmPackages.elm-format + haskellPackages.cabal-fmt + haskellPackages.ormolu + mdsh + nixpkgs-fmt + nodePackages.prettier + python3.pkgs.black + rufo + rustfmt + shellcheck + shfmt + terraform + ]; + + commands = [ + { + category = "development"; + package = pkgs.gomod2nix; + } + { + category = "development"; + package = pkgs.enumer; + } + ]; + }; + }; +} diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix new file mode 100644 index 0000000..fd370ca --- /dev/null +++ b/nix/nixpkgs.nix @@ -0,0 +1,16 @@ +{inputs, ...}: { + perSystem = {system, ...}: { + # customise nixpkgs instance + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ + inputs.gomod2nix.overlays.default + ]; + config = { + # for terraform + # todo make this more specific + allowUnfree = true; + }; + }; + }; +} diff --git a/nix/packages.nix b/nix/packages.nix new file mode 100644 index 0000000..dd695b9 --- /dev/null +++ b/nix/packages.nix @@ -0,0 +1,42 @@ +{inputs, ...}: { + imports = [ + inputs.flake-parts.flakeModules.easyOverlay + ]; + + perSystem = { + self', + inputs', + lib, + pkgs, + ... + }: { + packages = rec { + treefmt = inputs'.gomod2nix.legacyPackages.buildGoApplication rec { + pname = "treefmt"; + version = "0.0.1+dev"; + + # ensure we are using the same version of go to build with + inherit (pkgs) go; + + src = ../.; + modules = ../gomod2nix.toml; + + ldflags = [ + "-X 'build.Name=${pname}'" + "-X 'build.Version=${version}'" + ]; + + meta = with lib; { + description = "treefmt: one CLI to format your repo"; + homepage = "https://github.com/numtide/treefmt"; + license = licenses.mit; + mainProgram = "treefmt"; + }; + }; + + default = treefmt; + }; + + overlayAttrs = self'.packages; + }; +} diff --git a/nix/treefmt.nix b/nix/treefmt.nix new file mode 100644 index 0000000..ed6fc52 --- /dev/null +++ b/nix/treefmt.nix @@ -0,0 +1,32 @@ +{inputs, ...}: { + imports = [ + inputs.treefmt-nix.flakeModule + ]; + perSystem = {config, ...}: { + treefmt.config = { + inherit (config.flake-root) projectRootFile; + flakeCheck = true; + flakeFormatter = true; + programs = { + alejandra.enable = true; + deadnix.enable = true; + gofumpt.enable = true; + prettier.enable = true; + statix.enable = true; + }; + + settings.formatter.prettier.options = ["--tab-width" "4"]; + }; + + devshells.default = { + commands = [ + { + category = "formatting"; + name = "fmt"; + help = "format the repo"; + command = "nix fmt"; + } + ]; + }; + }; +} diff --git a/test/echo.toml b/test/echo.toml new file mode 100644 index 0000000..9e3295c --- /dev/null +++ b/test/echo.toml @@ -0,0 +1,3 @@ +[formatter.echo] +command = "echo" +includes = [ "*.*" ] \ No newline at end of file diff --git a/test/examples/elm/elm.json b/test/examples/elm/elm.json new file mode 100644 index 0000000..2e54076 --- /dev/null +++ b/test/examples/elm/elm.json @@ -0,0 +1,22 @@ +{ + "type": "application", + "source-directories": ["src"], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0" + }, + "indirect": { + "elm/json": "1.1.3", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/test/examples/elm/src/Main.elm b/test/examples/elm/src/Main.elm new file mode 100644 index 0000000..401f267 --- /dev/null +++ b/test/examples/elm/src/Main.elm @@ -0,0 +1,31 @@ +module Main exposing (Msg(..), main, update, view) + +import Browser +import Html exposing (Html, button, div, text) +import Html.Events exposing (onClick) + + +main = + Browser.sandbox { init = 0, update = update, view = view } + + +type Msg + = Increment + | Decrement + + +update msg model = + case msg of + Increment -> + model + 1 + + Decrement -> + model - 1 + + +view model = + div [] + [ button [ onClick Decrement ] [ text "-" ] + , div [] [ text (String.fromInt model) ] + , button [ onClick Increment ] [ text "+" ] + ] diff --git a/test/examples/go/go.mod b/test/examples/go/go.mod new file mode 100644 index 0000000..4f96884 --- /dev/null +++ b/test/examples/go/go.mod @@ -0,0 +1,3 @@ +module hello + +go 1.15 diff --git a/test/examples/go/main.go b/test/examples/go/main.go new file mode 100644 index 0000000..c048119 --- /dev/null +++ b/test/examples/go/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("hello world") +} diff --git a/test/examples/haskell-frontend/CHANGELOG.md b/test/examples/haskell-frontend/CHANGELOG.md new file mode 100644 index 0000000..f928d5e --- /dev/null +++ b/test/examples/haskell-frontend/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for haskell + +## 0.1.0.0 -- YYYY-mm-dd + +- First version. Released on an unsuspecting world. diff --git a/test/examples/haskell-frontend/Main.hs b/test/examples/haskell-frontend/Main.hs new file mode 100644 index 0000000..65ae4a0 --- /dev/null +++ b/test/examples/haskell-frontend/Main.hs @@ -0,0 +1,4 @@ +module Main where + +main :: IO () +main = putStrLn "Hello, Haskell!" diff --git a/test/examples/haskell-frontend/Setup.hs b/test/examples/haskell-frontend/Setup.hs new file mode 100644 index 0000000..e8ef27d --- /dev/null +++ b/test/examples/haskell-frontend/Setup.hs @@ -0,0 +1,3 @@ +import Distribution.Simple + +main = defaultMain diff --git a/test/examples/haskell-frontend/haskell-frontend.cabal b/test/examples/haskell-frontend/haskell-frontend.cabal new file mode 100644 index 0000000..2997318 --- /dev/null +++ b/test/examples/haskell-frontend/haskell-frontend.cabal @@ -0,0 +1,25 @@ +cabal-version: >=1.10 +-- Initial package description 'haskell.cabal' generated by 'cabal init'. +-- For further documentation, see http://haskell.org/cabal/users-guide/ + +name: haskell-frontend +version: 0.1.0.0 +-- synopsis: +-- description: +-- bug-reports: +-- license: +license-file: LICENSE +author: Andika Demas Riyandi +maintainer: andika.riyan@gmail.com +-- copyright: +-- category: +build-type: Simple +extra-source-files: CHANGELOG.md + +executable haskell-frontend + main-is: Main.hs + -- other-modules: + -- other-extensions: + build-depends: base >=4.14 && <4.15 + -- hs-source-dirs: + default-language: Haskell2010 diff --git a/test/examples/haskell/CHANGELOG.md b/test/examples/haskell/CHANGELOG.md new file mode 100644 index 0000000..f928d5e --- /dev/null +++ b/test/examples/haskell/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for haskell + +## 0.1.0.0 -- YYYY-mm-dd + +- First version. Released on an unsuspecting world. diff --git a/test/examples/haskell/Foo.hs b/test/examples/haskell/Foo.hs new file mode 100644 index 0000000..2ece54e --- /dev/null +++ b/test/examples/haskell/Foo.hs @@ -0,0 +1,4 @@ +module Foo where + +foo :: IO () +foo = putStrLn "Hello, Riyan!" diff --git a/test/examples/haskell/Main.hs b/test/examples/haskell/Main.hs new file mode 100644 index 0000000..07a0935 --- /dev/null +++ b/test/examples/haskell/Main.hs @@ -0,0 +1,4 @@ +module Main where + +main :: IO () +main = putStrLn "Hello, Riyan!" diff --git a/test/examples/haskell/Nested/Foo.hs b/test/examples/haskell/Nested/Foo.hs new file mode 100644 index 0000000..43dea3c --- /dev/null +++ b/test/examples/haskell/Nested/Foo.hs @@ -0,0 +1,4 @@ +module Nested.Foo where + +foo :: IO () +foo = putStrLn "Hello, Riyan!" diff --git a/test/examples/haskell/Setup.hs b/test/examples/haskell/Setup.hs new file mode 100644 index 0000000..e8ef27d --- /dev/null +++ b/test/examples/haskell/Setup.hs @@ -0,0 +1,3 @@ +import Distribution.Simple + +main = defaultMain diff --git a/test/examples/haskell/haskell.cabal b/test/examples/haskell/haskell.cabal new file mode 100644 index 0000000..a7d1fa5 --- /dev/null +++ b/test/examples/haskell/haskell.cabal @@ -0,0 +1,25 @@ +cabal-version: >=1.10 +-- Initial package description 'haskell.cabal' generated by 'cabal init'. +-- For further documentation, see http://haskell.org/cabal/users-guide/ + +name: haskell +version: 0.1.0.0 +-- synopsis: +-- description: +-- bug-reports: +-- license: +license-file: LICENSE +author: Andika Demas Riyandi +maintainer: andika.riyan@gmail.com +-- copyright: +-- category: +build-type: Simple +extra-source-files: CHANGELOG.md + +executable haskell + main-is: Main.hs + -- other-modules: + -- other-extensions: + build-depends: base >=4.14 && <4.15 + -- hs-source-dirs: + default-language: Haskell2010 diff --git a/test/examples/haskell/treefmt.toml b/test/examples/haskell/treefmt.toml new file mode 100644 index 0000000..6865944 --- /dev/null +++ b/test/examples/haskell/treefmt.toml @@ -0,0 +1,10 @@ +[formatter.haskell] +command = "ormolu" +options = [ + "--ghc-opt", "-XBangPatterns", + "--ghc-opt", "-XPatternSynonyms", + "--ghc-opt", "-XTypeApplications", + "--mode", "inplace", + "--check-idempotence", +] +includes = ["Foo.hs"] \ No newline at end of file diff --git a/test/examples/html/index.html b/test/examples/html/index.html new file mode 100644 index 0000000..201633d --- /dev/null +++ b/test/examples/html/index.html @@ -0,0 +1,10 @@ + + + + + Title + + +

Hi!

+ + diff --git a/test/examples/html/scripts/.gitkeep b/test/examples/html/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/examples/javascript/source/hello.js b/test/examples/javascript/source/hello.js new file mode 100644 index 0000000..4d352ce --- /dev/null +++ b/test/examples/javascript/source/hello.js @@ -0,0 +1,65 @@ +const helloFactory = function ({ React }) { + const { string, func } = React.PropTypes; + + return function Hello(props) { + // React wants propTypes & defaultProps + // to be static. + Hello.propTypes = { + word: string, + mode: string, + + actions: React.PropTypes.shape({ + setWord: func.isRequired, + setMode: func.isRequired, + }), + }; + + return { + props, // set props + + componentDidUpdate() { + this.refs.wordInput.getDOMNode().focus(); + }, + + render() { + const { word, mode } = this.props; + + const { setMode, setWord } = this.props.actions; + + const styles = { + displayMode: { + display: mode === "display" ? "inline" : "none", + }, + + editMode: { + display: mode === "edit" ? "inline" : "none", + }, + }; + + const onKeyUp = function (e) { + if (e.key !== "Enter") return; + + setWord(e.target.value); + setMode("display"); + }; + + return ( +

+ Hello,  + setMode("edit")}> + {word}! + + +

+ ); + }, + }; + }; +}; + +export default helloFactory; diff --git a/test/examples/nix/sources.nix b/test/examples/nix/sources.nix new file mode 100644 index 0000000..dccca92 --- /dev/null +++ b/test/examples/nix/sources.nix @@ -0,0 +1,242 @@ +# This file has been generated by Niv. +let + # + # The fetchers. fetch_ fetches specs of type . + # + fetch_file = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true + then + builtins_fetchurl + { + inherit (spec) url sha256; + name = name'; + } + else + pkgs.fetchurl { + inherit (spec) url sha256; + name = name'; + }; + + fetch_tarball = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true + then + builtins_fetchTarball + { + name = name'; + inherit (spec) url sha256; + } + else + pkgs.fetchzip { + name = name'; + inherit (spec) url sha256; + }; + + fetch_git = name: spec: + let + ref = + spec.ref + or ( + if spec ? branch + then "refs/heads/${spec.branch}" + else if spec ? tag + then "refs/tags/${spec.tag}" + else abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" + ); + in + builtins.fetchGit { + url = spec.repo; + inherit (spec) rev; + inherit ref; + }; + + fetch_local = spec: spec.path; + + fetch_builtin-tarball = name: + throw + '' [${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=tarball -a builtin=true''; + + fetch_builtin-url = name: + throw + '' [${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=file -a builtin=true''; + + # + # Various helpers + # + + # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 + sanitizeName = name: ( + concatMapStrings + (s: + if builtins.isList s + then "-" + else s) + ( + builtins.split "[^[:alnum:]+._?=-]+" + ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) + ) + ); + + # The set of packages used when specs are fetched using non-builtins. + mkPkgs = sources: system: + let + sourcesNixpkgs = + import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; + hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; + hasThisAsNixpkgsPath = == ./.; + in + if builtins.hasAttr "nixpkgs" sources + then sourcesNixpkgs + else if hasNixpkgsPath && ! hasThisAsNixpkgsPath + then import { } + else + abort + '' + Please specify either (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; + + # The actual fetching function. + fetch = pkgs: name: spec: + if ! builtins.hasAttr "type" spec + then abort "ERROR: niv spec ${name} does not have a 'type' attribute" + else if spec.type == "file" + then fetch_file pkgs name spec + else if spec.type == "tarball" + then fetch_tarball pkgs name spec + else if spec.type == "git" + then fetch_git name spec + else if spec.type == "local" + then fetch_local spec + else if spec.type == "builtin-tarball" + then fetch_builtin-tarball name + else if spec.type == "builtin-url" + then fetch_builtin-url name + else abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; + + # If the environment variable NIV_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + replace = name: drv: + let + saneName = + stringAsChars + (c: + if ((builtins.match "[a-zA-Z0-9]" c) == null) + then "_" + else c) + name; + ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; + in + if ersatz == "" + then drv + else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + if builtins.substring 0 1 ersatz == "/" + then /. + ersatz + else /. + builtins.getEnv "PWD" + "/${ersatz}"; + + # Ports of functions for older nix versions + + # a Nix version of mapAttrs if the built-in doesn't exist + mapAttrs = + builtins.mapAttrs + or ( + f: set: + with builtins; + listToAttrs (map + (attr: { + name = attr; + value = f attr set.${attr}; + }) + (attrNames set)) + ); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = first: last: + if first > last + then [ ] + else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatMapStrings = f: list: concatStrings (map f list); + concatStrings = builtins.concatStringsSep ""; + + # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 + optionalAttrs = cond: as: + if cond + then as + else { }; + + # fetchTarball version that is compatible between all the versions of Nix + builtins_fetchTarball = + { url + , name ? null + , sha256 + , + } @ attrs: + let + inherit (builtins) lessThan nixVersion fetchTarball; + in + if lessThan nixVersion "1.12" + then fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else fetchTarball attrs; + + # fetchurl version that is compatible between all the versions of Nix + builtins_fetchurl = + { url + , name ? null + , sha256 + , + } @ attrs: + let + inherit (builtins) lessThan nixVersion fetchurl; + in + if lessThan nixVersion "1.12" + then fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else fetchurl attrs; + + # Create the final "sources" from the config + mkSources = config: + mapAttrs + ( + name: spec: + if builtins.hasAttr "outPath" spec + then + abort + "The values in sources.json should not have an 'outPath' attribute" + else spec // { outPath = replace name (fetch config.pkgs name spec); } + ) + config.sources; + + # The "config" used by the fetchers + mkConfig = + { sourcesFile ? if builtins.pathExists ./sources.json + then ./sources.json + else null + , sources ? if (sourcesFile == null) + then { } + else builtins.fromJSON (builtins.readFile sourcesFile) + , system ? builtins.currentSystem + , pkgs ? mkPkgs sources system + , + }: rec { + # The sources, i.e. the attribute set of spec name to spec + inherit sources; + + # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers + inherit pkgs; + }; +in +mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/test/examples/python/main.py b/test/examples/python/main.py new file mode 100644 index 0000000..79fe90b --- /dev/null +++ b/test/examples/python/main.py @@ -0,0 +1,12 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def hello_world(): + return "Hello world" + + +if __name__ == "__main__": + app.run() diff --git a/test/examples/python/requirements.txt b/test/examples/python/requirements.txt new file mode 100644 index 0000000..200fdf3 --- /dev/null +++ b/test/examples/python/requirements.txt @@ -0,0 +1 @@ +Flask==0.12.1 \ No newline at end of file diff --git a/test/examples/python/virtualenv_proxy.py b/test/examples/python/virtualenv_proxy.py new file mode 100644 index 0000000..cef7ff4 --- /dev/null +++ b/test/examples/python/virtualenv_proxy.py @@ -0,0 +1,104 @@ +import datetime +import os +import sys +import traceback + +if sys.version_info[0] == 3: + + def to_str(value): + return value.decode(sys.getfilesystemencoding()) + + def execfile(path, global_dict): + """Execute a file""" + with open(path, "r") as f: + code = f.read() + code = code.replace("\r\n", "\n") + "\n" + exec(code, global_dict) + +else: + + def to_str(value): + return value.encode(sys.getfilesystemencoding()) + + +def log(txt): + """Logs fatal errors to a log file if WSGI_LOG env var is defined""" + log_file = os.environ.get("WSGI_LOG") + if log_file: + f = open(log_file, "a+") + try: + f.write("%s: %s" % (datetime.datetime.now(), txt)) + finally: + f.close() + + +def get_wsgi_handler(handler_name): + if not handler_name: + raise Exception("WSGI_ALT_VIRTUALENV_HANDLER env var must be set") + + if not isinstance(handler_name, str): + handler_name = to_str(handler_name) + + module_name, _, callable_name = handler_name.rpartition(".") + should_call = callable_name.endswith("()") + callable_name = callable_name[:-2] if should_call else callable_name + name_list = [(callable_name, should_call)] + handler = None + last_tb = "" + + while module_name: + try: + handler = __import__(module_name, fromlist=[name_list[0][0]]) + last_tb = "" + for name, should_call in name_list: + handler = getattr(handler, name) + if should_call: + handler = handler() + break + except ImportError: + module_name, _, callable_name = module_name.rpartition(".") + should_call = callable_name.endswith("()") + callable_name = callable_name[:-2] if should_call else callable_name + name_list.insert(0, (callable_name, should_call)) + handler = None + last_tb = ": " + traceback.format_exc() + + if handler is None: + raise ValueError('"%s" could not be imported%s' % (handler_name, last_tb)) + + return handler + + +activate_this = os.getenv("WSGI_ALT_VIRTUALENV_ACTIVATE_THIS") +if not activate_this: + raise Exception("WSGI_ALT_VIRTUALENV_ACTIVATE_THIS is not set") + + +def get_virtualenv_handler(): + log("Activating virtualenv with %s\n" % activate_this) + execfile(activate_this, dict(__file__=activate_this)) + + log("Getting handler %s\n" % os.getenv("WSGI_ALT_VIRTUALENV_HANDLER")) + handler = get_wsgi_handler(os.getenv("WSGI_ALT_VIRTUALENV_HANDLER")) + log("Got handler: %r\n" % handler) + return handler + + +def get_venv_handler(): + log("Activating venv with executable at %s\n" % activate_this) + import site + + sys.executable = activate_this + old_sys_path, sys.path = sys.path, [] + + site.main() + + sys.path.insert(0, "") + for item in old_sys_path: + if item not in sys.path: + sys.path.append(item) + + log("Getting handler %s\n" % os.getenv("WSGI_ALT_VIRTUALENV_HANDLER")) + handler = get_wsgi_handler(os.getenv("WSGI_ALT_VIRTUALENV_HANDLER")) + log("Got handler: %r\n" % handler) + return handler diff --git a/test/examples/ruby/bundler.rb b/test/examples/ruby/bundler.rb new file mode 100644 index 0000000..6a3a14f --- /dev/null +++ b/test/examples/ruby/bundler.rb @@ -0,0 +1,452 @@ +# frozen_string_literal: true +require "fileutils" +require "pathname" +require "rbconfig" +require "thread" +require "bundler/environment_preserver" +require "bundler/gem_remote_fetcher" +require "bundler/rubygems_ext" +require "bundler/rubygems_integration" +require "bundler/version" +require "bundler/constants" +require "bundler/current_ruby" +require "bundler/errors" + +module Bundler + environment_preserver = EnvironmentPreserver.new(ENV, %w(PATH GEM_PATH)) + ORIGINAL_ENV = environment_preserver.restore + ENV.replace(environment_preserver.backup) + SUDO_MUTEX = Mutex.new + + autoload :Definition, "bundler/definition" + autoload :Dependency, "bundler/dependency" + autoload :DepProxy, "bundler/dep_proxy" + autoload :Deprecate, "bundler/deprecate" + autoload :Dsl, "bundler/dsl" + autoload :EndpointSpecification, "bundler/endpoint_specification" + autoload :Environment, "bundler/environment" + autoload :Env, "bundler/env" + autoload :Fetcher, "bundler/fetcher" + autoload :GemHelper, "bundler/gem_helper" + autoload :GemHelpers, "bundler/gem_helpers" + autoload :Graph, "bundler/graph" + autoload :Index, "bundler/index" + autoload :Installer, "bundler/installer" + autoload :Injector, "bundler/injector" + autoload :LazySpecification, "bundler/lazy_specification" + autoload :LockfileParser, "bundler/lockfile_parser" + autoload :MatchPlatform, "bundler/match_platform" + autoload :Mirror, "bundler/mirror" + autoload :Mirrors, "bundler/mirror" + autoload :RemoteSpecification, "bundler/remote_specification" + autoload :Resolver, "bundler/resolver" + autoload :Retry, "bundler/retry" + autoload :RubyVersion, "bundler/ruby_version" + autoload :RubyDsl, "bundler/ruby_dsl" + autoload :Runtime, "bundler/runtime" + autoload :Settings, "bundler/settings" + autoload :SharedHelpers, "bundler/shared_helpers" + autoload :SpecSet, "bundler/spec_set" + autoload :StubSpecification, "bundler/stub_specification" + autoload :Source, "bundler/source" + autoload :SourceList, "bundler/source_list" + autoload :RubyGemsGemInstaller, "bundler/rubygems_gem_installer" + autoload :UI, "bundler/ui" + + class << self + attr_writer :bundle_path + + def configure + @configured ||= configure_gem_home_and_path + end + + def ui + (defined?(@ui) && @ui) || (self.ui = UI::Silent.new) + end + + def ui=(ui) + Bundler.rubygems.ui = ui ? UI::RGProxy.new(ui) : nil + @ui = ui + end + + # Returns absolute path of where gems are installed on the filesystem. + def bundle_path + @bundle_path ||= Pathname.new(settings.path).expand_path(root) + end + + # Returns absolute location of where binstubs are installed to. + def bin_path + @bin_path ||= begin + path = settings[:bin] || "bin" + path = Pathname.new(path).expand_path(root).expand_path + SharedHelpers.filesystem_access(path) { |p| FileUtils.mkdir_p(p) } + path + end + end + + def setup(*groups) + # Return if all groups are already loaded + return @setup if defined?(@setup) + + definition.validate_ruby! + + if groups.empty? + # Load all groups, but only once + @setup = load.setup + else + load.setup(*groups) + end + end + + def require(*groups) + setup(*groups).require(*groups) + end + + def load + @load ||= Runtime.new(root, definition) + end + + def environment + Bundler::Environment.new(root, definition) + end + + # Returns an instance of Bundler::Definition for given Gemfile and lockfile + # + # @param unlock [Hash, Boolean, nil] Gems that have been requested + # to be updated or true if all gems should be updated + # @return [Bundler::Definition] + def definition(unlock = nil) + @definition = nil if unlock + @definition ||= begin + configure + upgrade_lockfile + Definition.build(default_gemfile, default_lockfile, unlock) + end + end + + def locked_gems + return @locked_gems if defined?(@locked_gems) + if Bundler.default_lockfile.exist? + lock = Bundler.read_file(Bundler.default_lockfile) + @locked_gems = LockfileParser.new(lock) + else + @locked_gems = nil + end + end + + def ruby_scope + "#{Bundler.rubygems.ruby_engine}/#{Bundler.rubygems.config_map[:ruby_version]}" + end + + def user_bundle_path + Pathname.new(Bundler.rubygems.user_home).join(".bundle") + end + + def home + bundle_path.join("bundler") + end + + def install_path + home.join("gems") + end + + def specs_path + bundle_path.join("specifications") + end + + def cache + bundle_path.join("cache/bundler") + end + + def user_cache + user_bundle_path.join("cache") + end + + def root + @root ||= begin + default_gemfile.dirname.expand_path + rescue GemfileNotFound + bundle_dir = default_bundle_dir + raise GemfileNotFound, "Could not locate Gemfile or .bundle/ directory" unless bundle_dir + Pathname.new(File.expand_path("..", bundle_dir)) + end + end + + def app_config_path + if ENV["BUNDLE_APP_CONFIG"] + Pathname.new(ENV["BUNDLE_APP_CONFIG"]).expand_path(root) + else + root.join(".bundle") + end + end + + def app_cache(custom_path = nil) + path = custom_path || root + path.join(settings.app_cache_path) + end + + def tmp(name = Process.pid.to_s) + Pathname.new(Dir.mktmpdir(["bundler", name])) + end + + def rm_rf(path) + FileUtils.remove_entry_secure(path) if path && File.exist?(path) + end + + def settings + return @settings if defined?(@settings) + @settings = Settings.new(app_config_path) + rescue GemfileNotFound + @settings = Settings.new(Pathname.new(".bundle").expand_path) + end + + # @return [Hash] Environment present before Bundler was activated + def original_env + ORIGINAL_ENV.clone + end + + # @deprecated Use `original_env` instead + # @return [Hash] Environment with all bundler-related variables removed + def clean_env + env = original_env + + if env.key?("BUNDLE_ORIG_MANPATH") + env["MANPATH"] = env["BUNDLE_ORIG_MANPATH"] + end + + env.delete_if { |k, _| k[0, 7] == "BUNDLE_" } + + if env.key?("RUBYOPT") + env["RUBYOPT"] = env["RUBYOPT"].sub "-rbundler/setup", "" + end + + if env.key?("RUBYLIB") + rubylib = env["RUBYLIB"].split(File::PATH_SEPARATOR) + rubylib.delete(File.expand_path("..", __FILE__)) + env["RUBYLIB"] = rubylib.join(File::PATH_SEPARATOR) + end + + env + end + + def with_original_env + with_env(original_env) { yield } + end + + def with_clean_env + with_env(clean_env) { yield } + end + + def clean_system(*args) + with_clean_env { Kernel.system(*args) } + end + + def clean_exec(*args) + with_clean_env { Kernel.exec(*args) } + end + + def default_gemfile + SharedHelpers.default_gemfile + end + + def default_lockfile + SharedHelpers.default_lockfile + end + + def default_bundle_dir + SharedHelpers.default_bundle_dir + end + + def system_bindir + # Gem.bindir doesn't always return the location that Rubygems will install + # system binaries. If you put '-n foo' in your .gemrc, Rubygems will + # install binstubs there instead. Unfortunately, Rubygems doesn't expose + # that directory at all, so rather than parse .gemrc ourselves, we allow + # the directory to be set as well, via `bundle config bindir foo`. + Bundler.settings[:system_bindir] || Bundler.rubygems.gem_bindir + end + + def requires_sudo? + return @requires_sudo if defined?(@requires_sudo_ran) + + sudo_present = which "sudo" if settings.allow_sudo? + + if sudo_present + # the bundle path and subdirectories need to be writable for Rubygems + # to be able to unpack and install gems without exploding + path = bundle_path + path = path.parent until path.exist? + + # bins are written to a different location on OS X + bin_dir = Pathname.new(Bundler.system_bindir) + bin_dir = bin_dir.parent until bin_dir.exist? + + # if any directory is not writable, we need sudo + files = [path, bin_dir] | Dir[path.join("build_info/*").to_s] | Dir[path.join("*").to_s] + sudo_needed = files.any? { |f| !File.writable?(f) } + end + + @requires_sudo_ran = true + @requires_sudo = settings.allow_sudo? && sudo_present && sudo_needed + end + + def mkdir_p(path) + if requires_sudo? + sudo "mkdir -p '#{path}'" unless File.exist?(path) + else + SharedHelpers.filesystem_access(path, :write) do |p| + FileUtils.mkdir_p(p) + end + end + end + + def which(executable) + if File.file?(executable) && File.executable?(executable) + executable + elsif paths = ENV["PATH"] + quote = '"'.freeze + paths.split(File::PATH_SEPARATOR).find do |path| + path = path[1..-2] if path.start_with?(quote) && path.end_with?(quote) + executable_path = File.expand_path(executable, path) + return executable_path if File.file?(executable_path) && File.executable?(executable_path) + end + end + end + + def sudo(str) + SUDO_MUTEX.synchronize do + prompt = "\n\n" + <<-PROMPT.gsub(/^ {6}/, "").strip + " " + Your user account isn't allowed to install to the system Rubygems. + You can cancel this installation and run: + + bundle install --path vendor/bundle + + to install the gems into ./vendor/bundle/, or you can enter your password + and install the bundled gems to Rubygems using sudo. + + Password: + PROMPT + + `sudo -p "#{prompt}" #{str}` + end + end + + def read_file(file) + File.open(file, "rb", &:read) + end + + def load_marshal(data) + Marshal.load(data) + rescue => e + raise MarshalError, "#{e.class}: #{e.message}" + end + + def load_gemspec(file, validate = false) + @gemspec_cache ||= {} + key = File.expand_path(file) + @gemspec_cache[key] ||= load_gemspec_uncached(file, validate) + # Protect against caching side-effected gemspecs by returning a + # new instance each time. + @gemspec_cache[key].dup if @gemspec_cache[key] + end + + def load_gemspec_uncached(file, validate = false) + path = Pathname.new(file) + # Eval the gemspec from its parent directory, because some gemspecs + # depend on "./" relative paths. + SharedHelpers.chdir(path.dirname.to_s) do + contents = path.read + spec = if contents[0..2] == "---" # YAML header + eval_yaml_gemspec(path, contents) + else + eval_gemspec(path, contents) + end + return unless spec + spec.loaded_from = path.expand_path.to_s + Bundler.rubygems.validate(spec) if validate + spec + end + end + + def clear_gemspec_cache + @gemspec_cache = {} + end + + def git_present? + return @git_present if defined?(@git_present) + @git_present = Bundler.which("git") || Bundler.which("git.exe") + end + + def reset! + @definition = nil + end + + private + + def eval_yaml_gemspec(path, contents) + # If the YAML is invalid, Syck raises an ArgumentError, and Psych + # raises a Psych::SyntaxError. See psyched_yaml.rb for more info. + Gem::Specification.from_yaml(contents) + rescue YamlLibrarySyntaxError, ArgumentError, Gem::EndOfYAMLException, Gem::Exception + eval_gemspec(path, contents) + end + + def eval_gemspec(path, contents) + eval(contents, TOPLEVEL_BINDING, path.expand_path.to_s) + rescue ScriptError, StandardError => e + original_line = e.backtrace.find { |line| line.include?(path.to_s) } + msg = String.new + msg << "There was a #{e.class} while loading #{path.basename}: \n#{e.message}" + msg << " from\n #{original_line}" if original_line + msg << "\n" + + if e.is_a?(LoadError) && RUBY_VERSION >= "1.9" + msg << "\nDoes it try to require a relative path? That's been removed in Ruby 1.9." + end + + raise GemspecError, msg + end + + def configure_gem_home_and_path + blank_home = ENV["GEM_HOME"].nil? || ENV["GEM_HOME"].empty? + if settings[:disable_shared_gems] + ENV["GEM_PATH"] = "" + elsif blank_home || Bundler.rubygems.gem_dir != bundle_path.to_s + possibles = [Bundler.rubygems.gem_dir, Bundler.rubygems.gem_path] + paths = possibles.flatten.compact.uniq.reject(&:empty?) + ENV["GEM_PATH"] = paths.join(File::PATH_SEPARATOR) + end + + configure_gem_home + bundle_path + end + + def configure_gem_home + # TODO: This mkdir_p is only needed for JRuby <= 1.5 and should go away (GH #602) + begin + FileUtils.mkdir_p bundle_path.to_s + rescue + nil + end + + ENV["GEM_HOME"] = File.expand_path(bundle_path, root) + Bundler.rubygems.clear_paths + end + + def upgrade_lockfile + lockfile = default_lockfile + return unless lockfile.exist? && lockfile.read(3) == "---" + Bundler.ui.warn "Detected Gemfile.lock generated by 0.9, deleting..." + lockfile.rmtree + end + + # @param env [Hash] + def with_env(env) + backup = ENV.to_hash + ENV.replace(env) + yield + ensure + ENV.replace(backup) + end + end +end diff --git a/test/examples/rust/Cargo.toml b/test/examples/rust/Cargo.toml new file mode 100644 index 0000000..64190c6 --- /dev/null +++ b/test/examples/rust/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rust" +version = "0.1.0" +authors = ["Andika Demas Riyandi "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/test/examples/rust/src/main.rs b/test/examples/rust/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/test/examples/rust/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/test/examples/shell/foo.sh b/test/examples/shell/foo.sh new file mode 100755 index 0000000..8d5fe8c --- /dev/null +++ b/test/examples/shell/foo.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +hello() { + echo "Hello" +} + +hello diff --git a/test/examples/terraform/main.tf b/test/examples/terraform/main.tf new file mode 100644 index 0000000..799e8de --- /dev/null +++ b/test/examples/terraform/main.tf @@ -0,0 +1,4 @@ + +resource "my_resource" "xxx" { + option = [1, 2, 3] +} diff --git a/test/examples/terraform/two.tf b/test/examples/terraform/two.tf new file mode 100644 index 0000000..d111967 --- /dev/null +++ b/test/examples/terraform/two.tf @@ -0,0 +1,4 @@ + +resource "other_resource" "xxx" { + xxx = "xxx" +} diff --git a/test/treefmt.toml b/test/treefmt.toml new file mode 100644 index 0000000..f5ac8aa --- /dev/null +++ b/test/treefmt.toml @@ -0,0 +1,82 @@ +# One CLI to format the code tree - https://github.com/numtide/treefmt + +[formatter.python] +command = "black" +includes = ["*.py"] + +[formatter.elm] +command = "elm-format" +options = ["--yes"] +includes = ["*.elm"] + +[formatter.go] +command = "gofmt" +options = ["-w"] +includes = ["*.go"] + +[formatter.haskell] +command = "ormolu" +options = [ + "--ghc-opt", "-XBangPatterns", + "--ghc-opt", "-XPatternSynonyms", + "--ghc-opt", "-XTypeApplications", + "--mode", "inplace", + "--check-idempotence", +] +includes = ["*.hs"] +excludes = ["examples/haskell/"] + +[formatter.nix] +command = "nixpkgs-fmt" +includes = ["*.nix"] +# Act as an example on how to exclude specific files +excludes = ["examples/nix/sources.nix"] + +[formatter.ruby] +command = "rufo" +options = ["-x"] +includes = ["*.rb"] + +[formatter.prettier] +command = "prettier" +options = ["--write"] +includes = [ + "*.css", + "*.html", + "*.js", + "*.json", + "*.jsx", + "*.md", + "*.mdx", + "*.scss", + "*.ts", + "*.yaml", +] +excludes = ["CHANGELOG.md"] + +[formatter.rust] +command = "rustfmt" +options = ["--edition", "2018"] +includes = ["*.rs"] + +[formatter.shell] +command = "/bin/sh" +options = [ + "-euc", + """ +# First lint all the scripts +shellcheck "$@" + +# Then format them +shfmt -i 2 -s -w "$@" + """, + "--", # bash swallows the second argument when using -c +] +includes = ["*.sh"] + +[formatter.terraform] +# Careful, only terraform 1.3.0 or later accept a list of files. +# see https://github.com/numtide/treefmt/issues/97 +command = "terraform" +options = ["fmt"] +includes = ["*.tf"] \ No newline at end of file