├── .bumpversion.toml ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .idea ├── .gitignore ├── aws.xml ├── google-java-format.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jpa-buddy.xml ├── misc.xml ├── modules.xml ├── rs-env.iml ├── ruff.xml ├── runConfigurations │ ├── Test_test_edit.xml │ └── rsenv.sh └── vcs.xml ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── doc ├── asciinema_story.md ├── concept.png ├── example │ ├── global │ │ ├── cloud.env │ │ └── global1.env │ ├── local.env │ ├── prod.env │ └── shared │ │ └── level3.env ├── hierarchy.png ├── jetbrain.png └── rsenv.pptx ├── pyproject.toml ├── rsenv ├── Cargo.lock ├── Cargo.toml ├── src │ ├── arena.rs │ ├── builder.rs │ ├── cli │ │ ├── args.rs │ │ ├── commands.rs │ │ └── mod.rs │ ├── edit.rs │ ├── envrc.rs │ ├── errors.rs │ ├── lib.rs │ ├── main.rs │ ├── tree_traits.rs │ └── util │ │ ├── mod.rs │ │ ├── path.rs │ │ └── testing.rs └── tests │ ├── resources │ └── environments │ │ ├── complex │ │ ├── a │ │ │ └── level3.env │ │ ├── dot.envrc │ │ ├── level1.env │ │ ├── level2.env │ │ ├── level4.env │ │ └── result.env │ │ ├── fail │ │ ├── level11.env │ │ └── root.env │ │ ├── graph │ │ ├── level11.env │ │ ├── level12.env │ │ ├── level13.env │ │ ├── level21.env │ │ ├── level31.env │ │ ├── result.env │ │ ├── root.env │ │ └── unlinked.env │ │ ├── graph2 │ │ ├── error.env │ │ ├── global1.env │ │ ├── level21.env │ │ ├── level22.env │ │ ├── result1.env │ │ ├── result2.env │ │ └── shared.env │ │ ├── max_prefix │ │ ├── aws │ │ │ └── root.env │ │ └── confguard │ │ │ └── xxx │ │ │ └── local.env │ │ ├── parallel │ │ ├── a_int.env │ │ ├── a_prod.env │ │ ├── a_test.env │ │ ├── b_int.env │ │ ├── b_prod.env │ │ ├── b_test.env │ │ ├── int.env │ │ ├── openfiles.vim │ │ ├── prod.env │ │ └── test.env │ │ ├── tree │ │ ├── level11.env │ │ ├── level12.env │ │ ├── level13.env │ │ ├── level21.env │ │ ├── level22.env │ │ ├── level32.env │ │ └── root.env │ │ └── tree2 │ │ ├── confguard │ │ ├── level11.env │ │ ├── level12.env │ │ ├── level13.env │ │ ├── level21.env │ │ ├── level22.env │ │ └── subdir │ │ │ └── level32.env │ │ └── root.env │ ├── test_edit.rs │ ├── test_env_vars.rs │ ├── test_lib.rs │ ├── test_tree.rs │ └── test_update_dot_envrc.rs ├── scripts ├── print_env.py └── rsenv.sh ├── src └── example_package │ └── __init__.py └── tests └── __init__.py /.bumpversion.toml: -------------------------------------------------------------------------------- 1 | [tool.bumpversion] 2 | current_version = "1.3.0" 3 | parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" 4 | serialize = ["{major}.{minor}.{patch}"] 5 | search = "{current_version}" 6 | replace = "{new_version}" 7 | regex = false 8 | ignore_missing_version = false 9 | tag = true 10 | sign_tags = false 11 | tag_name = "v{new_version}" 12 | tag_message = "Bump version: {current_version} → {new_version}" 13 | allow_dirty = false 14 | commit = true 15 | message = "Bump version: {current_version} → {new_version}" 16 | commit_args = "" 17 | 18 | [[tool.bumpversion.files]] 19 | filename = "VERSION" 20 | 21 | [[tool.bumpversion.files]] 22 | filename = "./rsenv/Cargo.toml" 23 | search = 'version = "{current_version}"' 24 | replace = 'version = "{new_version}"' 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Test 21 | run: make test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | generated.vim 164 | doc/~$rsenv.pptx 165 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/aws.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/google-java-format.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | -------------------------------------------------------------------------------- /.idea/jpa-buddy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/rs-env.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/ruff.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Test_test_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | -------------------------------------------------------------------------------- /.idea/runConfigurations/rsenv.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # DO NOT DELETE 4 | # 5 | [[ -f "$SOPS_PATH/environments/${RUN_ENV:-local}.env" ]] && rsenv build "$SOPS_PATH/environments/${RUN_ENV:-local}.env" 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2023, [sysid](https://sysid.github.io/). 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | #MAKEFLAGS += --no-print-directory 3 | 4 | # You can set these variables from the command line, and also from the environment for the first two. 5 | PREFIX ?= /usr/local 6 | BINPREFIX ?= "$(PREFIX)/bin" 7 | 8 | VERSION = $(shell cat VERSION) 9 | 10 | SHELL = bash 11 | .ONESHELL: 12 | 13 | app_root := $(if $(PROJ_DIR),$(PROJ_DIR),$(CURDIR)) 14 | pkg_src = $(app_root)/rsenv 15 | tests_src = $(app_root)/rsenv/tests 16 | BINARY = rsenv 17 | 18 | # Makefile directory 19 | CODE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) 20 | 21 | # define files 22 | MANS = $(wildcard ./*.md) 23 | MAN_HTML = $(MANS:.md=.html) 24 | MAN_PAGES = $(MANS:.md=.1) 25 | # avoid circular targets 26 | MAN_BINS = $(filter-out ./tw-extras.md, $(MANS)) 27 | 28 | ################################################################################ 29 | # Admin \ 30 | ADMIN:: ## ################################################################## 31 | .PHONY: init-env 32 | init-env: ## init-env 33 | @rm -fr ~/xxx/* 34 | @mkdir -p ~/xxx 35 | @cp -r $(tests_src)/resources/environments/complex/dot.envrc ~/xxx/.envrc 36 | cat ~/xxx/.envrc 37 | 38 | .PHONY: show-env 39 | show-env: ## show-env 40 | @tree -a ~/xxx 41 | 42 | .PHONY: test 43 | test: ## test 44 | RUST_LOG=DEBUG pushd $(pkg_src) && cargo test -- --test-threads=1 # --nocapture 45 | #RUST_LOG=DEBUG pushd $(pkg_src) && cargo test 46 | 47 | .PHONY: run-edit-leaf 48 | run-edit-leaf: ## run-edit-leaf: expect to open entire branch 49 | pushd $(pkg_src) && cargo run -- edit-leaf tests/resources/environments/tree2/confguard/subdir/level32.env 50 | 51 | .PHONY: run-leaves 52 | run-leaves: ## run-leaves: expect level21, level32, level13, level11 53 | pushd $(pkg_src) && cargo run -- leaves tests/resources/environments/tree2/confguard 54 | 55 | .PHONY: run-edit 56 | run-edit: ## run-edit: expect fzf selection for open 57 | pushd $(pkg_src) && cargo run -- edit ./tests/resources/environments/complex 58 | 59 | .PHONY: run-build 60 | run-build: ## run-build: expect fully compiled env vars list 61 | pushd $(pkg_src) && time cargo run -- -d -d build ./tests/resources/environments/complex/level4.env 62 | 63 | .PHONY: run-select-leaf 64 | run-select-leaf: ## run-select-leaf: expect updated .envrc (idempotent) 65 | rsenv/target/debug/rsenv select-leaf $(SOPS_PATH)/environments/local.env 66 | cat .envrc 67 | 68 | .PHONY: run-select 69 | run-select: ## run-select: select sops env and update .envrc 70 | rsenv/target/debug/rsenv select $(SOPS_PATH) 71 | cat .envrc 72 | 73 | .PHONY: run-files 74 | run-files: ## run-files: create branch 75 | pushd $(pkg_src) && time cargo run -- -d -d files ./tests/resources/environments/complex/level4.env 76 | 77 | ### Expected .enrc entry: 78 | # #------------------------------- rsenv start -------------------------------- 79 | # export PIPENV_VENV_IN_PROJECT=1 # creates .venv 80 | # export PYTHONPATH=$PROJ_DIR 81 | # export RUN_ENV=local 82 | # export SOPS_PATH=$HOME/dev/s/private/sec-sops/confguard/rs-sops-20ae57f0 83 | # export TERRAFORM_PROMPT=0 84 | # export VAR_1=var_1 85 | # export VAR_2=var_2 86 | # export VAR_3=var_31 87 | # export VAR_4=var_42 88 | # export VAR_5=var_53 89 | # export VAR_6=var_64 90 | # export VAR_7=var_74 91 | # export senv="source $PROJ_DIR/scripts/env.sh" 92 | # #-------------------------------- rsenv end --------------------------------- 93 | .PHONY: run-envrc 94 | run-envrc: init-env ## run-envrc: expect above entry in .envrc 95 | pushd $(pkg_src) && time cargo run -- -d -d envrc ./tests/resources/environments/complex/level4.env ~/xxx/.envrc 96 | #pushd $(pkg_src) && time cargo run -- envrc ./tests/resources/environments/complex/level4.env ~/xxx/.envrc 97 | cat ~/xxx/.envrc 98 | 99 | .PHONY: test-fzf-edit 100 | test-fzf-edit: ## test-fzf-edit 101 | pushd $(pkg_src) && cargo test --package rsenv --test test_edit test_select_file_with_suffix -- --exact --nocapture --ignored 102 | 103 | .PHONY: test-edit 104 | test-edit: ## test-edit 105 | pushd $(pkg_src) && cargo test --package rsenv --test test_edit test_open_files_in_editor -- --exact --nocapture --ignored 106 | 107 | .PHONY: test-vimscript 108 | test-vimscript: ## test-vimscript 109 | pushd $(pkg_src) && cargo test --package rsenv --test test_edit test_create_vimscript -- --exact --nocapture --ignored 110 | 111 | ################################################################################ 112 | # Building, Deploying \ 113 | BUILDING: ## ################################################################## 114 | 115 | .PHONY: doc 116 | doc: ## doc 117 | @rustup doc --std 118 | pushd $(pkg_src) && cargo doc --open 119 | 120 | .PHONY: all 121 | all: clean build install ## all 122 | : 123 | 124 | .PHONY: upload 125 | upload: ## upload 126 | @if [ -z "$$CARGO_REGISTRY_TOKEN" ]; then \ 127 | echo "Error: CARGO_REGISTRY_TOKEN is not set"; \ 128 | exit 1; \ 129 | fi 130 | @echo "CARGO_REGISTRY_TOKEN is set" 131 | pushd $(pkg_src) && cargo release publish --execute 132 | 133 | .PHONY: build 134 | build: ## build 135 | pushd $(pkg_src) && cargo build --release 136 | 137 | #.PHONY: install 138 | #install: uninstall ## install 139 | #@cp -vf $(pkg_src)/target/release/$(BINARY) ~/bin/$(BINARY) 140 | .PHONY: install 141 | install: uninstall ## install 142 | @VERSION=$(shell cat VERSION) && \ 143 | echo "-M- Installagin $$VERSION" && \ 144 | cp -vf rsenv/target/release/$(BINARY) ~/bin/$(BINARY)$$VERSION && \ 145 | ln -vsf ~/bin/$(BINARY)$$VERSION ~/bin/$(BINARY) 146 | 147 | 148 | .PHONY: install-runenv 149 | install-runenv: uninstall-runenv ## install-runenv 150 | @cp -vf $(app_root)/scripts/rsenv.sh ~/dev/binx/rsenv.sh 151 | 152 | .PHONY: uninstall-runenv 153 | uninstall-runenv: ## uninstall-runenv 154 | @rm -f ~/dev/binx/rsenv.sh 155 | 156 | 157 | .PHONY: uninstall 158 | uninstall: ## uninstall 159 | -@test -f ~/bin/$(BINARY) && rm -v ~/bin/$(BINARY) 160 | 161 | .PHONY: bump-major 162 | bump-major: ## bump-major, tag and push 163 | bump-my-version bump --commit --tag major 164 | git push 165 | git push --tags 166 | @$(MAKE) create-release 167 | 168 | .PHONY: bump-minor 169 | bump-minor: ## bump-minor, tag and push 170 | bump-my-version bump --commit --tag minor 171 | git push 172 | git push --tags 173 | @$(MAKE) create-release 174 | 175 | .PHONY: bump-patch 176 | bump-patch: ## bump-patch, tag and push 177 | bump-my-version bump --commit --tag patch 178 | git push 179 | git push --tags 180 | @$(MAKE) create-release 181 | 182 | .PHONY: style 183 | style: ## style 184 | pushd $(pkg_src) && cargo fmt 185 | 186 | .PHONY: lint 187 | lint: ## lint 188 | pushd $(pkg_src) && cargo clippy 189 | 190 | .PHONY: create-release 191 | create-release: ## create a release on GitHub via the gh cli 192 | @if command -v gh version &>/dev/null; then \ 193 | echo "Creating GitHub release for v$(VERSION)"; \ 194 | gh release create "v$(VERSION)" --generate-notes; \ 195 | else \ 196 | echo "You do not have the github-cli installed. Please create release from the repo manually."; \ 197 | exit 1; \ 198 | fi 199 | 200 | 201 | ################################################################################ 202 | # Clean \ 203 | CLEAN: ## ############################################################ 204 | 205 | .PHONY: clean 206 | clean:clean-rs ## clean all 207 | : 208 | 209 | .PHONY: clean-build 210 | clean-build: ## remove build artifacts 211 | rm -fr build/ 212 | rm -fr dist/ 213 | rm -fr .eggs/ 214 | find . \( -path ./env -o -path ./venv -o -path ./.env -o -path ./.venv \) -prune -o -name '*.egg-info' -exec rm -fr {} + 215 | find . \( -path ./env -o -path ./venv -o -path ./.env -o -path ./.venv \) -prune -o -name '*.egg' -exec rm -f {} + 216 | 217 | .PHONY: clean-pyc 218 | clean-pyc: ## remove Python file artifacts 219 | find . -name '*.pyc' -exec rm -f {} + 220 | find . -name '*.pyo' -exec rm -f {} + 221 | find . -name '*~' -exec rm -f {} + 222 | find . -name '__pycache__' -exec rm -fr {} + 223 | 224 | .PHONY: clean-rs 225 | clean-rs: ## clean-rs 226 | pushd $(pkg_src) && cargo clean -v 227 | 228 | ################################################################################ 229 | # Misc \ 230 | MISC: ## ############################################################ 231 | 232 | define PRINT_HELP_PYSCRIPT 233 | import re, sys 234 | 235 | for line in sys.stdin: 236 | match = re.match(r'^([%a-zA-Z0-9_-]+):.*?## (.*)$$', line) 237 | if match: 238 | target, help = match.groups() 239 | if target != "dummy": 240 | print("\033[36m%-20s\033[0m %s" % (target, help)) 241 | endef 242 | export PRINT_HELP_PYSCRIPT 243 | 244 | .PHONY: help 245 | help: 246 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 247 | 248 | debug: ## debug 249 | @echo "-D- CODE_DIR: $(CODE_DIR)" 250 | 251 | 252 | .PHONY: list 253 | list: * ## list 254 | @echo $^ 255 | 256 | .PHONY: list2 257 | %: %.md ## list2 258 | @echo $^ 259 | 260 | 261 | %-plan: ## call with: make -plan 262 | @echo $@ : $* 263 | @echo $@ : $^ 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rs-env 2 | 3 | > [Hierarchical environment variable management](https://sysid.github.io/hierarchical-environment-variable-management/) 4 | 5 | ## Why 6 | Managing environment variables for different stages, regions, etc. is an unavoidable chore 7 | when working on cloud projects. 8 | 9 | Especially the challenge of avoiding duplication and knowing where a particular value is coming from. 10 | Hierarchical variable management seems to be a good solution for this problem. 11 | 12 | # Features 13 | - Compile a resulting set of environment variables from a linked list of `.env` files. 14 | - Linked `.env` files form trees. Paths from leave-nodes to root (branches) form the resulting set of variables. 15 | - Last defined variable wins, i.e. child tops parent. 16 | - Smart environment selection via builtin FZF (fuzzy find). 17 | - Quick edit via builtin FZF. 18 | - Side-by-side Tree edit. 19 | - [direnv](https://direnv.net/) integration: Have the resulting variable list written to your `.envrc` file. 20 | - [JetBrains](https://www.jetbrains.com/) integration via [EnvFile](https://plugins.jetbrains.com/plugin/7861-envfile) plugin. 21 | 22 | ### Concept 23 | ![concept](doc/concept.png) 24 | 25 | 26 | ### Installation 27 | ```bash 28 | cargo install rs-env 29 | ``` 30 | 31 | ### Usage 32 | The resulting set of environment variables comes from a merge of all linked `.env` files. 33 | 34 | - **branch**: a linear list of files, each file can have one parent (no DAG). 35 | - **tree**: a collection of branches (files can be part of multiple branches, but only one parent) 36 | - environment variables are defined in files `.env` and must be prefixed with `export` command 37 | - See [examples](./rsenv/tests/resources/environments) 38 | - multiple trees/branches per project are supported 39 | - files are linked by adding the comment line `# rsenv: ` or via: `rsenv link .env .env`. 40 | 41 | Publish the resulting set of variables to the shell: 42 | ```bash 43 | source <(rsenv build ) 44 | ``` 45 | 46 | ``` 47 | Hierarchical environment variable management 48 | 49 | Usage: rsenv [OPTIONS] [NAME] [COMMAND] 50 | 51 | Commands: 52 | build Build and display the complete set of environment variables 53 | envrc Write environment variables to .envrc file (requires direnv) 54 | files List all files in the environment hierarchy 55 | edit-leaf Edit an environment file and all its parent files 56 | edit Interactively select and edit an environment hierarchy 57 | select-leaf Update .envrc with selected environment (requires direnv) 58 | select Interactively select environment and update .envrc (requires direnv) 59 | link Create parent-child relationships between environment files 60 | branches Show all branches (linear representation) 61 | tree Show all trees (hierarchical representation) 62 | tree-edit Edit all environment hierarchies side-by-side (requires vim) 63 | leaves List all leaf environment files 64 | help Print this message or the help of the given subcommand(s) 65 | 66 | Arguments: 67 | [NAME] Name of the configuration to operate on (optional) 68 | 69 | Options: 70 | -d, --debug... Enable debug logging. Multiple flags (-d, -dd, -ddd) increase verbosity 71 | --generate Generate shell completion scripts [possible values: bash, elvish, fish, powershell, zsh] 72 | --info Display version and configuration information 73 | -h, --help Print help 74 | -V, --version Print version 75 | ``` 76 | 77 | #### Basic 78 | 79 |
80 | 81 | #### Select via FZF 82 | 83 |
84 | 85 | #### Tree and Branch structure (Smart edit) 86 | 87 |
88 | 89 | ## Integrations 90 | ### direnv 91 | [direnv](https://direnv.net/) activates environments automatically. 92 | - rs-env can update the `.envrc` file with the dependency graph variables. 93 | 94 | 95 | ### JetBrains Integration 96 | Life injection of environment variables: 97 | - Plugin [EnvFile](https://plugins.jetbrains.com/plugin/7861-envfile) can be used to life-inject environment variables. 98 | - Use the script `runenv.sh` as the "EnvFile" script (tick executable checkbox !). 99 | - The environment variable `RUN_ENV` parametrizes which environment to load. 100 | - It will look for a file `.env` in the specified directory. 101 | 102 | [![jetbrain](doc/jetbrain.png)](doc/jetbrain.png) 103 | 104 | 105 | 106 | ## Development 107 | - Tests for "skim" need valid terminal, so they are run via Makefile. 108 | - Test for `rsenv select`: run debug target and check rsenv .envrc file. 109 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.3.0 2 | -------------------------------------------------------------------------------- /doc/asciinema_story.md: -------------------------------------------------------------------------------- 1 | # Storyline rsenv 2 | 3 | ## Prep 4 | - make font much bigger !!! 5 | - clean .envrc 6 | - rm *.env 7 | 8 | ## Run 9 | ```bash 10 | asciinema rec --overwrite -i 3 /tmp/rsenv.cast 11 | 12 | rsenv 13 | cls 14 | 15 | vim -o root.env l1.env l2.env 16 | rsenv build l2.env 17 | rsenv files l2.env 18 | rsenv edit . 19 | rm *.env 20 | 21 | 22 | rsenv 23 | cls 24 | tree 25 | 26 | more .envrc 27 | rsenv build environments/complex/ 28 | rsenv envrc environments/complex/level4.env 29 | more .envrc 30 | 31 | cls; asciinema rec --overwrite -i 3 /tmp/rsenv3.cast 32 | rsenv tree environments/parallel/ 33 | rsenv tree-edit environments/parallel/ 34 | # replace q-nr 35 | 36 | cls; asciinema rec --overwrite -i 3 /tmp/rsenv4.cast 37 | cls 38 | rsenv 39 | more .envrc 40 | rsenv select . # prod 41 | more .envrc 42 | ``` 43 | 44 | ## Close 45 | 46 | CTRL-D 47 | -------------------------------------------------------------------------------- /doc/concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysid/rs-env/86e82958e53092fc6be7d0b91e4688faf50a8feb/doc/concept.png -------------------------------------------------------------------------------- /doc/example/global/cloud.env: -------------------------------------------------------------------------------- 1 | # rsenv: global.env 2 | 3 | # Level2 overwrite 4 | export VAR_4=cloud_42 5 | export VAR_5=cloud_52 6 | -------------------------------------------------------------------------------- /doc/example/global/global1.env: -------------------------------------------------------------------------------- 1 | # Level1 overwrite 2 | export VAR_1=var_11 3 | export VAR_2=var_21 4 | export VAR_3=var_31 5 | -------------------------------------------------------------------------------- /doc/example/local.env: -------------------------------------------------------------------------------- 1 | # rsenv: shared/level3.env 2 | 3 | other stuff 4 | # comments 5 | 6 | # new variables just added 7 | export VAR_6=local_64 8 | export VAR_7=local_74 9 | -------------------------------------------------------------------------------- /doc/example/prod.env: -------------------------------------------------------------------------------- 1 | # rsenv shared/level3.env 2 | 3 | other stuff 4 | # comments 5 | 6 | # new variables just added 7 | export VAR_6=prod_64 8 | export VAR_7=prod_74 9 | -------------------------------------------------------------------------------- /doc/example/shared/level3.env: -------------------------------------------------------------------------------- 1 | # rsenv: ../global/cloud.env 2 | 3 | # Level3 overwrite 4 | export VAR_5=var_53 5 | export VAR_6=var_63 # new variable added 6 | -------------------------------------------------------------------------------- /doc/hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysid/rs-env/86e82958e53092fc6be7d0b91e4688faf50a8feb/doc/hierarchy.png -------------------------------------------------------------------------------- /doc/jetbrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysid/rs-env/86e82958e53092fc6be7d0b91e4688faf50a8feb/doc/jetbrain.png -------------------------------------------------------------------------------- /doc/rsenv.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysid/rs-env/86e82958e53092fc6be7d0b91e4688faf50a8feb/doc/rsenv.pptx -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "" 3 | version = "" 4 | description = "" 5 | authors = [ 6 | {name = "sysid", email = "sysid@gmx.de"}, 7 | ] 8 | dependencies = [] 9 | requires-python = ">=3.11" 10 | readme = "README.md" 11 | license = {text = "MIT"} 12 | 13 | -------------------------------------------------------------------------------- /rsenv/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys 0.59.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.6" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.59.0", 76 | ] 77 | 78 | [[package]] 79 | name = "anyhow" 80 | version = "1.0.95" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 83 | 84 | [[package]] 85 | name = "arrayvec" 86 | version = "0.7.4" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 89 | 90 | [[package]] 91 | name = "atty" 92 | version = "0.2.14" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 95 | dependencies = [ 96 | "hermit-abi", 97 | "libc", 98 | "winapi", 99 | ] 100 | 101 | [[package]] 102 | name = "autocfg" 103 | version = "1.4.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 106 | 107 | [[package]] 108 | name = "beef" 109 | version = "0.5.2" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" 112 | 113 | [[package]] 114 | name = "bitflags" 115 | version = "1.3.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 118 | 119 | [[package]] 120 | name = "bitflags" 121 | version = "2.5.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 124 | 125 | [[package]] 126 | name = "bumpalo" 127 | version = "3.16.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 130 | 131 | [[package]] 132 | name = "cc" 133 | version = "1.2.5" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" 136 | dependencies = [ 137 | "shlex", 138 | ] 139 | 140 | [[package]] 141 | name = "cfg-if" 142 | version = "1.0.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 145 | 146 | [[package]] 147 | name = "chrono" 148 | version = "0.4.39" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 151 | dependencies = [ 152 | "android-tzdata", 153 | "iana-time-zone", 154 | "js-sys", 155 | "num-traits", 156 | "wasm-bindgen", 157 | "windows-targets 0.52.6", 158 | ] 159 | 160 | [[package]] 161 | name = "clap" 162 | version = "3.2.25" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 165 | dependencies = [ 166 | "atty", 167 | "bitflags 1.3.2", 168 | "clap_lex 0.2.4", 169 | "indexmap", 170 | "once_cell", 171 | "strsim 0.10.0", 172 | "termcolor", 173 | "textwrap", 174 | ] 175 | 176 | [[package]] 177 | name = "clap" 178 | version = "4.5.23" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 181 | dependencies = [ 182 | "clap_builder", 183 | "clap_derive", 184 | ] 185 | 186 | [[package]] 187 | name = "clap_builder" 188 | version = "4.5.23" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 191 | dependencies = [ 192 | "anstream", 193 | "anstyle", 194 | "clap_lex 0.7.4", 195 | "strsim 0.11.1", 196 | ] 197 | 198 | [[package]] 199 | name = "clap_complete" 200 | version = "4.5.40" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "ac2e663e3e3bed2d32d065a8404024dad306e699a04263ec59919529f803aee9" 203 | dependencies = [ 204 | "clap 4.5.23", 205 | ] 206 | 207 | [[package]] 208 | name = "clap_derive" 209 | version = "4.5.18" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 212 | dependencies = [ 213 | "heck", 214 | "proc-macro2", 215 | "quote", 216 | "syn 2.0.91", 217 | ] 218 | 219 | [[package]] 220 | name = "clap_lex" 221 | version = "0.2.4" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 224 | dependencies = [ 225 | "os_str_bytes", 226 | ] 227 | 228 | [[package]] 229 | name = "clap_lex" 230 | version = "0.7.4" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 233 | 234 | [[package]] 235 | name = "colorchoice" 236 | version = "1.0.3" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 239 | 240 | [[package]] 241 | name = "colored" 242 | version = "2.2.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 245 | dependencies = [ 246 | "lazy_static", 247 | "windows-sys 0.59.0", 248 | ] 249 | 250 | [[package]] 251 | name = "core-foundation-sys" 252 | version = "0.8.7" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 255 | 256 | [[package]] 257 | name = "crossbeam" 258 | version = "0.8.4" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" 261 | dependencies = [ 262 | "crossbeam-channel", 263 | "crossbeam-deque", 264 | "crossbeam-epoch", 265 | "crossbeam-queue", 266 | "crossbeam-utils", 267 | ] 268 | 269 | [[package]] 270 | name = "crossbeam-channel" 271 | version = "0.5.12" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" 274 | dependencies = [ 275 | "crossbeam-utils", 276 | ] 277 | 278 | [[package]] 279 | name = "crossbeam-deque" 280 | version = "0.8.5" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 283 | dependencies = [ 284 | "crossbeam-epoch", 285 | "crossbeam-utils", 286 | ] 287 | 288 | [[package]] 289 | name = "crossbeam-epoch" 290 | version = "0.9.18" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 293 | dependencies = [ 294 | "crossbeam-utils", 295 | ] 296 | 297 | [[package]] 298 | name = "crossbeam-queue" 299 | version = "0.3.11" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" 302 | dependencies = [ 303 | "crossbeam-utils", 304 | ] 305 | 306 | [[package]] 307 | name = "crossbeam-utils" 308 | version = "0.8.19" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 311 | 312 | [[package]] 313 | name = "crossterm" 314 | version = "0.27.0" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 317 | dependencies = [ 318 | "bitflags 2.5.0", 319 | "crossterm_winapi", 320 | "libc", 321 | "mio", 322 | "parking_lot", 323 | "signal-hook", 324 | "signal-hook-mio", 325 | "winapi", 326 | ] 327 | 328 | [[package]] 329 | name = "crossterm_winapi" 330 | version = "0.9.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 333 | dependencies = [ 334 | "winapi", 335 | ] 336 | 337 | [[package]] 338 | name = "ctor" 339 | version = "0.2.9" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" 342 | dependencies = [ 343 | "quote", 344 | "syn 2.0.91", 345 | ] 346 | 347 | [[package]] 348 | name = "darling" 349 | version = "0.14.4" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" 352 | dependencies = [ 353 | "darling_core", 354 | "darling_macro", 355 | ] 356 | 357 | [[package]] 358 | name = "darling_core" 359 | version = "0.14.4" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" 362 | dependencies = [ 363 | "fnv", 364 | "ident_case", 365 | "proc-macro2", 366 | "quote", 367 | "strsim 0.10.0", 368 | "syn 1.0.109", 369 | ] 370 | 371 | [[package]] 372 | name = "darling_macro" 373 | version = "0.14.4" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" 376 | dependencies = [ 377 | "darling_core", 378 | "quote", 379 | "syn 1.0.109", 380 | ] 381 | 382 | [[package]] 383 | name = "defer-drop" 384 | version = "1.3.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "f613ec9fa66a6b28cdb1842b27f9adf24f39f9afc4dcdd9fdecee4aca7945c57" 387 | dependencies = [ 388 | "crossbeam-channel", 389 | "once_cell", 390 | ] 391 | 392 | [[package]] 393 | name = "deranged" 394 | version = "0.3.11" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 397 | dependencies = [ 398 | "powerfmt", 399 | ] 400 | 401 | [[package]] 402 | name = "derive_builder" 403 | version = "0.11.2" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" 406 | dependencies = [ 407 | "derive_builder_macro", 408 | ] 409 | 410 | [[package]] 411 | name = "derive_builder_core" 412 | version = "0.11.2" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" 415 | dependencies = [ 416 | "darling", 417 | "proc-macro2", 418 | "quote", 419 | "syn 1.0.109", 420 | ] 421 | 422 | [[package]] 423 | name = "derive_builder_macro" 424 | version = "0.11.2" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" 427 | dependencies = [ 428 | "derive_builder_core", 429 | "syn 1.0.109", 430 | ] 431 | 432 | [[package]] 433 | name = "dirs-next" 434 | version = "2.0.0" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 437 | dependencies = [ 438 | "cfg-if", 439 | "dirs-sys-next", 440 | ] 441 | 442 | [[package]] 443 | name = "dirs-sys-next" 444 | version = "0.1.2" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 447 | dependencies = [ 448 | "libc", 449 | "redox_users", 450 | "winapi", 451 | ] 452 | 453 | [[package]] 454 | name = "either" 455 | version = "1.11.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" 458 | 459 | [[package]] 460 | name = "env_logger" 461 | version = "0.9.3" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" 464 | dependencies = [ 465 | "atty", 466 | "humantime", 467 | "log", 468 | "regex", 469 | "termcolor", 470 | ] 471 | 472 | [[package]] 473 | name = "errno" 474 | version = "0.3.10" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 477 | dependencies = [ 478 | "libc", 479 | "windows-sys 0.59.0", 480 | ] 481 | 482 | [[package]] 483 | name = "fastrand" 484 | version = "2.3.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 487 | 488 | [[package]] 489 | name = "fnv" 490 | version = "1.0.7" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 493 | 494 | [[package]] 495 | name = "fs_extra" 496 | version = "1.3.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 499 | 500 | [[package]] 501 | name = "futures" 502 | version = "0.3.30" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 505 | dependencies = [ 506 | "futures-channel", 507 | "futures-core", 508 | "futures-executor", 509 | "futures-io", 510 | "futures-sink", 511 | "futures-task", 512 | "futures-util", 513 | ] 514 | 515 | [[package]] 516 | name = "futures-channel" 517 | version = "0.3.30" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 520 | dependencies = [ 521 | "futures-core", 522 | "futures-sink", 523 | ] 524 | 525 | [[package]] 526 | name = "futures-core" 527 | version = "0.3.30" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 530 | 531 | [[package]] 532 | name = "futures-executor" 533 | version = "0.3.30" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 536 | dependencies = [ 537 | "futures-core", 538 | "futures-task", 539 | "futures-util", 540 | ] 541 | 542 | [[package]] 543 | name = "futures-io" 544 | version = "0.3.30" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 547 | 548 | [[package]] 549 | name = "futures-macro" 550 | version = "0.3.30" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 553 | dependencies = [ 554 | "proc-macro2", 555 | "quote", 556 | "syn 2.0.91", 557 | ] 558 | 559 | [[package]] 560 | name = "futures-sink" 561 | version = "0.3.30" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 564 | 565 | [[package]] 566 | name = "futures-task" 567 | version = "0.3.30" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 570 | 571 | [[package]] 572 | name = "futures-timer" 573 | version = "3.0.3" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 576 | 577 | [[package]] 578 | name = "futures-util" 579 | version = "0.3.30" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 582 | dependencies = [ 583 | "futures-channel", 584 | "futures-core", 585 | "futures-io", 586 | "futures-macro", 587 | "futures-sink", 588 | "futures-task", 589 | "memchr", 590 | "pin-project-lite", 591 | "pin-utils", 592 | "slab", 593 | ] 594 | 595 | [[package]] 596 | name = "fuzzy-matcher" 597 | version = "0.3.7" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 600 | dependencies = [ 601 | "thread_local", 602 | ] 603 | 604 | [[package]] 605 | name = "generational-arena" 606 | version = "0.2.9" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "877e94aff08e743b651baaea359664321055749b398adff8740a7399af7796e7" 609 | dependencies = [ 610 | "cfg-if", 611 | ] 612 | 613 | [[package]] 614 | name = "getrandom" 615 | version = "0.2.15" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 618 | dependencies = [ 619 | "cfg-if", 620 | "libc", 621 | "wasi", 622 | ] 623 | 624 | [[package]] 625 | name = "glob" 626 | version = "0.3.1" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 629 | 630 | [[package]] 631 | name = "hashbrown" 632 | version = "0.12.3" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 635 | 636 | [[package]] 637 | name = "heck" 638 | version = "0.5.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 641 | 642 | [[package]] 643 | name = "hermit-abi" 644 | version = "0.1.19" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 647 | dependencies = [ 648 | "libc", 649 | ] 650 | 651 | [[package]] 652 | name = "humantime" 653 | version = "2.1.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 656 | 657 | [[package]] 658 | name = "iana-time-zone" 659 | version = "0.1.61" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 662 | dependencies = [ 663 | "android_system_properties", 664 | "core-foundation-sys", 665 | "iana-time-zone-haiku", 666 | "js-sys", 667 | "wasm-bindgen", 668 | "windows-core", 669 | ] 670 | 671 | [[package]] 672 | name = "iana-time-zone-haiku" 673 | version = "0.1.2" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 676 | dependencies = [ 677 | "cc", 678 | ] 679 | 680 | [[package]] 681 | name = "ident_case" 682 | version = "1.0.1" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 685 | 686 | [[package]] 687 | name = "indexmap" 688 | version = "1.9.3" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 691 | dependencies = [ 692 | "autocfg", 693 | "hashbrown", 694 | ] 695 | 696 | [[package]] 697 | name = "is_terminal_polyfill" 698 | version = "1.70.1" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 701 | 702 | [[package]] 703 | name = "itertools" 704 | version = "0.12.1" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 707 | dependencies = [ 708 | "either", 709 | ] 710 | 711 | [[package]] 712 | name = "js-sys" 713 | version = "0.3.76" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" 716 | dependencies = [ 717 | "once_cell", 718 | "wasm-bindgen", 719 | ] 720 | 721 | [[package]] 722 | name = "lazy_static" 723 | version = "1.5.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 726 | 727 | [[package]] 728 | name = "libc" 729 | version = "0.2.169" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 732 | 733 | [[package]] 734 | name = "libredox" 735 | version = "0.1.3" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 738 | dependencies = [ 739 | "bitflags 2.5.0", 740 | "libc", 741 | ] 742 | 743 | [[package]] 744 | name = "linux-raw-sys" 745 | version = "0.4.14" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 748 | 749 | [[package]] 750 | name = "lock_api" 751 | version = "0.4.11" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 754 | dependencies = [ 755 | "autocfg", 756 | "scopeguard", 757 | ] 758 | 759 | [[package]] 760 | name = "log" 761 | version = "0.4.22" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 764 | 765 | [[package]] 766 | name = "matchers" 767 | version = "0.1.0" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 770 | dependencies = [ 771 | "regex-automata 0.1.10", 772 | ] 773 | 774 | [[package]] 775 | name = "memchr" 776 | version = "2.7.4" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 779 | 780 | [[package]] 781 | name = "memoffset" 782 | version = "0.6.5" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 785 | dependencies = [ 786 | "autocfg", 787 | ] 788 | 789 | [[package]] 790 | name = "mio" 791 | version = "0.8.11" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 794 | dependencies = [ 795 | "libc", 796 | "log", 797 | "wasi", 798 | "windows-sys 0.48.0", 799 | ] 800 | 801 | [[package]] 802 | name = "nix" 803 | version = "0.24.3" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" 806 | dependencies = [ 807 | "bitflags 1.3.2", 808 | "cfg-if", 809 | "libc", 810 | ] 811 | 812 | [[package]] 813 | name = "nix" 814 | version = "0.25.1" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" 817 | dependencies = [ 818 | "autocfg", 819 | "bitflags 1.3.2", 820 | "cfg-if", 821 | "libc", 822 | "memoffset", 823 | "pin-utils", 824 | ] 825 | 826 | [[package]] 827 | name = "nu-ansi-term" 828 | version = "0.46.0" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 831 | dependencies = [ 832 | "overload", 833 | "winapi", 834 | ] 835 | 836 | [[package]] 837 | name = "num-conv" 838 | version = "0.1.0" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 841 | 842 | [[package]] 843 | name = "num-traits" 844 | version = "0.2.19" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 847 | dependencies = [ 848 | "autocfg", 849 | ] 850 | 851 | [[package]] 852 | name = "once_cell" 853 | version = "1.20.2" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 856 | 857 | [[package]] 858 | name = "os_str_bytes" 859 | version = "6.6.1" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 862 | 863 | [[package]] 864 | name = "overload" 865 | version = "0.1.1" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 868 | 869 | [[package]] 870 | name = "parking_lot" 871 | version = "0.12.1" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 874 | dependencies = [ 875 | "lock_api", 876 | "parking_lot_core", 877 | ] 878 | 879 | [[package]] 880 | name = "parking_lot_core" 881 | version = "0.9.9" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 884 | dependencies = [ 885 | "cfg-if", 886 | "libc", 887 | "redox_syscall", 888 | "smallvec", 889 | "windows-targets 0.48.5", 890 | ] 891 | 892 | [[package]] 893 | name = "pathdiff" 894 | version = "0.2.3" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 897 | 898 | [[package]] 899 | name = "pin-project-lite" 900 | version = "0.2.14" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 903 | 904 | [[package]] 905 | name = "pin-utils" 906 | version = "0.1.0" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 909 | 910 | [[package]] 911 | name = "powerfmt" 912 | version = "0.2.0" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 915 | 916 | [[package]] 917 | name = "proc-macro2" 918 | version = "1.0.92" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 921 | dependencies = [ 922 | "unicode-ident", 923 | ] 924 | 925 | [[package]] 926 | name = "quote" 927 | version = "1.0.37" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 930 | dependencies = [ 931 | "proc-macro2", 932 | ] 933 | 934 | [[package]] 935 | name = "rayon" 936 | version = "1.10.0" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 939 | dependencies = [ 940 | "either", 941 | "rayon-core", 942 | ] 943 | 944 | [[package]] 945 | name = "rayon-core" 946 | version = "1.12.1" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 949 | dependencies = [ 950 | "crossbeam-deque", 951 | "crossbeam-utils", 952 | ] 953 | 954 | [[package]] 955 | name = "redox_syscall" 956 | version = "0.4.1" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 959 | dependencies = [ 960 | "bitflags 1.3.2", 961 | ] 962 | 963 | [[package]] 964 | name = "redox_users" 965 | version = "0.4.5" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" 968 | dependencies = [ 969 | "getrandom", 970 | "libredox", 971 | "thiserror 1.0.58", 972 | ] 973 | 974 | [[package]] 975 | name = "regex" 976 | version = "1.11.1" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 979 | dependencies = [ 980 | "aho-corasick", 981 | "memchr", 982 | "regex-automata 0.4.9", 983 | "regex-syntax 0.8.5", 984 | ] 985 | 986 | [[package]] 987 | name = "regex-automata" 988 | version = "0.1.10" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 991 | dependencies = [ 992 | "regex-syntax 0.6.29", 993 | ] 994 | 995 | [[package]] 996 | name = "regex-automata" 997 | version = "0.4.9" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1000 | dependencies = [ 1001 | "aho-corasick", 1002 | "memchr", 1003 | "regex-syntax 0.8.5", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "regex-syntax" 1008 | version = "0.6.29" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1011 | 1012 | [[package]] 1013 | name = "regex-syntax" 1014 | version = "0.8.5" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1017 | 1018 | [[package]] 1019 | name = "relative-path" 1020 | version = "1.9.2" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" 1023 | 1024 | [[package]] 1025 | name = "rsenv" 1026 | version = "1.3.0" 1027 | dependencies = [ 1028 | "anyhow", 1029 | "clap 4.5.23", 1030 | "clap_complete", 1031 | "colored", 1032 | "crossbeam", 1033 | "crossterm", 1034 | "ctor", 1035 | "fs_extra", 1036 | "generational-arena", 1037 | "itertools", 1038 | "lazy_static", 1039 | "pathdiff", 1040 | "regex", 1041 | "rstest", 1042 | "skim", 1043 | "tempfile", 1044 | "termtree", 1045 | "thiserror 2.0.9", 1046 | "tracing", 1047 | "tracing-subscriber", 1048 | "walkdir", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "rstest" 1053 | version = "0.19.0" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" 1056 | dependencies = [ 1057 | "futures", 1058 | "futures-timer", 1059 | "rstest_macros", 1060 | "rustc_version", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "rstest_macros" 1065 | version = "0.19.0" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" 1068 | dependencies = [ 1069 | "cfg-if", 1070 | "glob", 1071 | "proc-macro2", 1072 | "quote", 1073 | "regex", 1074 | "relative-path", 1075 | "rustc_version", 1076 | "syn 2.0.91", 1077 | "unicode-ident", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "rustc_version" 1082 | version = "0.4.0" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 1085 | dependencies = [ 1086 | "semver", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "rustix" 1091 | version = "0.38.42" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 1094 | dependencies = [ 1095 | "bitflags 2.5.0", 1096 | "errno", 1097 | "libc", 1098 | "linux-raw-sys", 1099 | "windows-sys 0.59.0", 1100 | ] 1101 | 1102 | [[package]] 1103 | name = "rustversion" 1104 | version = "1.0.15" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" 1107 | 1108 | [[package]] 1109 | name = "same-file" 1110 | version = "1.0.6" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1113 | dependencies = [ 1114 | "winapi-util", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "scopeguard" 1119 | version = "1.2.0" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1122 | 1123 | [[package]] 1124 | name = "semver" 1125 | version = "1.0.22" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" 1128 | 1129 | [[package]] 1130 | name = "serde" 1131 | version = "1.0.197" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 1134 | dependencies = [ 1135 | "serde_derive", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "serde_derive" 1140 | version = "1.0.197" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 1143 | dependencies = [ 1144 | "proc-macro2", 1145 | "quote", 1146 | "syn 2.0.91", 1147 | ] 1148 | 1149 | [[package]] 1150 | name = "sharded-slab" 1151 | version = "0.1.7" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1154 | dependencies = [ 1155 | "lazy_static", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "shlex" 1160 | version = "1.3.0" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1163 | 1164 | [[package]] 1165 | name = "signal-hook" 1166 | version = "0.3.17" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1169 | dependencies = [ 1170 | "libc", 1171 | "signal-hook-registry", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "signal-hook-mio" 1176 | version = "0.2.3" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 1179 | dependencies = [ 1180 | "libc", 1181 | "mio", 1182 | "signal-hook", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "signal-hook-registry" 1187 | version = "1.4.1" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1190 | dependencies = [ 1191 | "libc", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "skim" 1196 | version = "0.10.4" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "e5d28de0a6cb2cdd83a076f1de9d965b973ae08b244df1aa70b432946dda0f32" 1199 | dependencies = [ 1200 | "atty", 1201 | "beef", 1202 | "bitflags 1.3.2", 1203 | "chrono", 1204 | "clap 3.2.25", 1205 | "crossbeam", 1206 | "defer-drop", 1207 | "derive_builder", 1208 | "env_logger", 1209 | "fuzzy-matcher", 1210 | "lazy_static", 1211 | "log", 1212 | "nix 0.25.1", 1213 | "rayon", 1214 | "regex", 1215 | "shlex", 1216 | "time", 1217 | "timer", 1218 | "tuikit", 1219 | "unicode-width", 1220 | "vte", 1221 | ] 1222 | 1223 | [[package]] 1224 | name = "slab" 1225 | version = "0.4.9" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1228 | dependencies = [ 1229 | "autocfg", 1230 | ] 1231 | 1232 | [[package]] 1233 | name = "smallvec" 1234 | version = "1.13.2" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1237 | 1238 | [[package]] 1239 | name = "strsim" 1240 | version = "0.10.0" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1243 | 1244 | [[package]] 1245 | name = "strsim" 1246 | version = "0.11.1" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1249 | 1250 | [[package]] 1251 | name = "syn" 1252 | version = "1.0.109" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1255 | dependencies = [ 1256 | "proc-macro2", 1257 | "quote", 1258 | "unicode-ident", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "syn" 1263 | version = "2.0.91" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" 1266 | dependencies = [ 1267 | "proc-macro2", 1268 | "quote", 1269 | "unicode-ident", 1270 | ] 1271 | 1272 | [[package]] 1273 | name = "tempfile" 1274 | version = "3.15.0" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" 1277 | dependencies = [ 1278 | "cfg-if", 1279 | "fastrand", 1280 | "getrandom", 1281 | "once_cell", 1282 | "rustix", 1283 | "windows-sys 0.59.0", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "term" 1288 | version = "0.7.0" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" 1291 | dependencies = [ 1292 | "dirs-next", 1293 | "rustversion", 1294 | "winapi", 1295 | ] 1296 | 1297 | [[package]] 1298 | name = "termcolor" 1299 | version = "1.4.1" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1302 | dependencies = [ 1303 | "winapi-util", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "termtree" 1308 | version = "0.4.1" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 1311 | 1312 | [[package]] 1313 | name = "textwrap" 1314 | version = "0.16.1" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 1317 | 1318 | [[package]] 1319 | name = "thiserror" 1320 | version = "1.0.58" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" 1323 | dependencies = [ 1324 | "thiserror-impl 1.0.58", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "thiserror" 1329 | version = "2.0.9" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" 1332 | dependencies = [ 1333 | "thiserror-impl 2.0.9", 1334 | ] 1335 | 1336 | [[package]] 1337 | name = "thiserror-impl" 1338 | version = "1.0.58" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" 1341 | dependencies = [ 1342 | "proc-macro2", 1343 | "quote", 1344 | "syn 2.0.91", 1345 | ] 1346 | 1347 | [[package]] 1348 | name = "thiserror-impl" 1349 | version = "2.0.9" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" 1352 | dependencies = [ 1353 | "proc-macro2", 1354 | "quote", 1355 | "syn 2.0.91", 1356 | ] 1357 | 1358 | [[package]] 1359 | name = "thread_local" 1360 | version = "1.1.8" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1363 | dependencies = [ 1364 | "cfg-if", 1365 | "once_cell", 1366 | ] 1367 | 1368 | [[package]] 1369 | name = "time" 1370 | version = "0.3.36" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1373 | dependencies = [ 1374 | "deranged", 1375 | "num-conv", 1376 | "powerfmt", 1377 | "serde", 1378 | "time-core", 1379 | ] 1380 | 1381 | [[package]] 1382 | name = "time-core" 1383 | version = "0.1.2" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1386 | 1387 | [[package]] 1388 | name = "timer" 1389 | version = "0.2.0" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" 1392 | dependencies = [ 1393 | "chrono", 1394 | ] 1395 | 1396 | [[package]] 1397 | name = "tracing" 1398 | version = "0.1.41" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1401 | dependencies = [ 1402 | "pin-project-lite", 1403 | "tracing-attributes", 1404 | "tracing-core", 1405 | ] 1406 | 1407 | [[package]] 1408 | name = "tracing-attributes" 1409 | version = "0.1.28" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1412 | dependencies = [ 1413 | "proc-macro2", 1414 | "quote", 1415 | "syn 2.0.91", 1416 | ] 1417 | 1418 | [[package]] 1419 | name = "tracing-core" 1420 | version = "0.1.33" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1423 | dependencies = [ 1424 | "once_cell", 1425 | "valuable", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "tracing-log" 1430 | version = "0.2.0" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1433 | dependencies = [ 1434 | "log", 1435 | "once_cell", 1436 | "tracing-core", 1437 | ] 1438 | 1439 | [[package]] 1440 | name = "tracing-subscriber" 1441 | version = "0.3.19" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1444 | dependencies = [ 1445 | "matchers", 1446 | "nu-ansi-term", 1447 | "once_cell", 1448 | "regex", 1449 | "sharded-slab", 1450 | "smallvec", 1451 | "thread_local", 1452 | "tracing", 1453 | "tracing-core", 1454 | "tracing-log", 1455 | ] 1456 | 1457 | [[package]] 1458 | name = "tuikit" 1459 | version = "0.5.0" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "5e19c6ab038babee3d50c8c12ff8b910bdb2196f62278776422f50390d8e53d8" 1462 | dependencies = [ 1463 | "bitflags 1.3.2", 1464 | "lazy_static", 1465 | "log", 1466 | "nix 0.24.3", 1467 | "term", 1468 | "unicode-width", 1469 | ] 1470 | 1471 | [[package]] 1472 | name = "unicode-ident" 1473 | version = "1.0.14" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1476 | 1477 | [[package]] 1478 | name = "unicode-width" 1479 | version = "0.1.11" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 1482 | 1483 | [[package]] 1484 | name = "utf8parse" 1485 | version = "0.2.2" 1486 | source = "registry+https://github.com/rust-lang/crates.io-index" 1487 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1488 | 1489 | [[package]] 1490 | name = "valuable" 1491 | version = "0.1.0" 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" 1493 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1494 | 1495 | [[package]] 1496 | name = "vte" 1497 | version = "0.11.1" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" 1500 | dependencies = [ 1501 | "arrayvec", 1502 | "utf8parse", 1503 | "vte_generate_state_changes", 1504 | ] 1505 | 1506 | [[package]] 1507 | name = "vte_generate_state_changes" 1508 | version = "0.1.1" 1509 | source = "registry+https://github.com/rust-lang/crates.io-index" 1510 | checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" 1511 | dependencies = [ 1512 | "proc-macro2", 1513 | "quote", 1514 | ] 1515 | 1516 | [[package]] 1517 | name = "walkdir" 1518 | version = "2.5.0" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1521 | dependencies = [ 1522 | "same-file", 1523 | "winapi-util", 1524 | ] 1525 | 1526 | [[package]] 1527 | name = "wasi" 1528 | version = "0.11.0+wasi-snapshot-preview1" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1531 | 1532 | [[package]] 1533 | name = "wasm-bindgen" 1534 | version = "0.2.99" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" 1537 | dependencies = [ 1538 | "cfg-if", 1539 | "once_cell", 1540 | "wasm-bindgen-macro", 1541 | ] 1542 | 1543 | [[package]] 1544 | name = "wasm-bindgen-backend" 1545 | version = "0.2.99" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" 1548 | dependencies = [ 1549 | "bumpalo", 1550 | "log", 1551 | "proc-macro2", 1552 | "quote", 1553 | "syn 2.0.91", 1554 | "wasm-bindgen-shared", 1555 | ] 1556 | 1557 | [[package]] 1558 | name = "wasm-bindgen-macro" 1559 | version = "0.2.99" 1560 | source = "registry+https://github.com/rust-lang/crates.io-index" 1561 | checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" 1562 | dependencies = [ 1563 | "quote", 1564 | "wasm-bindgen-macro-support", 1565 | ] 1566 | 1567 | [[package]] 1568 | name = "wasm-bindgen-macro-support" 1569 | version = "0.2.99" 1570 | source = "registry+https://github.com/rust-lang/crates.io-index" 1571 | checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" 1572 | dependencies = [ 1573 | "proc-macro2", 1574 | "quote", 1575 | "syn 2.0.91", 1576 | "wasm-bindgen-backend", 1577 | "wasm-bindgen-shared", 1578 | ] 1579 | 1580 | [[package]] 1581 | name = "wasm-bindgen-shared" 1582 | version = "0.2.99" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" 1585 | 1586 | [[package]] 1587 | name = "winapi" 1588 | version = "0.3.9" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1591 | dependencies = [ 1592 | "winapi-i686-pc-windows-gnu", 1593 | "winapi-x86_64-pc-windows-gnu", 1594 | ] 1595 | 1596 | [[package]] 1597 | name = "winapi-i686-pc-windows-gnu" 1598 | version = "0.4.0" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1601 | 1602 | [[package]] 1603 | name = "winapi-util" 1604 | version = "0.1.9" 1605 | source = "registry+https://github.com/rust-lang/crates.io-index" 1606 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1607 | dependencies = [ 1608 | "windows-sys 0.59.0", 1609 | ] 1610 | 1611 | [[package]] 1612 | name = "winapi-x86_64-pc-windows-gnu" 1613 | version = "0.4.0" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1616 | 1617 | [[package]] 1618 | name = "windows-core" 1619 | version = "0.52.0" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1622 | dependencies = [ 1623 | "windows-targets 0.52.6", 1624 | ] 1625 | 1626 | [[package]] 1627 | name = "windows-sys" 1628 | version = "0.48.0" 1629 | source = "registry+https://github.com/rust-lang/crates.io-index" 1630 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1631 | dependencies = [ 1632 | "windows-targets 0.48.5", 1633 | ] 1634 | 1635 | [[package]] 1636 | name = "windows-sys" 1637 | version = "0.59.0" 1638 | source = "registry+https://github.com/rust-lang/crates.io-index" 1639 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1640 | dependencies = [ 1641 | "windows-targets 0.52.6", 1642 | ] 1643 | 1644 | [[package]] 1645 | name = "windows-targets" 1646 | version = "0.48.5" 1647 | source = "registry+https://github.com/rust-lang/crates.io-index" 1648 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1649 | dependencies = [ 1650 | "windows_aarch64_gnullvm 0.48.5", 1651 | "windows_aarch64_msvc 0.48.5", 1652 | "windows_i686_gnu 0.48.5", 1653 | "windows_i686_msvc 0.48.5", 1654 | "windows_x86_64_gnu 0.48.5", 1655 | "windows_x86_64_gnullvm 0.48.5", 1656 | "windows_x86_64_msvc 0.48.5", 1657 | ] 1658 | 1659 | [[package]] 1660 | name = "windows-targets" 1661 | version = "0.52.6" 1662 | source = "registry+https://github.com/rust-lang/crates.io-index" 1663 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1664 | dependencies = [ 1665 | "windows_aarch64_gnullvm 0.52.6", 1666 | "windows_aarch64_msvc 0.52.6", 1667 | "windows_i686_gnu 0.52.6", 1668 | "windows_i686_gnullvm", 1669 | "windows_i686_msvc 0.52.6", 1670 | "windows_x86_64_gnu 0.52.6", 1671 | "windows_x86_64_gnullvm 0.52.6", 1672 | "windows_x86_64_msvc 0.52.6", 1673 | ] 1674 | 1675 | [[package]] 1676 | name = "windows_aarch64_gnullvm" 1677 | version = "0.48.5" 1678 | source = "registry+https://github.com/rust-lang/crates.io-index" 1679 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1680 | 1681 | [[package]] 1682 | name = "windows_aarch64_gnullvm" 1683 | version = "0.52.6" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1686 | 1687 | [[package]] 1688 | name = "windows_aarch64_msvc" 1689 | version = "0.48.5" 1690 | source = "registry+https://github.com/rust-lang/crates.io-index" 1691 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1692 | 1693 | [[package]] 1694 | name = "windows_aarch64_msvc" 1695 | version = "0.52.6" 1696 | source = "registry+https://github.com/rust-lang/crates.io-index" 1697 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1698 | 1699 | [[package]] 1700 | name = "windows_i686_gnu" 1701 | version = "0.48.5" 1702 | source = "registry+https://github.com/rust-lang/crates.io-index" 1703 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1704 | 1705 | [[package]] 1706 | name = "windows_i686_gnu" 1707 | version = "0.52.6" 1708 | source = "registry+https://github.com/rust-lang/crates.io-index" 1709 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1710 | 1711 | [[package]] 1712 | name = "windows_i686_gnullvm" 1713 | version = "0.52.6" 1714 | source = "registry+https://github.com/rust-lang/crates.io-index" 1715 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1716 | 1717 | [[package]] 1718 | name = "windows_i686_msvc" 1719 | version = "0.48.5" 1720 | source = "registry+https://github.com/rust-lang/crates.io-index" 1721 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1722 | 1723 | [[package]] 1724 | name = "windows_i686_msvc" 1725 | version = "0.52.6" 1726 | source = "registry+https://github.com/rust-lang/crates.io-index" 1727 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1728 | 1729 | [[package]] 1730 | name = "windows_x86_64_gnu" 1731 | version = "0.48.5" 1732 | source = "registry+https://github.com/rust-lang/crates.io-index" 1733 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1734 | 1735 | [[package]] 1736 | name = "windows_x86_64_gnu" 1737 | version = "0.52.6" 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" 1739 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1740 | 1741 | [[package]] 1742 | name = "windows_x86_64_gnullvm" 1743 | version = "0.48.5" 1744 | source = "registry+https://github.com/rust-lang/crates.io-index" 1745 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1746 | 1747 | [[package]] 1748 | name = "windows_x86_64_gnullvm" 1749 | version = "0.52.6" 1750 | source = "registry+https://github.com/rust-lang/crates.io-index" 1751 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1752 | 1753 | [[package]] 1754 | name = "windows_x86_64_msvc" 1755 | version = "0.48.5" 1756 | source = "registry+https://github.com/rust-lang/crates.io-index" 1757 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1758 | 1759 | [[package]] 1760 | name = "windows_x86_64_msvc" 1761 | version = "0.52.6" 1762 | source = "registry+https://github.com/rust-lang/crates.io-index" 1763 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1764 | -------------------------------------------------------------------------------- /rsenv/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsenv" 3 | version = "1.3.0" 4 | edition = "2021" 5 | authors = ["sysid "] 6 | description = "Hierarchical environment variable management" 7 | keywords = ["development", "environment"] 8 | repository = "https://github.com/sysid/rs-env" 9 | readme = "../README.md" 10 | license = "BSD-3-Clause" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | anyhow = "1.0.95" 16 | clap = { version = "4.5.23", features = ["derive"] } 17 | clap_complete = "4.5.40" 18 | colored = "2.2.0" 19 | crossbeam = "0.8.4" 20 | crossterm = "0.27.0" 21 | ctor = "0.2.9" 22 | fs_extra = "1.3.0" 23 | generational-arena = "0.2.9" 24 | itertools = "0.12.1" 25 | lazy_static = "1.5.0" 26 | pathdiff = { version = "0.2.3" } 27 | regex = "1.11.1" 28 | rstest = "0.19.0" 29 | skim = "0.10.4" 30 | tempfile = "3.15.0" 31 | termtree = "0.4.1" 32 | thiserror = "2.0.9" 33 | tracing = "0.1.41" 34 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 35 | walkdir = "2.5.0" 36 | 37 | [dev-dependencies] 38 | 39 | [package.metadata.test] 40 | parallel = false 41 | -------------------------------------------------------------------------------- /rsenv/src/arena.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::fmt; 3 | use generational_arena::{Arena, Index}; 4 | use std::path::PathBuf; 5 | use tracing::instrument; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct NodeData { 9 | pub base_path: PathBuf, 10 | pub file_path: PathBuf, 11 | } 12 | 13 | impl fmt::Display for NodeData { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | write!(f, "{}", self.file_path.display()) 16 | } 17 | } 18 | 19 | #[derive(Debug)] 20 | pub struct TreeNode { 21 | pub data: NodeData, 22 | pub parent: Option, 23 | pub children: Vec, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct TreeArena { 28 | arena: Arena, 29 | root: Option, 30 | } 31 | 32 | impl Default for TreeArena { 33 | fn default() -> Self { 34 | Self::new() 35 | } 36 | } 37 | 38 | impl TreeArena { 39 | pub fn new() -> Self { 40 | Self { 41 | arena: Arena::new(), 42 | root: None, 43 | } 44 | } 45 | 46 | #[instrument(level = "trace", skip(self))] 47 | pub fn insert_node(&mut self, data: NodeData, parent: Option) -> Index { 48 | let node = TreeNode { 49 | data, 50 | parent, 51 | children: Vec::new(), 52 | }; 53 | let node_idx = self.arena.insert(node); 54 | 55 | if let Some(parent_idx) = parent { 56 | if let Some(parent) = self.arena.get_mut(parent_idx) { 57 | parent.children.push(node_idx); 58 | } 59 | } else { 60 | self.root = Some(node_idx); 61 | } 62 | 63 | node_idx 64 | } 65 | 66 | #[instrument(level = "trace", skip(self))] 67 | pub fn get_node(&self, idx: Index) -> Option<&TreeNode> { 68 | self.arena.get(idx) 69 | } 70 | 71 | #[instrument(level = "trace", skip(self))] 72 | pub fn get_node_mut(&mut self, idx: Index) -> Option<&mut TreeNode> { 73 | self.arena.get_mut(idx) 74 | } 75 | 76 | #[instrument(level = "trace", skip(self))] 77 | pub fn root(&self) -> Option { 78 | self.root 79 | } 80 | 81 | #[instrument(level = "trace", skip(self))] 82 | pub fn iter(&self) -> TreeIterator { 83 | TreeIterator::new(self) 84 | } 85 | 86 | #[instrument(level = "trace", skip(self))] 87 | pub fn iter_postorder(&self) -> PostOrderIterator { 88 | PostOrderIterator::new(self) 89 | } 90 | 91 | #[instrument(level = "debug", skip(self))] 92 | pub fn depth(&self) -> usize { 93 | if let Some(root) = self.root { 94 | self.calculate_depth(root) 95 | } else { 96 | 0 97 | } 98 | } 99 | 100 | #[instrument(level = "trace", skip(self))] 101 | fn calculate_depth(&self, node_idx: Index) -> usize { 102 | if let Some(node) = self.get_node(node_idx) { 103 | 1 + node.children 104 | .iter() 105 | .map(|&child| self.calculate_depth(child)) 106 | .max() 107 | .unwrap_or(0) 108 | } else { 109 | 0 110 | } 111 | } 112 | 113 | #[instrument(level = "debug", skip(self))] 114 | pub fn leaf_nodes(&self) -> Vec { 115 | let mut leaves = Vec::new(); 116 | if let Some(root) = self.root { 117 | self.collect_leaves(root, &mut leaves); 118 | } 119 | leaves 120 | } 121 | 122 | #[instrument(level = "trace", skip(self))] 123 | fn collect_leaves(&self, node_idx: Index, leaves: &mut Vec) { 124 | if let Some(node) = self.get_node(node_idx) { 125 | if node.children.is_empty() { 126 | leaves.push(node.data.file_path.clone().to_string_lossy().to_string()); 127 | } else { 128 | for &child in &node.children { 129 | self.collect_leaves(child, leaves); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | pub struct TreeIterator<'a> { 137 | arena: &'a TreeArena, 138 | stack: Vec, 139 | } 140 | 141 | impl<'a> TreeIterator<'a> { 142 | #[instrument(level = "trace")] 143 | fn new(arena: &'a TreeArena) -> Self { 144 | let mut stack = Vec::new(); 145 | if let Some(root) = arena.root() { 146 | stack.push(root); 147 | } 148 | Self { arena, stack } 149 | } 150 | } 151 | 152 | impl<'a> Iterator for TreeIterator<'a> { 153 | type Item = (Index, &'a TreeNode); 154 | 155 | #[instrument(level = "trace", skip(self))] 156 | fn next(&mut self) -> Option { 157 | if let Some(current_idx) = self.stack.pop() { 158 | if let Some(node) = self.arena.get_node(current_idx) { 159 | // Push children in reverse order for left-to-right traversal 160 | for &child in node.children.iter().rev() { 161 | self.stack.push(child); 162 | } 163 | return Some((current_idx, node)); 164 | } 165 | } 166 | None 167 | } 168 | } 169 | 170 | pub struct PostOrderIterator<'a> { 171 | arena: &'a TreeArena, 172 | stack: Vec<(Index, bool)>, 173 | } 174 | 175 | impl<'a> PostOrderIterator<'a> { 176 | #[instrument(level = "trace")] 177 | fn new(arena: &'a TreeArena) -> Self { 178 | let mut stack = Vec::new(); 179 | if let Some(root) = arena.root() { 180 | stack.push((root, false)); 181 | } 182 | Self { arena, stack } 183 | } 184 | } 185 | 186 | impl<'a> Iterator for PostOrderIterator<'a> { 187 | type Item = (Index, &'a TreeNode); 188 | 189 | #[instrument(level = "trace", skip(self))] 190 | fn next(&mut self) -> Option { 191 | while let Some((current_idx, visited)) = self.stack.pop() { 192 | if let Some(node) = self.arena.get_node(current_idx) { 193 | if !visited { 194 | self.stack.push((current_idx, true)); 195 | for &child in node.children.iter().rev() { 196 | self.stack.push((child, false)); 197 | } 198 | } else { 199 | return Some((current_idx, node)); 200 | } 201 | } 202 | } 203 | None 204 | } 205 | } -------------------------------------------------------------------------------- /rsenv/src/builder.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::fs::File; 3 | use std::io::BufReader; 4 | use std::io::BufRead; 5 | use std::path::{Path, PathBuf}; 6 | use regex::Regex; 7 | use tracing::instrument; 8 | use walkdir::WalkDir; 9 | 10 | use crate::errors::{TreeError, TreeResult}; 11 | use crate::arena::{TreeArena, NodeData}; 12 | use crate::util::path::PathExt; 13 | 14 | pub struct TreeBuilder { 15 | relationship_cache: HashMap>, 16 | visited_paths: HashSet, 17 | parent_regex: Regex, 18 | } 19 | 20 | impl Default for TreeBuilder { 21 | fn default() -> Self { 22 | Self::new() 23 | } 24 | } 25 | 26 | impl TreeBuilder { 27 | pub fn new() -> Self { 28 | Self { 29 | relationship_cache: HashMap::new(), 30 | visited_paths: HashSet::new(), 31 | parent_regex: Regex::new(r"# rsenv: (.+)").unwrap(), 32 | } 33 | } 34 | 35 | #[instrument(level = "debug", skip(self))] 36 | pub fn build_from_directory(&mut self, directory_path: &Path) -> TreeResult> { 37 | if !directory_path.exists() { 38 | return Err(TreeError::FileNotFound(directory_path.to_path_buf())); 39 | } 40 | if !directory_path.is_dir() { 41 | return Err(TreeError::InvalidFormat { 42 | path: directory_path.to_path_buf(), 43 | reason: "Not a directory".to_string() 44 | }); 45 | } 46 | 47 | // Scan directory and build relationship cache 48 | self.scan_directory(directory_path)?; 49 | 50 | // Find root nodes 51 | let root_files = self.find_root_nodes(); 52 | 53 | // Build trees 54 | let mut trees = Vec::new(); 55 | for root in root_files { 56 | let tree = self.build_tree(&root)?; 57 | trees.push(tree); 58 | } 59 | 60 | Ok(trees) 61 | } 62 | 63 | #[instrument(level = "debug", skip(self))] 64 | fn scan_directory(&mut self, directory_path: &Path) -> TreeResult<()> { 65 | for entry in WalkDir::new(directory_path) { 66 | let entry = entry.map_err(|e| TreeError::PathResolution { 67 | path: directory_path.to_path_buf(), 68 | reason: e.to_string(), 69 | })?; 70 | 71 | if entry.file_type().is_file() { 72 | self.process_file(entry.path())?; 73 | } 74 | } 75 | Ok(()) 76 | } 77 | 78 | #[instrument(level = "debug", skip(self))] 79 | fn process_file(&mut self, path: &Path) -> TreeResult<()> { 80 | let file = File::open(path).map_err(TreeError::FileReadError)?; 81 | let reader = BufReader::new(file); 82 | let abs_path = path.to_canonical()?; 83 | let current_dir = abs_path.parent() 84 | .ok_or_else(|| TreeError::InvalidParent(path.to_path_buf()))?; 85 | 86 | for line in reader.lines() { 87 | let line = line.map_err(TreeError::FileReadError)?; 88 | if let Some(caps) = self.parent_regex.captures(&line) { 89 | let parent_relative = caps.get(1).unwrap().as_str(); 90 | let parent_path = current_dir.join(parent_relative); 91 | let parent_canonical = parent_path.to_canonical()?; 92 | 93 | self.relationship_cache 94 | .entry(parent_canonical) 95 | .or_default() 96 | .push(abs_path.clone()); 97 | } 98 | } 99 | Ok(()) 100 | } 101 | 102 | #[instrument(level = "debug", skip(self))] 103 | fn find_root_nodes(&self) -> Vec { 104 | self.relationship_cache 105 | .keys() 106 | .filter(|path| !self.relationship_cache.values().any(|v| v.contains(path))) 107 | .cloned() 108 | .collect() 109 | } 110 | 111 | #[instrument(level = "debug", skip(self))] 112 | fn build_tree(&mut self, root_path: &Path) -> TreeResult { 113 | let mut tree = TreeArena::new(); 114 | let mut stack = vec![(root_path.to_path_buf(), None)]; 115 | self.visited_paths.clear(); 116 | 117 | while let Some((current_path, parent_idx)) = stack.pop() { 118 | // Check for cycles 119 | if !self.visited_paths.insert(current_path.clone()) { 120 | return Err(TreeError::CycleDetected(current_path)); 121 | } 122 | 123 | let node_data = NodeData { 124 | base_path: current_path.parent() 125 | .ok_or_else(|| TreeError::InvalidParent(current_path.clone()))? 126 | .to_path_buf(), 127 | file_path: current_path.clone(), 128 | }; 129 | 130 | let current_idx = tree.insert_node(node_data, parent_idx); 131 | 132 | // Add children to stack 133 | if let Some(children) = self.relationship_cache.get(¤t_path) { 134 | for child in children { 135 | stack.push((child.clone(), Some(current_idx))); 136 | } 137 | } 138 | } 139 | 140 | Ok(tree) 141 | } 142 | } -------------------------------------------------------------------------------- /rsenv/src/cli/args.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand, ValueHint}; 2 | use clap_complete::Shell; 3 | 4 | #[derive(Parser, Debug, PartialEq)] 5 | #[command(author, version, about, long_about = None)] // Read from `Cargo.toml` 6 | #[command(arg_required_else_help = true)] 7 | /// A hierarchical environment variable manager for configuration files 8 | pub struct Cli { 9 | /// Name of the configuration to operate on (optional) 10 | name: Option, 11 | 12 | /// Enable debug logging. Multiple flags (-d, -dd, -ddd) increase verbosity 13 | #[arg(short, long, action = clap::ArgAction::Count)] 14 | pub debug: u8, 15 | 16 | /// Generate shell completion scripts 17 | #[arg(long = "generate", value_enum)] 18 | pub generator: Option, 19 | 20 | /// Display version and configuration information 21 | #[arg(long = "info")] 22 | pub info: bool, 23 | 24 | #[command(subcommand)] 25 | pub command: Option, 26 | } 27 | 28 | #[derive(Subcommand, Debug, PartialEq)] 29 | pub enum Commands { 30 | /// Build and display the complete set of environment variables 31 | Build { 32 | /// Path to the last linked environment file (leaf node in hierarchy) 33 | #[arg(value_hint = ValueHint::FilePath)] 34 | source_path: String, 35 | }, 36 | /// Write environment variables to .envrc file (requires direnv) 37 | Envrc { 38 | /// Path to the last linked environment file (leaf node in hierarchy) 39 | #[arg(value_hint = ValueHint::FilePath)] 40 | source_path: String, 41 | /// path to .envrc file 42 | #[arg(value_hint = ValueHint::FilePath)] 43 | envrc_path: Option, 44 | }, 45 | /// List all files in the environment hierarchy 46 | Files { 47 | /// Path to the last linked environment file (leaf node in hierarchy) 48 | #[arg(value_hint = ValueHint::FilePath)] 49 | source_path: String, 50 | }, 51 | /// Edit an environment file and all its parent files 52 | EditLeaf { 53 | /// Path to the last linked environment file (leaf node in hierarchy) 54 | #[arg(value_hint = ValueHint::FilePath)] 55 | source_path: String, 56 | }, 57 | /// Interactively select and edit an environment hierarchy 58 | Edit { 59 | /// Directory containing environment files 60 | #[arg(value_hint = ValueHint::DirPath)] 61 | source_dir: String, 62 | }, 63 | /// Update .envrc with selected environment (requires direnv) 64 | SelectLeaf { 65 | /// Path to the leaf environment file 66 | #[arg(value_hint = ValueHint::DirPath)] 67 | source_path: String, 68 | }, 69 | /// Interactively select environment and update .envrc (requires direnv) 70 | Select { 71 | /// Directory containing environment files 72 | #[arg(value_hint = ValueHint::DirPath)] 73 | source_dir: String, 74 | }, 75 | /// Create parent-child relationships between environment files 76 | Link { 77 | /// Environment files to link (root -> parent -> child) 78 | #[arg(value_hint = ValueHint::FilePath, num_args = 1..)] 79 | nodes: Vec, 80 | }, 81 | /// Show all branches (linear representation) 82 | Branches { 83 | /// Root directory containing environment files 84 | #[arg(value_hint = ValueHint::DirPath)] 85 | source_dir: String, 86 | }, 87 | /// Show all trees (hierarchical representation) 88 | Tree { 89 | /// Root directory containing environment files 90 | #[arg(value_hint = ValueHint::DirPath)] 91 | source_dir: String, 92 | }, 93 | /// Edit all environment hierarchies side-by-side (requires vim) 94 | TreeEdit { 95 | /// Root directory containing environment files 96 | #[arg(value_hint = ValueHint::DirPath)] 97 | source_dir: String, 98 | }, 99 | /// List all leaf environment files 100 | Leaves { 101 | /// Root directory containing environment files 102 | #[arg(value_hint = ValueHint::DirPath)] 103 | source_dir: String, 104 | }, 105 | } -------------------------------------------------------------------------------- /rsenv/src/cli/commands.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::args::{Cli, Commands}; 2 | use crate::edit::{ 3 | create_branches, create_vimscript, open_files_in_editor, select_file_with_suffix, 4 | }; 5 | use crate::envrc::update_dot_envrc; 6 | use crate::builder::TreeBuilder; 7 | use crate::{build_env_vars, get_files, is_dag, link_all, print_files}; 8 | use anyhow::{anyhow, Result}; 9 | use std::path::Path; 10 | use std::process; 11 | use std::io::Write; 12 | use crossterm::style::Stylize; 13 | use tracing::{debug, instrument}; 14 | use tempfile::NamedTempFile; 15 | 16 | pub fn execute_command(cli: &Cli) -> Result<()> { 17 | match &cli.command { 18 | Some(Commands::Build { source_path }) => _build(source_path), 19 | Some(Commands::Envrc { 20 | source_path, 21 | envrc_path, 22 | }) => _envrc(source_path, envrc_path.as_deref()), 23 | Some(Commands::Files { source_path }) => _files(source_path), 24 | Some(Commands::EditLeaf { source_path }) => _edit_leaf(source_path), 25 | Some(Commands::Edit { source_dir }) => _edit(source_dir), 26 | Some(Commands::SelectLeaf { source_path }) => _select_leaf(source_path), 27 | Some(Commands::Select { source_dir }) => _select(source_dir), 28 | Some(Commands::Link { nodes }) => _link(nodes), 29 | Some(Commands::Branches { source_dir }) => _branches(source_dir), 30 | Some(Commands::Tree { source_dir }) => _tree(source_dir), 31 | Some(Commands::TreeEdit { source_dir }) => _tree_edit(source_dir), 32 | Some(Commands::Leaves { source_dir }) => _leaves(source_dir), 33 | None => Ok(()) 34 | } 35 | } 36 | 37 | #[instrument] 38 | fn _build(source_path: &str) -> Result<()> { 39 | debug!("source_path: {:?}", source_path); 40 | let vars = build_env_vars(Path::new(source_path)).unwrap_or_else(|e| { 41 | eprintln!("{}", format!("Cannot build environment: {}", e).red()); 42 | process::exit(1); 43 | }); 44 | println!("{}", vars); 45 | Ok(()) 46 | } 47 | 48 | #[instrument] 49 | fn _envrc(source_path: &str, envrc_path: Option<&str>) -> Result<()> { 50 | let envrc_path = envrc_path.unwrap_or(".envrc"); 51 | debug!( 52 | "source_path: {:?}, envrc_path: {:?}", 53 | source_path, 54 | envrc_path 55 | ); 56 | let vars = build_env_vars(Path::new(source_path)).unwrap_or_else(|e| { 57 | eprintln!("{}", format!("Cannot build environment: {}", e).red()); 58 | process::exit(1); 59 | }); 60 | update_dot_envrc(Path::new(envrc_path), vars.as_str())?; 61 | Ok(()) 62 | } 63 | 64 | #[instrument] 65 | fn _files(source_path: &str) -> Result<()> { 66 | debug!("source_path: {:?}", source_path); 67 | print_files(Path::new(source_path)).unwrap_or_else(|e| { 68 | eprintln!("{}", format!("Cannot print environment: {}", e).red()); 69 | process::exit(1); 70 | }); 71 | Ok(()) 72 | } 73 | 74 | #[instrument] 75 | fn _edit_leaf(source_path: &str) -> Result<()> { 76 | let path = Path::new(source_path); 77 | if !path.exists() { 78 | return Err(anyhow!("File does not exist: {:?}", source_path)); 79 | } 80 | let files = get_files(path).unwrap_or_else(|e| { 81 | eprintln!("{}", format!("Cannot get files: {}", e).red()); 82 | process::exit(1); 83 | }); 84 | open_files_in_editor(files).unwrap_or_else(|e| { 85 | eprintln!("{}", format!("Cannot open files in editor: {}", e).red()); 86 | process::exit(1); 87 | }); 88 | Ok(()) 89 | } 90 | 91 | #[instrument] 92 | fn _edit(source_dir: &str) -> Result<()> { 93 | let path = Path::new(source_dir); 94 | if !path.exists() { 95 | eprintln!("Error: Directory does not exist: {:?}", source_dir); 96 | process::exit(1); 97 | } 98 | let selected_file = select_file_with_suffix(path, ".env").unwrap_or_else(|_| { 99 | eprintln!("{}", "No .env files found".to_string().red()); 100 | process::exit(1); 101 | }); 102 | println!("Selected: {}", selected_file.display()); 103 | let files = get_files(&selected_file).unwrap_or_else(|e| { 104 | eprintln!("{}", format!("Cannot get files: {}", e).red()); 105 | process::exit(1); 106 | }); 107 | open_files_in_editor(files).unwrap_or_else(|e| { 108 | eprintln!("{}", format!("Cannot open files in editor: {}", e).red()); 109 | process::exit(1); 110 | }); 111 | Ok(()) 112 | } 113 | 114 | #[instrument] 115 | fn _select_leaf(source_path: &str) -> Result<()> { 116 | let path = Path::new(source_path); 117 | if !path.exists() { 118 | eprintln!("Error: File does not exist: {:?}", source_path); 119 | process::exit(1); 120 | } 121 | _envrc(source_path, None) 122 | } 123 | 124 | #[instrument] 125 | fn _select(source_dir: &str) -> Result<()> { 126 | let path = Path::new(source_dir); 127 | if !path.exists() { 128 | eprintln!("Error: Directory does not exist: {:?}", source_dir); 129 | process::exit(1); 130 | } 131 | let selected_file = select_file_with_suffix(path, ".env").unwrap_or_else(|_| { 132 | eprintln!("{}", "No .env files found.".to_string().red()); 133 | process::exit(1); 134 | }); 135 | println!("Selected: {}", selected_file.display()); 136 | _envrc(selected_file.to_str().unwrap(), None) 137 | } 138 | 139 | #[instrument] 140 | fn _link(nodes: &[String]) -> Result<()> { 141 | let paths = nodes.iter() 142 | .map(|s| Path::new(s).to_path_buf()) 143 | .collect::>(); 144 | link_all(&paths); 145 | println!("Linked: {}", nodes.join(" <- ")); 146 | Ok(()) 147 | } 148 | 149 | #[instrument] 150 | fn _branches(source_path: &str) -> Result<()> { 151 | debug!("source_path: {:?}", source_path); 152 | let path = Path::new(source_path); 153 | if is_dag(path).expect("Failed to determine if DAG") { 154 | eprintln!( 155 | "{}", 156 | "Dependencies form a DAG, you cannot use tree based commands.".to_string().red() 157 | ); 158 | process::exit(1); 159 | } 160 | let mut builder = TreeBuilder::new(); 161 | let trees = builder.build_from_directory(path).unwrap_or_else(|e| { 162 | eprintln!("{}", format!("Cannot build trees: {}", e).red()); 163 | process::exit(1); 164 | }); 165 | println!("Found {} trees:\n", trees.len()); 166 | for tree in &trees { 167 | if let Some(root_idx) = tree.root() { 168 | if let Some(root_node) = tree.get_node(root_idx) { 169 | println!("Tree Root: {}", root_node.data.file_path.display()); 170 | let _path = [root_node.data.file_path.to_str().unwrap().to_string()]; 171 | // TODO: Implement print_leaf_paths for arena-based tree 172 | println!(); 173 | } 174 | } 175 | } 176 | Ok(()) 177 | } 178 | 179 | #[instrument] 180 | fn _tree(source_path: &str) -> Result<()> { 181 | debug!("source_path: {:?}", source_path); 182 | let path = Path::new(source_path); 183 | if is_dag(path).expect("Failed to determine if DAG") { 184 | eprintln!( 185 | "{}", 186 | "Dependencies form a DAG, you cannot use tree based commands.".to_string().red() 187 | ); 188 | process::exit(1); 189 | } 190 | let mut builder = TreeBuilder::new(); 191 | let trees = builder.build_from_directory(path).unwrap_or_else(|e| { 192 | eprintln!("{}", format!("Cannot build trees: {}", e).red()); 193 | process::exit(1); 194 | }); 195 | println!("Found {} trees:\n", trees.len()); 196 | for tree in &trees { 197 | if let Some(root_idx) = tree.root() { 198 | if let Some(root_node) = tree.get_node(root_idx) { 199 | println!("{}", root_node.data.file_path.display()); 200 | } 201 | } 202 | } 203 | Ok(()) 204 | } 205 | 206 | #[instrument] 207 | fn _tree_edit(source_path: &str) -> Result<()> { 208 | // vim -O3 test.env int.env prod.env -c "wincmd h" -c "sp test.env" -c "wincmd l" -c "sp int.env" -c "wincmd l" -c "sp prod.env" 209 | debug!("source_path: {:?}", source_path); 210 | let path = Path::new(source_path); 211 | if is_dag(path).expect("Failed to determine if DAG") { 212 | eprintln!( 213 | "{}", 214 | "Dependencies form a DAG, you cannot use tree based commands.".to_string().red() 215 | ); 216 | process::exit(1); 217 | } 218 | let mut builder = TreeBuilder::new(); 219 | let trees = builder.build_from_directory(path).unwrap_or_else(|e| { 220 | eprintln!("{}", format!("Cannot build trees: {}", e).red()); 221 | process::exit(1); 222 | }); 223 | println!("Editing {} trees...", trees.len()); 224 | 225 | let vimscript_files: Vec> = create_branches(&trees); 226 | 227 | let vimscript = create_vimscript( 228 | vimscript_files 229 | .iter() 230 | .map(|v| v.iter().map(|s| s.as_path()).collect()) 231 | .collect(), 232 | ); 233 | 234 | let mut tmpfile = NamedTempFile::new()?; 235 | tmpfile.write_all(vimscript.as_bytes())?; 236 | 237 | let status = process::Command::new("vim") 238 | .arg("-S") 239 | .arg(tmpfile.path()) 240 | .status() 241 | .expect("failed to run vim"); 242 | 243 | println!("Vim: {}", status); 244 | Ok(()) 245 | } 246 | 247 | #[instrument] 248 | fn _leaves(source_path: &str) -> Result<()> { 249 | debug!("source_path: {:?}", source_path); 250 | let path = Path::new(source_path); 251 | if is_dag(path).expect("Failed to determine if DAG") { 252 | eprintln!( 253 | "{}", 254 | "Dependencies form a DAG, you cannot use tree based commands.".to_string().red() 255 | ); 256 | process::exit(1); 257 | } 258 | let mut builder = TreeBuilder::new(); 259 | let trees = builder.build_from_directory(path).unwrap_or_else(|e| { 260 | eprintln!("{}", format!("Cannot build trees: {}", e).red()); 261 | process::exit(1); 262 | }); 263 | debug!("Found {} trees:\n", trees.len()); 264 | for tree in &trees { 265 | let leaf_nodes = tree.leaf_nodes(); 266 | for leaf in &leaf_nodes { 267 | println!("{}", leaf); 268 | } 269 | } 270 | Ok(()) 271 | } 272 | -------------------------------------------------------------------------------- /rsenv/src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod commands; 2 | pub mod args; -------------------------------------------------------------------------------- /rsenv/src/edit.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::process::Command; 3 | use std::env; 4 | use std::sync::Arc; 5 | 6 | use walkdir::WalkDir; 7 | use skim::prelude::*; 8 | use crossbeam::channel::bounded; 9 | use crossterm::{execute, terminal::{Clear, ClearType}}; 10 | use tracing::{debug, instrument}; 11 | 12 | use crate::errors::{TreeError, TreeResult}; 13 | use crate::arena::TreeArena; 14 | 15 | #[instrument(level = "debug")] 16 | pub fn select_file_with_suffix(dir: &Path, suffix: &str) -> TreeResult { 17 | debug!("Searching for files with suffix {} in {:?}", suffix, dir); 18 | 19 | // List all files with the given suffix 20 | let files: Vec = WalkDir::new(dir) 21 | .into_iter() 22 | .filter_map(|e| e.ok()) 23 | .filter(|e| !e.path().is_dir()) 24 | .filter(|e| e.path().to_string_lossy().ends_with(suffix)) 25 | .map(|e| e.path().to_path_buf()) 26 | .collect(); 27 | 28 | if files.is_empty() { 29 | return Err(TreeError::InternalError(format!( 30 | "No files found with suffix {} in {:?}", 31 | suffix, dir 32 | ))); 33 | } 34 | 35 | // Step 2: Create a channel and send items to the skim interface 36 | // create a channel with a bounded capacity (in this case, 100). The tx (sender) part of the 37 | // channel is used to send items, and the rx (receiver) part is passed to Skim::run_with(). 38 | let (tx, rx) = bounded(100); 39 | 40 | // Skim::run_with() expects a stream of items that implement the SkimItem trait, 41 | // which we can achieve by transforming our Vec into a stream of Arc objects. 42 | // For each file path String, we convert it to an Arc and then to Arc, 43 | // just like before. We then send each of these items through the tx (sender) part of the channel. 44 | for file in files.iter() { 45 | let item = Arc::new(file.to_string_lossy().into_owned()) as Arc; 46 | tx.send(item).map_err(|e| TreeError::InternalError( 47 | format!("Failed to send item through channel: {}", e) 48 | ))?; 49 | } 50 | 51 | // This step is important because Skim::run_with() needs to know when there are no more items to expect. 52 | drop(tx); // Close the channel 53 | 54 | let options = SkimOptionsBuilder::default() 55 | .height(Some("50%")) 56 | .multi(false) 57 | .build() 58 | .map_err(|e| TreeError::InternalError( 59 | format!("Failed to build skim options: {}", e) 60 | ))?; 61 | 62 | // Running Skim with the Receiver: Instead of creating and passing a stream of items directly, 63 | // we just pass the rx (receiver) part of the channel to Skim::run_with(). 64 | let selected_items = Skim::run_with(&options, Some(rx)) 65 | .map(|out| out.selected_items) 66 | .unwrap_or_default(); 67 | 68 | // Clear screen 69 | let mut stdout = std::io::stdout(); 70 | execute!(stdout, Clear(ClearType::FromCursorDown)) 71 | .map_err(|e| TreeError::InternalError( 72 | format!("Failed to clear screen: {}", e) 73 | ))?; 74 | 75 | // Step 3: Save the selection into a variable for later use 76 | selected_items 77 | .first() 78 | .map(|item| PathBuf::from(item.output().to_string())) 79 | .ok_or_else(|| TreeError::InternalError("No file selected".to_string())) 80 | } 81 | 82 | #[instrument(level = "debug")] 83 | pub fn open_files_in_editor(files: Vec) -> TreeResult<()> { 84 | debug!("Opening files in editor: {:?}", files); 85 | 86 | let editor = env::var("EDITOR").unwrap_or_else(|_| "vim".to_string()); 87 | if !editor.contains("vim") { 88 | return Err(TreeError::InternalError("Only vim is supported for now".to_string())); 89 | } 90 | 91 | let file_paths: Vec = files.iter() 92 | .map(|path| path.to_string_lossy().into_owned()) 93 | .collect(); 94 | 95 | // Spawn a new process to run the editor. 96 | // For Vim and NeoVim, the `-p` option opens files in separate tabs. 97 | Command::new(&editor) 98 | .arg("-O") 99 | .args(&file_paths) 100 | .status() 101 | .map_err(|e| TreeError::InternalError(format!("Failed to run editor: {}", e)))?; 102 | 103 | Ok(()) 104 | } 105 | 106 | #[instrument(level = "debug")] 107 | pub fn create_vimscript(files: Vec>) -> String { 108 | debug!("Creating vimscript for files: {:?}", files); 109 | 110 | let mut script = String::new(); 111 | 112 | for (col_idx, col_files) in files.iter().enumerate() { 113 | if col_files.is_empty() { 114 | continue; 115 | } 116 | 117 | if col_idx == 0 { 118 | // For the first column, start with 'edit' for the first file 119 | script.push_str(&format!("\" Open the first set of files ('{}') in the first column\n", 120 | col_files[0].display())); 121 | script.push_str(&format!("edit {}\n", col_files[0].display())); 122 | } else { 123 | // For subsequent columns, start with a 'split' for the first file in the list 124 | // and move the cursor to the new (right) column 125 | script.push_str(&format!("split {}\n", col_files[0].display())); 126 | script.push_str("\" move to right column\nwincmd L\n"); 127 | } 128 | 129 | // For the rest of the files in the list, add a 'split' command for each 130 | for file in &col_files[1..] { 131 | script.push_str(&format!("split {}\n", file.display())); 132 | } 133 | } 134 | 135 | // Add the final commands to the script 136 | script.push_str("\n\" make distribution equal\nwincmd =\n"); 137 | script.push_str("\n\" jump to left top corner\n1wincmd w\n"); 138 | 139 | script 140 | } 141 | 142 | #[instrument(level = "debug")] 143 | pub fn create_branches(trees: &[TreeArena]) -> Vec> { 144 | debug!("Creating branches for {} trees", trees.len()); 145 | 146 | let mut vimscript_files = Vec::new(); 147 | 148 | for (tree_idx, tree) in trees.iter().enumerate() { 149 | debug!("Processing tree {}", tree_idx); 150 | 151 | let leaf_nodes = tree.leaf_nodes(); 152 | debug!("Found {} leaf nodes", leaf_nodes.len()); 153 | 154 | for leaf in &leaf_nodes { 155 | debug!("Processing leaf: {}", leaf.to_string()); 156 | 157 | let mut branch = Vec::new(); 158 | if let Ok(files) = crate::get_files(Path::new(leaf)) { 159 | debug!("Found {} files in branch", files.len()); 160 | branch.extend(files); 161 | vimscript_files.push(branch); 162 | } 163 | } 164 | } 165 | 166 | debug!("Created {} branches", vimscript_files.len()); 167 | vimscript_files 168 | } -------------------------------------------------------------------------------- /rsenv/src/envrc.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::fs::{File, OpenOptions}; 3 | use std::io::{BufRead, BufReader, Read, Write}; 4 | use regex::Regex; 5 | use tracing::{debug, instrument}; 6 | use crate::errors::{TreeError, TreeResult}; 7 | use crate::util::path::ensure_file_exists; 8 | 9 | pub const START_SECTION_DELIMITER: &str = "#------------------------------- rsenv start --------------------------------"; 10 | pub const END_SECTION_DELIMITER: &str = "#-------------------------------- rsenv end ---------------------------------"; 11 | 12 | #[instrument(level = "debug")] 13 | pub fn update_dot_envrc(target_file_path: &Path, data: &str) -> TreeResult<()> { 14 | ensure_file_exists(target_file_path)?; 15 | 16 | let section = format!( 17 | "\n{start_section_delimiter}\n\ 18 | {data}\ 19 | {end_section_delimiter}\n", 20 | start_section_delimiter = START_SECTION_DELIMITER, 21 | data = data, 22 | end_section_delimiter = END_SECTION_DELIMITER, 23 | ); 24 | 25 | let file = File::open(target_file_path) 26 | .map_err(TreeError::FileReadError)?; 27 | let reader = BufReader::new(file); 28 | let lines: Vec = reader.lines() 29 | .collect::>() 30 | .map_err(TreeError::FileReadError)?; 31 | 32 | let start_index = lines.iter().position(|l| { 33 | l.starts_with(START_SECTION_DELIMITER) 34 | }); 35 | let end_index = lines.iter().position(|l| { 36 | l.starts_with(END_SECTION_DELIMITER) 37 | }); 38 | 39 | let mut new_file_content = String::new(); 40 | 41 | match (start_index, end_index) { 42 | (Some(start), Some(end)) if start < end => { 43 | new_file_content.push_str(&lines[..start].join("\n")); 44 | new_file_content.push_str(§ion); 45 | new_file_content.push_str(&lines[end + 1..].join("\n")); 46 | } 47 | _ => { 48 | new_file_content.push_str(&lines.join("\n")); 49 | new_file_content.push_str(§ion); 50 | } 51 | } 52 | 53 | let mut file = OpenOptions::new() 54 | .write(true) 55 | .truncate(true) 56 | .open(target_file_path) 57 | .map_err(TreeError::FileReadError)?; 58 | 59 | file.write_all(new_file_content.as_bytes()) 60 | .map_err(TreeError::FileReadError) 61 | } 62 | 63 | #[instrument(level = "debug")] 64 | pub fn delete_section(file_path: &Path) -> TreeResult<()> { 65 | let mut file = File::open(file_path) 66 | .map_err(TreeError::FileReadError)?; 67 | let mut contents = String::new(); 68 | file.read_to_string(&mut contents) 69 | .map_err(TreeError::FileReadError)?; 70 | 71 | // Define the regex 72 | // (?s) enables "single-line mode" where . matches any character including newline (\n), allows to span lines It's often also called "dotall mode". 73 | // In this case, we want to match across multiple lines, hence the s modifier is used. 74 | // (?s)#--------------------- rsenv start ----------------------.*#---------------------- rsenv end -----------------------\n 75 | let pattern = format!( 76 | r"(?s){start_section_delimiter}.*{end_section_delimiter}\n", 77 | start_section_delimiter = START_SECTION_DELIMITER, 78 | end_section_delimiter = END_SECTION_DELIMITER, 79 | ); 80 | debug!("pattern: {}", pattern); 81 | let re = Regex::new(pattern.as_str()) 82 | .map_err(|e| TreeError::InternalError(e.to_string()))?; 83 | 84 | // Assert that only one section 85 | let result = re.find_iter(&contents).collect::>(); 86 | if result.len() > 1 { 87 | return Err(TreeError::MultipleParents(file_path.to_path_buf())); 88 | } 89 | 90 | // Replace the matched section with an empty string 91 | let result = re.replace(&contents, ""); 92 | 93 | // Write the result back to the file 94 | let mut file = File::create(file_path) 95 | .map_err(TreeError::FileReadError)?; 96 | file.write_all(result.as_bytes()) 97 | .map_err(TreeError::FileReadError) 98 | } -------------------------------------------------------------------------------- /rsenv/src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum TreeError { 6 | #[error("Invalid parent path: {0}")] 7 | InvalidParent(PathBuf), 8 | 9 | #[error("File not found: {0}")] 10 | FileNotFound(PathBuf), 11 | 12 | #[error("Failed to read file: {0}")] 13 | FileReadError(#[from] std::io::Error), 14 | 15 | #[error("Invalid environment file format in {path}: {reason}")] 16 | InvalidFormat { 17 | path: PathBuf, 18 | reason: String, 19 | }, 20 | 21 | #[error("Cycle detected in environment hierarchy starting at: {0}")] 22 | CycleDetected(PathBuf), 23 | 24 | #[error("Path resolution failed: {path}, reason: {reason}")] 25 | PathResolution { 26 | path: PathBuf, 27 | reason: String, 28 | }, 29 | 30 | #[error("Multiple parent declarations found in: {0}")] 31 | MultipleParents(PathBuf), 32 | 33 | #[error("Internal tree operation failed: {0}")] 34 | InternalError(String), 35 | } 36 | 37 | pub type TreeResult = Result; -------------------------------------------------------------------------------- /rsenv/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::collections::BTreeMap; 3 | use std::fs::{File, symlink_metadata}; 4 | use std::io::{BufRead, BufReader}; 5 | use std::env; 6 | 7 | use regex::Regex; 8 | use tracing::{debug, instrument}; 9 | use walkdir::WalkDir; 10 | use crate::errors::{TreeError, TreeResult}; 11 | use crate::util::path::{ensure_file_exists, PathExt}; 12 | 13 | pub mod envrc; 14 | pub mod edit; 15 | pub mod tree_traits; 16 | pub mod cli; 17 | pub mod util; 18 | pub mod errors; 19 | pub mod builder; 20 | pub mod arena; 21 | 22 | /// Expands environment variables in a path string 23 | /// Supports both $VAR and ${VAR} syntax 24 | fn expand_env_vars(path: &str) -> String { 25 | let mut result = path.to_string(); 26 | 27 | // Find all occurrences of $VAR or ${VAR} 28 | let env_var_pattern = Regex::new(r"\$(\w+)|\$\{(\w+)\}").unwrap(); 29 | 30 | // Collect all matches first to avoid borrow checker issues with replace_all 31 | let matches: Vec<_> = env_var_pattern.captures_iter(path).collect(); 32 | 33 | for cap in matches { 34 | // Get the variable name from either $VAR or ${VAR} pattern 35 | let var_name = cap.get(1).or_else(|| cap.get(2)).unwrap().as_str(); 36 | let var_placeholder = if cap.get(1).is_some() { 37 | format!("${}", var_name) 38 | } else { 39 | format!("${{{}}}", var_name) 40 | }; 41 | 42 | // Replace with environment variable value or empty string if not found 43 | if let Ok(var_value) = std::env::var(var_name) { 44 | result = result.replace(&var_placeholder, &var_value); 45 | } 46 | } 47 | 48 | result 49 | } 50 | 51 | #[instrument(level = "trace")] 52 | pub fn get_files(file_path: &Path) -> TreeResult> { 53 | ensure_file_exists(file_path)?; 54 | let (_, files, _) = build_env(file_path)?; 55 | Ok(files) 56 | } 57 | 58 | #[instrument(level = "trace")] 59 | pub fn print_files(file_path: &Path) -> TreeResult<()> { 60 | let files = get_files(file_path)?; 61 | for f in files { 62 | println!("{}", f.display()); 63 | } 64 | Ok(()) 65 | } 66 | 67 | #[instrument(level = "trace")] 68 | pub fn build_env_vars(file_path: &Path) -> TreeResult { 69 | ensure_file_exists(file_path)?; 70 | 71 | let mut env_vars = String::new(); 72 | let (variables, _, _) = build_env(file_path)?; 73 | 74 | for (k, v) in variables { 75 | env_vars.push_str(&format!("export {}={}\n", k, v)); 76 | } 77 | 78 | Ok(env_vars) 79 | } 80 | 81 | #[instrument(level = "trace")] 82 | pub fn is_dag(dir_path: &Path) -> TreeResult { 83 | let re = Regex::new(r"# rsenv: (.+)") 84 | .map_err(|e| TreeError::InternalError(e.to_string()))?; 85 | 86 | // Walk through each file in the directory 87 | for entry in WalkDir::new(dir_path) { 88 | let entry = entry.map_err(|e| TreeError::PathResolution { 89 | path: dir_path.to_path_buf(), 90 | reason: e.to_string(), 91 | })?; 92 | 93 | if entry.file_type().is_file() { 94 | let file = File::open(entry.path()) 95 | .map_err(TreeError::FileReadError)?; 96 | let reader = BufReader::new(file); 97 | 98 | for line in reader.lines() { 99 | let line = line.map_err(TreeError::FileReadError)?; 100 | if let Some(caps) = re.captures(&line) { 101 | let parent_references: Vec<&str> = caps[1].split_whitespace().collect(); 102 | if parent_references.len() > 1 { 103 | return Ok(true); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | Ok(false) 110 | } 111 | 112 | /// Recursively builds map of environment variables from the specified file and its parents. 113 | /// 114 | /// This function reads the specified `file_path` and extracts environment variables from it. 115 | /// It recognizes `export` statements to capture key-value pairs and uses special `# rsenv:` 116 | /// comments to identify parent files for further extraction. 117 | /// 118 | /// child wins against parent 119 | /// rightmost sibling wins 120 | #[instrument(level = "debug")] 121 | pub fn build_env(file_path: &Path) -> TreeResult<(BTreeMap, Vec, bool)> { 122 | warn_if_symlink(file_path)?; 123 | let file_path = file_path.to_canonical()?; 124 | ensure_file_exists(&file_path)?; 125 | debug!("Current file_path: {:?}", file_path); 126 | 127 | let mut variables: BTreeMap = BTreeMap::new(); 128 | let mut files_read: Vec = Vec::new(); 129 | let mut is_dag = false; 130 | 131 | let mut to_read_files: Vec = vec![file_path]; 132 | 133 | while let Some(current_file) = to_read_files.pop() { 134 | ensure_file_exists(¤t_file)?; 135 | if files_read.contains(¤t_file) { 136 | continue; 137 | } 138 | 139 | files_read.push(current_file.clone()); 140 | 141 | let (vars, parents) = extract_env(¤t_file)?; 142 | is_dag = is_dag || parents.len() > 1; 143 | 144 | debug!("vars: {:?}, parents: {:?}, is_dag: {:?}", vars, parents, is_dag); 145 | 146 | for (k, v) in vars { 147 | variables.entry(k).or_insert(v); // first entry wins 148 | } 149 | 150 | for parent in parents { 151 | to_read_files.push(parent); 152 | } 153 | } 154 | 155 | Ok((variables, files_read, is_dag)) 156 | } 157 | 158 | /// Extracts environment variables and the parent path from a specified file. 159 | /// 160 | /// This function reads the given `file_path` to: 161 | /// 162 | /// 1. Identify and extract environment variables specified using the `export` keyword. 163 | /// 2. Identify any parent environment file via the special `# rsenv:` comment. 164 | /// parent's path can be relative to the child's path. 165 | /// 166 | /// The current working directory is temporarily changed to the directory of the `file_path` 167 | /// during the extraction process to construct correct parent paths. It is restored 168 | /// afterward. 169 | /// 170 | /// # Arguments 171 | /// 172 | /// * `file_path` - A string slice representing the path to the .env file. The function 173 | /// will attempt to canonicalize this path. 174 | /// 175 | /// # Returns 176 | /// 177 | /// A `Result` containing: 178 | /// 179 | /// * A tuple with: 180 | /// - A `BTreeMap` with the key as the variable name and the value as its corresponding value. 181 | /// - An `Option` containing a `Utf8PathBuf` pointing to the parent env file, if specified. 182 | /// * An error if there's any problem reading the file, extracting the variables, or if the 183 | /// path is invalid. 184 | /// 185 | /// # Errors 186 | /// 187 | /// This function will return an error in the following situations: 188 | /// 189 | /// * The provided `file_path` is invalid. 190 | /// * There's an issue reading or processing the env file. 191 | /// * The parent path specified in `# rsenv:` is invalid or not specified properly. 192 | #[instrument(level = "debug")] 193 | pub fn extract_env(file_path: &Path) -> TreeResult<(BTreeMap, Vec)> { 194 | warn_if_symlink(file_path)?; 195 | let file_path = file_path.to_canonical()?; 196 | debug!("Current file_path: {:?}", file_path); 197 | 198 | // Save the original current directory, to restore it later 199 | let original_dir = env::current_dir() 200 | .map_err(|e| TreeError::InternalError(format!("Failed to get current dir: {}", e)))?; 201 | 202 | // Change the current directory in order to construct correct parent path 203 | let parent_dir = file_path.parent() 204 | .ok_or_else(|| TreeError::InvalidParent(file_path.clone()))?; 205 | env::set_current_dir(parent_dir) 206 | .map_err(|e| TreeError::InternalError(format!("Failed to change dir: {}", e)))?; 207 | 208 | debug!("Current directory: {:?}", env::current_dir().unwrap_or_default()); 209 | 210 | let file = File::open(&file_path) 211 | .map_err(TreeError::FileReadError)?; 212 | let reader = BufReader::new(file); 213 | 214 | let mut variables: BTreeMap = BTreeMap::new(); 215 | let mut parent_paths: Vec = Vec::new(); 216 | 217 | for line in reader.lines() { 218 | let line = line.map_err(TreeError::FileReadError)?; 219 | 220 | // Check for the rsenv comment 221 | if line.starts_with("# rsenv:") { 222 | let parents: Vec<&str> = line.trim_start_matches("# rsenv:").split_whitespace().collect(); 223 | for parent in parents { 224 | if !parent.is_empty() { 225 | // Expand environment variables in the path 226 | let expanded_path = expand_env_vars(parent); 227 | let parent_path = PathBuf::from(expanded_path).to_canonical() 228 | .map_err(|_| TreeError::InvalidParent(PathBuf::from(parent)))?; 229 | parent_paths.push(parent_path); 230 | } 231 | } 232 | debug!("parent_paths: {:?}", parent_paths); 233 | } 234 | 235 | // Check for the export prefix 236 | else if line.starts_with("export ") { 237 | let parts: Vec<&str> = line.split('=').collect(); 238 | if parts.len() > 1 { 239 | let var_name: Vec<&str> = parts[0].split_whitespace().collect(); 240 | if var_name.len() > 1 { 241 | variables.insert(var_name[1].to_string(), parts[1].to_string()); 242 | } 243 | } 244 | } 245 | } 246 | 247 | // After executing your code, restore the original current directory 248 | env::set_current_dir(original_dir) 249 | .map_err(|e| TreeError::InternalError(format!("Failed to restore dir: {}", e)))?; 250 | 251 | Ok((variables, parent_paths)) 252 | } 253 | 254 | #[instrument(level = "trace")] 255 | fn warn_if_symlink(file_path: &Path) -> TreeResult<()> { 256 | let metadata = symlink_metadata(file_path) 257 | .map_err(TreeError::FileReadError)?; 258 | if metadata.file_type().is_symlink() { 259 | eprintln!("Warning: The file {} is a symbolic link.", file_path.display()); 260 | } 261 | Ok(()) 262 | } 263 | 264 | /// Links a parent file to a child file by adding a special comment to the child file. 265 | /// The comment contains the relative path from the child to the parent. 266 | /// If the child file already has a parent, the function will replace the existing parent. 267 | /// If the child file has multiple parents, the function will return an error. 268 | #[instrument(level = "debug")] 269 | pub fn link(parent: &Path, child: &Path) -> TreeResult<()> { 270 | let parent = parent.to_canonical()?; 271 | let child = child.to_canonical()?; 272 | debug!("parent: {:?} <- child: {:?}", parent, child); 273 | 274 | let mut child_contents = std::fs::read_to_string(&child) 275 | .map_err(TreeError::FileReadError)?; 276 | let mut lines: Vec<_> = child_contents.lines().map(|s| s.to_string()).collect(); 277 | 278 | // Calculate the relative path from child to parent 279 | let relative_path = pathdiff::diff_paths(&parent, child.parent().unwrap()) 280 | .ok_or_else(|| TreeError::PathResolution { 281 | path: parent.clone(), 282 | reason: "Failed to compute relative path".to_string(), 283 | })?; 284 | 285 | // Find and count the lines that start with "# rsenv:" 286 | let mut rsenv_lines = 0; 287 | let mut rsenv_index = None; 288 | for (i, line) in lines.iter().enumerate() { 289 | if line.starts_with("# rsenv:") { 290 | rsenv_lines += 1; 291 | rsenv_index = Some(i); 292 | } 293 | } 294 | 295 | // Based on the count, perform the necessary operations 296 | match rsenv_lines { 297 | 0 => { 298 | // No "# rsenv:" line found, so we add it 299 | lines.insert(0, format!("# rsenv: {}", relative_path.display())); 300 | } 301 | 1 => { 302 | // One "# rsenv:" line found, so we replace it 303 | if let Some(index) = rsenv_index { 304 | lines[index] = format!("# rsenv: {}", relative_path.display()); 305 | } 306 | } 307 | _ => { 308 | // More than one "# rsenv:" line found, we throw an error 309 | return Err(TreeError::MultipleParents(child)); 310 | } 311 | } 312 | 313 | // Write the modified content back to the child file 314 | child_contents = lines.join("\n"); 315 | std::fs::write(&child, child_contents) 316 | .map_err(TreeError::FileReadError)?; 317 | 318 | Ok(()) 319 | } 320 | 321 | #[instrument(level = "debug")] 322 | pub fn unlink(child: &Path) -> TreeResult<()> { 323 | let child = child.to_canonical()?; 324 | debug!("child: {:?}", child); 325 | 326 | let mut child_contents = std::fs::read_to_string(&child) 327 | .map_err(TreeError::FileReadError)?; 328 | let mut lines: Vec<_> = child_contents.lines().map(|s| s.to_string()).collect(); 329 | 330 | // Find and count the lines that start with "# rsenv:" 331 | let mut rsenv_lines = 0; 332 | let mut rsenv_index = None; 333 | for (i, line) in lines.iter().enumerate() { 334 | if line.starts_with("# rsenv:") { 335 | rsenv_lines += 1; 336 | rsenv_index = Some(i); 337 | } 338 | } 339 | 340 | match rsenv_lines { 341 | 0 => {} 342 | 1 => { 343 | // One "# rsenv:" line found, so we replace it 344 | if let Some(index) = rsenv_index { 345 | lines[index] = "# rsenv:".to_string(); 346 | } 347 | } 348 | _ => { 349 | return Err(TreeError::MultipleParents(child)); 350 | } 351 | } 352 | // Write the modified content back to the child file 353 | child_contents = lines.join("\n"); 354 | std::fs::write(&child, child_contents) 355 | .map_err(TreeError::FileReadError)?; 356 | 357 | Ok(()) 358 | } 359 | 360 | /// links a list of env files together and build the hierarchical environment variables tree 361 | #[instrument(level = "debug")] 362 | pub fn link_all(nodes: &[PathBuf]) { 363 | debug!("nodes: {:?}", nodes); 364 | let mut parent = None; 365 | for node in nodes { 366 | if let Some(parent_path) = parent { 367 | link(parent_path, node).expect("Failed to link"); 368 | } else { 369 | unlink(node).unwrap(); 370 | } 371 | parent = Some(node); 372 | } 373 | } -------------------------------------------------------------------------------- /rsenv/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | 3 | use anyhow::{Context, Result}; 4 | use clap::{Args, Command, CommandFactory, Parser, Subcommand, ValueHint}; 5 | use clap_complete::{generate, Generator, Shell}; 6 | use colored::Colorize; 7 | use rsenv::cli::args::{Cli, Commands}; 8 | use rsenv::cli::commands::execute_command; 9 | use rsenv::edit::{ 10 | create_branches, create_vimscript, open_files_in_editor, select_file_with_suffix, 11 | }; 12 | use rsenv::envrc::update_dot_envrc; 13 | use rsenv::{build_env_vars, get_files, is_dag, link, link_all, print_files}; 14 | use std::cell::RefCell; 15 | use std::collections::BTreeMap; 16 | use std::fs::File; 17 | use std::io::{BufRead, BufReader, Write}; 18 | use std::rc::Rc; 19 | use std::{env, io, process}; 20 | use tracing::level_filters::LevelFilter; 21 | use tracing_subscriber::filter::filter_fn; 22 | use tracing_subscriber::fmt::format::FmtSpan; 23 | use tracing_subscriber::layer::SubscriberExt; 24 | use tracing_subscriber::util::SubscriberInitExt; 25 | use tracing_subscriber::{fmt, Layer}; 26 | 27 | fn print_completions(gen: G, cmd: &mut Command) { 28 | generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); 29 | } 30 | 31 | fn main() { 32 | let cli = Cli::parse(); 33 | 34 | if let Some(generator) = cli.generator { 35 | let mut cmd = Cli::command(); 36 | eprintln!("Generating completion file for {generator:?}..."); 37 | print_completions(generator, &mut cmd); 38 | } 39 | if cli.info { 40 | use clap::CommandFactory; // Trait which returns the current command 41 | if let Some(a) = Cli::command().get_author() { 42 | println!("AUTHOR: {}", a) 43 | } 44 | if let Some(v) = Cli::command().get_version() { 45 | println!("VERSION: {}", v) 46 | } 47 | } 48 | 49 | setup_logging(cli.debug); 50 | 51 | if let Err(e) = execute_command(&cli) { 52 | eprintln!("{}", format!("Error: {}", e).red()); 53 | std::process::exit(1); 54 | } 55 | } 56 | 57 | fn setup_logging(verbosity: u8) { 58 | tracing::debug!("INIT: Attempting logger init from main.rs"); 59 | 60 | let filter = match verbosity { 61 | 0 => LevelFilter::WARN, 62 | 1 => LevelFilter::INFO, 63 | 2 => LevelFilter::DEBUG, 64 | 3 => LevelFilter::TRACE, 65 | _ => { 66 | eprintln!("Don't be crazy, max is -d -d -d"); 67 | LevelFilter::TRACE 68 | } 69 | }; 70 | 71 | // Create a noisy module filter (Gotcha: empty matches all!) 72 | let noisy_modules = ["x"]; 73 | let module_filter = filter_fn(move |metadata| { 74 | !noisy_modules 75 | .iter() 76 | .any(|name| metadata.target().starts_with(name)) 77 | }); 78 | 79 | // Create a subscriber with formatted output directed to stderr 80 | let fmt_layer = fmt::layer() 81 | .with_writer(std::io::stderr) // Set writer first 82 | .with_target(true) 83 | .with_thread_names(false) 84 | .with_span_events(FmtSpan::ENTER) 85 | .with_span_events(FmtSpan::CLOSE); 86 | 87 | // Apply filters to the layer 88 | let filtered_layer = fmt_layer.with_filter(filter).with_filter(module_filter); 89 | 90 | tracing_subscriber::registry().with(filtered_layer).init(); 91 | 92 | // Log initial debug level 93 | match filter { 94 | LevelFilter::INFO => tracing::info!("Debug mode: info"), 95 | LevelFilter::DEBUG => tracing::debug!("Debug mode: debug"), 96 | LevelFilter::TRACE => tracing::debug!("Debug mode: trace"), 97 | _ => {} 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use super::*; 104 | use rsenv::util::testing; 105 | use tracing::info; 106 | 107 | #[ctor::ctor] 108 | fn init() { 109 | testing::init_test_setup(); 110 | } 111 | 112 | // https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#testing 113 | #[test] 114 | fn verify_cli() { 115 | use clap::CommandFactory; 116 | Cli::command().debug_assert(); 117 | info!("Debug mode: info"); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /rsenv/src/tree_traits.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Workaround for error: https://doc.rust-lang.org/error_codes/E0116.html 3 | Cannot define inherent `impl` for a type outside of the crate where the type is defined 4 | 5 | define a trait that has the desired associated functions/types/constants and implement the trait for the type in question 6 | */ 7 | use crate::arena::TreeArena; 8 | use generational_arena::Index; 9 | use termtree::Tree; 10 | use tracing::instrument; 11 | 12 | pub trait TreeNodeConvert { 13 | fn to_tree_string(&self) -> Tree; 14 | } 15 | 16 | impl TreeNodeConvert for TreeArena { 17 | #[instrument(level = "trace", skip(self))] 18 | fn to_tree_string(&self) -> Tree { 19 | if let Some(root_idx) = self.root() { 20 | let mut tree = Tree::new(self.get_node(root_idx).unwrap().data.file_path.display().to_string()); 21 | 22 | fn build_tree(arena: &TreeArena, node_idx: Index, parent_tree: &mut Tree) { 23 | if let Some(node) = arena.get_node(node_idx) { 24 | for &child_idx in &node.children { 25 | if let Some(child) = arena.get_node(child_idx) { 26 | let mut child_tree = Tree::new(child.data.file_path.display().to_string()); 27 | build_tree(arena, child_idx, &mut child_tree); 28 | parent_tree.push(child_tree); 29 | } 30 | } 31 | } 32 | } 33 | 34 | build_tree(self, root_idx, &mut tree); 35 | tree 36 | } else { 37 | Tree::new("Empty tree".to_string()) 38 | } 39 | } 40 | } 41 | 42 | #[instrument(level = "trace", skip(tree))] 43 | pub fn build_tree_representation(tree: &TreeArena, node_idx: Index, tree_repr: &mut Tree) { 44 | if let Some(node) = tree.get_node(node_idx) { 45 | // Sort children only for display purposes 46 | let mut children = node.children.clone(); 47 | children.sort_by(|a, b| { 48 | let a_node = tree.get_node(*a).unwrap(); 49 | let b_node = tree.get_node(*b).unwrap(); 50 | a_node.data.file_path.cmp(&b_node.data.file_path) 51 | }); 52 | 53 | for &child_idx in &children { 54 | if let Some(child_node) = tree.get_node(child_idx) { 55 | let mut child_tree = Tree::new(child_node.data.file_path.to_string_lossy().to_string()); 56 | build_tree_representation(tree, child_idx, &mut child_tree); 57 | tree_repr.push(child_tree); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rsenv/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod testing; 2 | pub mod path; -------------------------------------------------------------------------------- /rsenv/src/util/path.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{TreeError, TreeResult}; 2 | use std::ffi::OsStr; 3 | use std::path::{Path, PathBuf}; 4 | 5 | pub trait PathExt { 6 | fn is_env_file(&self) -> bool; 7 | fn to_canonical(&self) -> TreeResult; 8 | fn to_string_lossy_cached(&self) -> String; 9 | } 10 | 11 | impl PathExt for Path { 12 | fn is_env_file(&self) -> bool { 13 | self.extension() == Some(OsStr::new("env")) 14 | } 15 | 16 | fn to_canonical(&self) -> TreeResult { 17 | self.canonicalize().map_err(|e| TreeError::PathResolution { 18 | path: self.to_path_buf(), 19 | reason: e.to_string(), 20 | }) 21 | } 22 | 23 | fn to_string_lossy_cached(&self) -> String { 24 | self.to_string_lossy().into_owned() 25 | } 26 | } 27 | 28 | pub fn ensure_file_exists(path: &Path) -> TreeResult<()> { 29 | if !path.exists() { 30 | Err(TreeError::FileNotFound(path.to_path_buf())) 31 | } else if !path.is_file() { 32 | Err(TreeError::InvalidFormat { 33 | path: path.to_path_buf(), 34 | reason: "Not a file".to_string(), 35 | }) 36 | } else { 37 | Ok(()) 38 | } 39 | } 40 | 41 | pub fn get_relative_path(from: &Path, to: &Path) -> TreeResult { 42 | pathdiff::diff_paths(to, from).ok_or_else(|| TreeError::PathResolution { 43 | path: to.to_path_buf(), 44 | reason: "Could not compute relative path".to_string(), 45 | }) 46 | } 47 | 48 | // Helper function for cross-platform path comparison 49 | pub fn normalize_path_separator(s: &str) -> String { 50 | s.replace('\\', "/") 51 | } 52 | 53 | pub fn relativize_tree_str(tree_str: &str, start: &str) -> String { 54 | tree_str 55 | .lines() 56 | .map(|line| { 57 | // Preserve indentation and tree characters 58 | let prefix_end = line.find('/').unwrap_or(0); 59 | let prefix = &line[..prefix_end]; 60 | let path = &line[prefix_end..]; 61 | 62 | if path.contains('/') { 63 | format!("{}{}", prefix, relativize_path(path, start)) 64 | } else { 65 | line.to_string() 66 | } 67 | }) 68 | .collect::>() 69 | .join("\n") 70 | + "\n" 71 | } 72 | pub fn relativize_path(path: &str, start: &str) -> String { 73 | // Convert a single full path to a relative path starting at "start" 74 | match path.find(start) { 75 | Some(pos) => path[pos..].to_string(), 76 | None => path.to_string(), // If "start" is not found, return the original path 77 | } 78 | } 79 | 80 | pub fn relativize_paths(leaf_nodes: Vec, start: &str) -> Vec { 81 | // Convert a Vec of paths to relative paths starting at "start" 82 | leaf_nodes 83 | .iter() 84 | .map(|path| relativize_path(path, start)) 85 | .collect() 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | 92 | #[test] 93 | fn test_relativize_path_with_matching_path() { 94 | let path = "/some/dir/tests/foo/bar"; 95 | let start = "tests/"; 96 | let result = relativize_path(path, start); 97 | assert_eq!(result, "tests/foo/bar"); 98 | } 99 | 100 | #[test] 101 | fn test_relativize_path_with_no_matching_start() { 102 | let path = "/some/dir/foo/bar"; 103 | let start = "tests/"; 104 | let result = relativize_path(path, start); 105 | assert_eq!(result, "/some/dir/foo/bar"); 106 | } 107 | 108 | #[test] 109 | fn test_relativize_path_start_at_beginning() { 110 | let path = "tests/foo/bar"; 111 | let start = "tests/"; 112 | let result = relativize_path(path, start); 113 | assert_eq!(result, "tests/foo/bar"); 114 | } 115 | 116 | #[test] 117 | fn test_relativize_paths_with_mixed_cases() { 118 | let paths = vec![ 119 | "/some/dir/tests/foo/bar".to_string(), 120 | "/other/tests/baz".to_string(), 121 | "/not/matching/path".to_string(), 122 | ]; 123 | let start = "tests/"; 124 | let result = relativize_paths(paths, start); 125 | assert_eq!( 126 | result, 127 | vec![ 128 | "tests/foo/bar".to_string(), 129 | "tests/baz".to_string(), 130 | "/not/matching/path".to_string(), 131 | ] 132 | ); 133 | } 134 | 135 | #[test] 136 | fn test_relativize_paths_all_matching() { 137 | let paths = vec![ 138 | "tests/foo/bar".to_string(), 139 | "tests/baz".to_string(), 140 | "tests/another/file".to_string(), 141 | ]; 142 | let start = "tests/"; 143 | let result = relativize_paths(paths, start); 144 | assert_eq!( 145 | result, 146 | vec![ 147 | "tests/foo/bar".to_string(), 148 | "tests/baz".to_string(), 149 | "tests/another/file".to_string(), 150 | ] 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /rsenv/src/util/testing.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Once; 3 | use tracing::{debug, info}; 4 | use tracing_subscriber::{ 5 | filter::filter_fn, 6 | fmt::{self, format::FmtSpan}, 7 | prelude::*, 8 | EnvFilter, 9 | }; 10 | 11 | static TEST_SETUP: Once = Once::new(); 12 | 13 | pub fn init_test_setup() 14 | { 15 | TEST_SETUP.call_once(|| { 16 | if env::var("RUST_LOG").is_err() { 17 | env::set_var("RUST_LOG", "trace"); 18 | } 19 | // global logging subscriber, used by all tracing log macros 20 | setup_test_logging(); 21 | info!("Test Setup complete"); 22 | }); 23 | } 24 | 25 | fn setup_test_logging() { 26 | debug!("INIT: Attempting logger init from testing.rs"); 27 | if env::var("RUST_LOG").is_err() { 28 | env::set_var("RUST_LOG", "trace"); 29 | } 30 | 31 | // Create a filter for noisy modules 32 | let noisy_modules = [""]; 33 | let module_filter = filter_fn(move |metadata| { 34 | !noisy_modules 35 | .iter() 36 | .any(|name| metadata.target().starts_with(name)) 37 | }); 38 | 39 | // Set up the subscriber with environment filter 40 | let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")); 41 | 42 | // Build and set the subscriber 43 | let subscriber = tracing_subscriber::registry().with( 44 | fmt::layer() 45 | .with_writer(std::io::stderr) 46 | .with_target(true) 47 | .with_thread_names(false) 48 | .with_span_events(FmtSpan::ENTER) 49 | .with_span_events(FmtSpan::CLOSE) 50 | .with_filter(module_filter) 51 | .with_filter(env_filter), 52 | ); 53 | 54 | // Only set if we haven't already set a global subscriber 55 | if tracing::dispatcher::has_been_set() { 56 | debug!("Tracing subscriber already set"); 57 | } else { 58 | subscriber.try_init().unwrap_or_else(|e| { 59 | eprintln!("Error: Failed to set up logging: {}", e); 60 | }); 61 | } 62 | } 63 | 64 | // test 65 | #[cfg(test)] 66 | mod tests { 67 | use super::*; 68 | 69 | #[test] 70 | fn test_init_test_setup() { 71 | init_test_setup(); 72 | } 73 | } -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/complex/a/level3.env: -------------------------------------------------------------------------------- 1 | # rsenv: ../level2.env 2 | 3 | # Level3 overwrite 4 | export VAR_5=var_53 5 | export VAR_6=var_63 # new variable added 6 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/complex/dot.envrc: -------------------------------------------------------------------------------- 1 | # vim: set foldmethod=marker foldmarker={{{,}}}: 2 | #!/usr/bin/env bash 3 | # shellcheck disable=SC1091 4 | source "$HOME/dev/binx/profile/sane_fn.sh" 5 | PROJ_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | export PROJ_DIR 7 | Green "-M- exporting PROJ_DIR: $PROJ_DIR" 8 | 9 | ############### Python ############### 10 | # Emulate the pipenvs's activate, because we can't source things in direnv 11 | #layout_pipenv 12 | #layout_poetry 13 | #dotenv 14 | export PYTHONPATH=$PROJ_DIR 15 | export PIPENV_VENV_IN_PROJECT=1 # creates .venv 16 | #export POETRY_VIRTUALENVS_IN_PROJECT=1 # creates .venv 17 | 18 | if which tmux > /dev/null 2>&1; then 19 | tmux rename-window "$(basename "$PROJ_DIR")" 20 | fi 21 | 22 | ############### Exports ############### 23 | export RUN_ENV=local 24 | export senv="source $PROJ_DIR/scripts/env.sh" 25 | #export TW_FZF_ROOT="$HOME/dev" 26 | export TERRAFORM_PROMPT=0 27 | 28 | ############### Java ############### 29 | #export MAVEN_PROFILE=bmw 30 | #export JAVA_HOME="$HOME/.asdf/installs/java/openjdk-20" 31 | #PATH_add $JAVA_HOME/bin 32 | 33 | ############### BMW ############### 34 | #export GH_HOST=atc-github.azure.cloud.bmw 35 | #dotenv ~/dev/s/private/sec-sops/bmw.env 36 | 37 | PATH_add $PROJ_DIR/scripts 38 | 39 | ### unset for PyPi 40 | #unset TWINE_USERNAME 41 | #unset TWINE_PASSWORD 42 | 43 | # Default export, valid for all environments 44 | export VAR_1=var_1 45 | export VAR_2=var_2 46 | export VAR_3=var_3 47 | export VAR_4=var_4 48 | export VAR_5=var_5 49 | 50 | #------------------------------- confguard start -------------------------------- 51 | # config.relative = true 52 | # config.version = 2 53 | # state.sentinel = 'rs-sops-20ae57f0' 54 | # state.timestamp = '2023-07-29T13:07:30.006Z' 55 | # state.sourceDir = '$HOME/dev/s/private/rs-sops' 56 | export SOPS_PATH=$HOME/dev/s/private/sec-sops/confguard/rs-sops-20ae57f0 57 | dotenv $SOPS_PATH/environments/local.env 58 | #-------------------------------- confguard end --------------------------------- 59 | 60 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/complex/level1.env: -------------------------------------------------------------------------------- 1 | # rsenv: dot.envrc 2 | 3 | # Level1 overwrite 4 | export VAR_3=var_31 5 | export VAR_4=var_41 6 | export VAR_5=var_51 7 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/complex/level2.env: -------------------------------------------------------------------------------- 1 | # rsenv: level1.env 2 | 3 | # Level2 overwrite 4 | export VAR_4=var_42 5 | export VAR_5=var_52 6 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/complex/level4.env: -------------------------------------------------------------------------------- 1 | # rsenv: a/level3.env 2 | 3 | export VAR_6=var_64 4 | export VAR_7=var_74 5 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/complex/result.env: -------------------------------------------------------------------------------- 1 | export VAR_1=var_1 2 | export VAR_2=var_2 3 | export VAR_3=var_31 4 | export VAR_4=var_42 5 | export VAR_5=var_53 6 | export VAR_6=var_64 7 | export VAR_7=var_74 8 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/fail/level11.env: -------------------------------------------------------------------------------- 1 | # rsenv: not-existing.env 2 | export var1=1 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/fail/root.env: -------------------------------------------------------------------------------- 1 | export root=root 2 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph/level11.env: -------------------------------------------------------------------------------- 1 | # rsenv: root.env 2 | export var11=11 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph/level12.env: -------------------------------------------------------------------------------- 1 | # rsenv: root.env 2 | export var12=12 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph/level13.env: -------------------------------------------------------------------------------- 1 | # rsenv: root.env 2 | export var13=13 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph/level21.env: -------------------------------------------------------------------------------- 1 | # rsenv: level11.env level12.env level13.env 2 | export var21=21 3 | export root=21 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph/level31.env: -------------------------------------------------------------------------------- 1 | # rsenv: level21.env root.env 2 | # rightmost sibling wins (NOT level21.env, but root.env) 3 | export var13=31 4 | export var31=31 5 | #export root=31 6 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph/result.env: -------------------------------------------------------------------------------- 1 | export root=root 2 | export var11=11 3 | export var12=12 4 | export var13=31 5 | export var21=21 6 | export var31=31 7 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph/root.env: -------------------------------------------------------------------------------- 1 | export root=root 2 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph/unlinked.env: -------------------------------------------------------------------------------- 1 | export unlinked=unlinked 2 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph2/error.env: -------------------------------------------------------------------------------- 1 | # rsenv: not-existing.env 2 | export error=error 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph2/global1.env: -------------------------------------------------------------------------------- 1 | export global=global1 2 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph2/level21.env: -------------------------------------------------------------------------------- 1 | # rsenv: global1.env shared.env 2 | export var21=21 3 | export global=21 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph2/level22.env: -------------------------------------------------------------------------------- 1 | # rsenv: global1.env shared.env 2 | export var22=22 3 | export var222=222 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph2/result1.env: -------------------------------------------------------------------------------- 1 | export global=21 2 | export shared=shared 3 | export var21=21 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph2/result2.env: -------------------------------------------------------------------------------- 1 | export global=global1 2 | export shared=shared 3 | export var22=22 4 | export var222=222 5 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/graph2/shared.env: -------------------------------------------------------------------------------- 1 | export shared=shared 2 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/max_prefix/aws/root.env: -------------------------------------------------------------------------------- 1 | #################################### root.env #################################### 2 | export VAR1=from_root 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/max_prefix/confguard/xxx/local.env: -------------------------------------------------------------------------------- 1 | ################################### local.env ################################### 2 | # rsenv: ../../aws/root.env 3 | export VAR2=from_local 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/a_int.env: -------------------------------------------------------------------------------- 1 | export a_var1=a_int_var1 2 | export a_var2=a_int_var2 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/a_prod.env: -------------------------------------------------------------------------------- 1 | export a_var1=a_prod_var1 2 | export a_var2=a_prod_var2 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/a_test.env: -------------------------------------------------------------------------------- 1 | export a_var1=a_test_var1 2 | export a_var2=a_test_var2 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/b_int.env: -------------------------------------------------------------------------------- 1 | # rsenv: a_int.env 2 | export b_var1=b_int_var1 3 | export b_var2=b_int_var2 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/b_prod.env: -------------------------------------------------------------------------------- 1 | # rsenv: a_prod.env 2 | export b_var1=b_prod_var1 3 | export b_var2=b_prod_var2 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/b_test.env: -------------------------------------------------------------------------------- 1 | # rsenv: a_test.env 2 | export b_var1=b_test_var1 3 | export b_var2=b_test_var2 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/int.env: -------------------------------------------------------------------------------- 1 | # rsenv: b_int.env 2 | export var1=int_var1 3 | export var2=int_var2 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/openfiles.vim: -------------------------------------------------------------------------------- 1 | " vim -S openfiles.vim 2 | " openfiles.vim 3 | 4 | " Open the first set of files (those containing 'test.env') in the first column 5 | edit a_test.env 6 | split b_test.env 7 | split test.env 8 | 9 | split a_int.env 10 | " move to right column 11 | wincmd L 12 | split b_int.env 13 | split int.env 14 | 15 | split a_prod.env 16 | wincmd L 17 | split b_prod.env 18 | "split prod.env 19 | 20 | " make distribution equal 21 | wincmd = 22 | 23 | " jumpt to left top corner 24 | 1wincmd w 25 | 26 | " Move cursor back to the top-left window 27 | "wincmd H 28 | "wincmd k 29 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/prod.env: -------------------------------------------------------------------------------- 1 | # rsenv: b_prod.env 2 | export var1=prod_var1 3 | export var2=prod_var2 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/parallel/test.env: -------------------------------------------------------------------------------- 1 | # rsenv: b_test.env 2 | export var1=test_var1 3 | export var2=test_var2 4 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree/level11.env: -------------------------------------------------------------------------------- 1 | # rsenv: root.env 2 | export var1=1 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree/level12.env: -------------------------------------------------------------------------------- 1 | # rsenv: root.env 2 | export var2=12 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree/level13.env: -------------------------------------------------------------------------------- 1 | # rsenv: root.env 2 | export var3=3 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree/level21.env: -------------------------------------------------------------------------------- 1 | # rsenv: level12.env 2 | export var21=21 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree/level22.env: -------------------------------------------------------------------------------- 1 | # rsenv: level12.env 2 | export var22=22 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree/level32.env: -------------------------------------------------------------------------------- 1 | # rsenv: level22.env 2 | export var31=32 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree/root.env: -------------------------------------------------------------------------------- 1 | export root=root 2 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree2/confguard/level11.env: -------------------------------------------------------------------------------- 1 | # rsenv: ../root.env 2 | export var1=1 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree2/confguard/level12.env: -------------------------------------------------------------------------------- 1 | # rsenv: ../root.env 2 | export var2=12 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree2/confguard/level13.env: -------------------------------------------------------------------------------- 1 | # rsenv: ../root.env 2 | export var3=3 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree2/confguard/level21.env: -------------------------------------------------------------------------------- 1 | # rsenv: level12.env 2 | export var21=21 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree2/confguard/level22.env: -------------------------------------------------------------------------------- 1 | # rsenv: level12.env 2 | export var22=22 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree2/confguard/subdir/level32.env: -------------------------------------------------------------------------------- 1 | # rsenv: ../level22.env 2 | export var31=32 3 | -------------------------------------------------------------------------------- /rsenv/tests/resources/environments/tree2/root.env: -------------------------------------------------------------------------------- 1 | export root=root 2 | -------------------------------------------------------------------------------- /rsenv/tests/test_edit.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::path::Path; 3 | use std::process::Command; 4 | 5 | use rstest::rstest; 6 | 7 | use rsenv::builder::TreeBuilder; 8 | use rsenv::edit::{ 9 | create_branches, create_vimscript, open_files_in_editor, select_file_with_suffix, 10 | }; 11 | use rsenv::errors::TreeResult; 12 | use rsenv::get_files; 13 | 14 | #[rstest] 15 | #[ignore = "Interactive via Makefile"] 16 | fn given_directory_when_selecting_file_with_suffix_then_returns_valid_file() -> TreeResult<()> { 17 | let dir = Path::new("./tests/resources/data"); 18 | let suffix = ".env"; 19 | let result = select_file_with_suffix(dir, suffix)?; 20 | println!("Selected: {}", result.display()); 21 | assert!(result.to_string_lossy().ends_with(suffix)); 22 | Ok(()) 23 | } 24 | 25 | #[rstest] 26 | #[ignore = "Interactive via Makefile"] 27 | fn given_valid_files_when_opening_in_editor_then_opens_successfully() -> TreeResult<()> { 28 | let files = get_files(Path::new( 29 | "./tests/resources/environments/complex/level4.env", 30 | ))?; 31 | open_files_in_editor(files)?; 32 | Ok(()) 33 | } 34 | 35 | #[rstest] 36 | #[ignore = "Interactive via Makefile"] 37 | fn given_file_list_when_creating_vimscript_then_generates_valid_interactive_script() -> TreeResult<()> { 38 | let files = [vec!["a_test.env", "b_test.env", "test.env"], 39 | vec!["a_int.env", "b_int.env", "int.env"], 40 | vec!["a_prod.env"]]; 41 | 42 | let script = create_vimscript( 43 | files 44 | .iter() 45 | .map(|v| v.iter().map(Path::new).collect()) 46 | .collect(), 47 | ); 48 | println!("{}", script); 49 | 50 | // Save script to file 51 | let vimscript_filename = "tests/resources/environments/generated.vim"; 52 | let mut file = std::fs::File::create(vimscript_filename)?; 53 | file.write_all(script.as_bytes())?; 54 | 55 | // Run vim with the generated script 56 | let status = Command::new("vim") 57 | .arg("-S") 58 | .arg(vimscript_filename) 59 | .status()?; 60 | 61 | println!("Vim exited with status: {:?}", status); 62 | Ok(()) 63 | } 64 | 65 | #[rstest] 66 | fn given_file_list_when_creating_vimscript_then_generates_expected_script() { 67 | let files = [vec!["a_test.env", "b_test.env", "test.env"], 68 | vec!["a_int.env", "b_int.env", "int.env"], 69 | vec!["a_prod.env"]]; 70 | 71 | let script = create_vimscript( 72 | files 73 | .iter() 74 | .map(|v| v.iter().map(Path::new).collect()) 75 | .collect(), 76 | ); 77 | 78 | let expected = "\ 79 | \" Open the first set of files ('a_test.env') in the first column 80 | edit a_test.env 81 | split b_test.env 82 | split test.env 83 | split a_int.env 84 | \" move to right column 85 | wincmd L 86 | split b_int.env 87 | split int.env 88 | split a_prod.env 89 | \" move to right column 90 | wincmd L 91 | 92 | \" make distribution equal 93 | wincmd = 94 | 95 | \" jump to left top corner 96 | 1wincmd w 97 | "; 98 | 99 | assert_eq!(script, expected); 100 | } 101 | 102 | #[rstest] 103 | fn given_tree_structure_when_creating_branches_then_returns_correct_branch_paths() -> TreeResult<()> { 104 | let mut builder = TreeBuilder::new(); 105 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/tree"))?; 106 | let mut result: Vec> = create_branches(&trees) 107 | .into_iter() 108 | .map(|branch| { 109 | branch 110 | .into_iter() 111 | .map(|path| { 112 | path.file_name() 113 | .expect("Invalid path") 114 | .to_string_lossy() 115 | .into_owned() 116 | }) 117 | .collect() 118 | }) 119 | .collect(); 120 | 121 | // Sort both result and expected for stable comparison 122 | result.sort(); 123 | 124 | let mut expected = vec![ 125 | vec!["level11.env", "root.env"], 126 | vec!["level13.env", "root.env"], 127 | vec!["level32.env", "level22.env", "level12.env", "root.env"], 128 | vec!["level21.env", "level12.env", "root.env"], 129 | ]; 130 | expected.sort(); 131 | assert_eq!(result, expected); 132 | Ok(()) 133 | } 134 | 135 | #[rstest] 136 | fn given_parallel_structure_when_creating_branches_then_returns_correct_paths() -> TreeResult<()> { 137 | let mut builder = TreeBuilder::new(); 138 | let trees = 139 | builder.build_from_directory(Path::new("./tests/resources/environments/parallel"))?; 140 | let mut result: Vec> = create_branches(&trees) 141 | .into_iter() 142 | .map(|branch| { 143 | branch 144 | .into_iter() 145 | .map(|path| { 146 | path.file_name() 147 | .expect("Invalid path") 148 | .to_string_lossy() 149 | .into_owned() 150 | }) 151 | .collect() 152 | }) 153 | .collect(); 154 | result.sort(); 155 | 156 | let mut expected = vec![ 157 | vec!["int.env", "b_int.env", "a_int.env"], 158 | vec!["prod.env", "b_prod.env", "a_prod.env"], 159 | vec!["test.env", "b_test.env", "a_test.env"], 160 | ]; 161 | expected.sort(); 162 | assert_eq!(result, expected); 163 | Ok(()) 164 | } 165 | 166 | #[rstest] 167 | fn given_complex_structure_when_creating_branches_then_returns_correct_hierarchy() -> TreeResult<()> { 168 | let mut builder = TreeBuilder::new(); 169 | let trees = 170 | builder.build_from_directory(Path::new("./tests/resources/environments/complex"))?; 171 | let mut result: Vec> = create_branches(&trees) 172 | .into_iter() 173 | .map(|branch| { 174 | branch 175 | .into_iter() 176 | .map(|path| { 177 | path.file_name() 178 | .expect("Invalid path") 179 | .to_string_lossy() 180 | .into_owned() 181 | }) 182 | .collect() 183 | }) 184 | .collect(); 185 | result.sort(); 186 | 187 | let mut expected = vec![vec![ 188 | "level4.env", 189 | "level3.env", 190 | "level2.env", 191 | "level1.env", 192 | "dot.envrc", 193 | ]]; 194 | expected.sort(); 195 | assert_eq!(result, expected); 196 | Ok(()) 197 | } 198 | -------------------------------------------------------------------------------- /rsenv/tests/test_env_vars.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::{PathBuf}; 3 | use std::fs::{self}; 4 | use rstest::{fixture, rstest}; 5 | use tempfile::tempdir; 6 | use rsenv::errors::TreeResult; 7 | use rsenv::util::testing; 8 | 9 | #[ctor::ctor] 10 | fn init() { 11 | testing::init_test_setup(); 12 | } 13 | 14 | #[fixture] 15 | fn temp_dir_with_env_vars() -> (PathBuf, String) { 16 | let temp_dir = tempdir().unwrap(); 17 | let temp_path = temp_dir.path().to_path_buf(); 18 | 19 | // Set a test environment variable 20 | let env_var_name = "RSENV_TEST_DIR"; 21 | env::set_var(env_var_name, temp_path.to_string_lossy().to_string()); 22 | 23 | // Create parent file 24 | let parent_path = temp_path.join("parent.env"); 25 | fs::write(&parent_path, "export PARENT_VAR=parent_value\n").unwrap(); 26 | 27 | // Create child file with $VAR syntax - using direct string writing 28 | let child_path = temp_path.join("child.env"); 29 | fs::write(&child_path, "# rsenv: $RSENV_TEST_DIR/parent.env\nexport CHILD_VAR=child_value\n").unwrap(); 30 | 31 | // Create child file with ${VAR} syntax 32 | let child_path2 = temp_path.join("child2.env"); 33 | fs::write(&child_path2, "# rsenv: ${RSENV_TEST_DIR}/parent.env\nexport CHILD2_VAR=child2_value\n").unwrap(); 34 | 35 | (temp_dir.into_path(), env_var_name.to_string()) 36 | } 37 | 38 | #[rstest] 39 | fn given_env_var_in_path_when_extracting_env_then_expands_variables(temp_dir_with_env_vars: (PathBuf, String)) -> TreeResult<()> { 40 | let (temp_path, env_var_name) = temp_dir_with_env_vars; 41 | 42 | // Test with $VAR syntax 43 | let child_path = temp_path.join("child.env"); 44 | let parent_path = temp_path.join("parent.env"); 45 | 46 | let (variables, files) = rsenv::extract_env(&child_path)?; 47 | 48 | // Verify environment variable was expanded correctly 49 | assert_eq!(files.len(), 1); 50 | assert_eq!(files[0], parent_path.canonicalize()?); 51 | assert_eq!(variables.get("CHILD_VAR"), Some(&"child_value".to_string())); 52 | 53 | // Test with build_env to ensure full integration 54 | let (all_vars, all_files, _) = rsenv::build_env(&child_path)?; 55 | 56 | // Verify variables from both files 57 | assert_eq!(all_vars.get("CHILD_VAR"), Some(&"child_value".to_string())); 58 | assert_eq!(all_vars.get("PARENT_VAR"), Some(&"parent_value".to_string())); 59 | 60 | // Verify both files were processed 61 | assert_eq!(all_files.len(), 2); 62 | assert!(all_files.contains(&child_path.canonicalize()?)); 63 | assert!(all_files.contains(&parent_path.canonicalize()?)); 64 | 65 | // Test with ${VAR} syntax 66 | let child_path2 = temp_path.join("child2.env"); 67 | 68 | let (_, files2) = rsenv::extract_env(&child_path2)?; 69 | assert_eq!(files2.len(), 1); 70 | assert_eq!(files2[0], parent_path.canonicalize()?); 71 | 72 | // Clean up 73 | env::remove_var(&env_var_name); 74 | 75 | Ok(()) 76 | } 77 | 78 | #[rstest] 79 | fn given_nonexistent_env_var_when_extracting_env_then_handles_gracefully() -> TreeResult<()> { 80 | // Create temporary directory 81 | let temp_dir = tempdir()?; 82 | let temp_path = temp_dir.path(); 83 | 84 | // Ensure the environment variable doesn't exist 85 | let non_existent_var = "RSENV_NONEXISTENT_VAR"; 86 | env::remove_var(non_existent_var); 87 | 88 | // Create child file with nonexistent environment variable - use direct writing 89 | let child_path = temp_path.join("child.env"); 90 | fs::write(&child_path, "# rsenv: ${RSENV_NONEXISTENT_VAR}/parent.env\nexport CHILD_VAR=child_value\n").unwrap(); 91 | 92 | // The extraction should fail gracefully with an appropriate error 93 | let result = rsenv::extract_env(&child_path); 94 | 95 | // Should be an error, and specifically an InvalidParent error 96 | assert!(result.is_err()); 97 | match result { 98 | Err(rsenv::errors::TreeError::InvalidParent(_)) => { 99 | // This is the expected error type 100 | assert!(true); 101 | }, 102 | _ => { 103 | // Any other error type or success is unexpected 104 | assert!(false, "Expected InvalidParent error but got: {:?}", result); 105 | } 106 | } 107 | 108 | Ok(()) 109 | } 110 | 111 | #[rstest] 112 | fn test_expand_env_vars() { 113 | // This function directly tests the expand_env_vars helper function 114 | // Since it's an internal function, we'll re-implement it here for testing 115 | fn expand_env_vars(path: &str) -> String { 116 | use regex::Regex; 117 | let mut result = path.to_string(); 118 | 119 | // Find all occurrences of $VAR or ${VAR} 120 | let env_var_pattern = Regex::new(r"\$(\w+)|\$\{(\w+)\}").unwrap(); 121 | 122 | // Collect all matches first to avoid borrow checker issues with replace_all 123 | let matches: Vec<_> = env_var_pattern.captures_iter(path).collect(); 124 | 125 | for cap in matches { 126 | // Get the variable name from either $VAR or ${VAR} pattern 127 | let var_name = cap.get(1).or_else(|| cap.get(2)).unwrap().as_str(); 128 | let var_placeholder = if cap.get(1).is_some() { 129 | format!("${}", var_name) 130 | } else { 131 | format!("${{{}}}", var_name) 132 | }; 133 | 134 | // Replace with environment variable value or empty string if not found 135 | if let Ok(var_value) = std::env::var(var_name) { 136 | result = result.replace(&var_placeholder, &var_value); 137 | } 138 | } 139 | 140 | result 141 | } 142 | 143 | // Set up test environment variables 144 | env::set_var("TEST_VAR_1", "value1"); 145 | env::set_var("TEST_VAR_2", "value2"); 146 | 147 | // Test $VAR syntax 148 | let input = "/path/$TEST_VAR_1/file"; 149 | let expected = "/path/value1/file"; 150 | assert_eq!(expand_env_vars(input), expected); 151 | 152 | // Test ${VAR} syntax 153 | let input = "/path/${TEST_VAR_2}/file"; 154 | let expected = "/path/value2/file"; 155 | assert_eq!(expand_env_vars(input), expected); 156 | 157 | // Test multiple variables 158 | let input = "/path/$TEST_VAR_1/${TEST_VAR_2}/file"; 159 | let expected = "/path/value1/value2/file"; 160 | assert_eq!(expand_env_vars(input), expected); 161 | 162 | // Test non-existent variable 163 | let input = "/path/$NONEXISTENT_VAR/file"; 164 | let expected = "/path/$NONEXISTENT_VAR/file"; // Should remain unchanged 165 | assert_eq!(expand_env_vars(input), expected); 166 | 167 | // Clean up 168 | env::remove_var("TEST_VAR_1"); 169 | env::remove_var("TEST_VAR_2"); 170 | } -------------------------------------------------------------------------------- /rsenv/tests/test_lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::os::unix::fs::symlink; 3 | use std::path::{Path, PathBuf}; 4 | use std::process::Command; 5 | use std::{env, fs}; 6 | 7 | use fs_extra::{copy_items, dir}; 8 | use regex::Regex; 9 | use rsenv::errors::{TreeError, TreeResult}; 10 | use rsenv::util::testing; 11 | use rsenv::{build_env, build_env_vars, extract_env, is_dag, link, link_all, print_files, unlink}; 12 | use rstest::{fixture, rstest}; 13 | use tempfile::tempdir; 14 | use tracing::debug; 15 | 16 | #[ctor::ctor] 17 | fn init() { 18 | testing::init_test_setup(); 19 | } 20 | 21 | #[fixture] 22 | fn temp_dir() -> PathBuf { 23 | let tempdir = tempdir().unwrap(); 24 | let options = dir::CopyOptions::new(); 25 | copy_items( 26 | &[ 27 | "tests/resources/environments/complex/level1.env", 28 | "tests/resources/environments/complex/level2.env", 29 | "tests/resources/environments/complex/a", 30 | ], 31 | tempdir.path(), 32 | &options, 33 | ) 34 | .expect("Failed to copy test project directory"); 35 | 36 | tempdir.into_path() 37 | } 38 | 39 | #[rstest] 40 | fn given_env_file_when_extracting_env_then_returns_correct_variables_and_parent() -> TreeResult<()> 41 | { 42 | let (variables, parent) = extract_env(Path::new( 43 | "./tests/resources/environments/complex/level4.env", 44 | ))?; 45 | debug!("variables: {:?}", variables); 46 | debug!("parent: {:?}", parent); 47 | assert_eq!(variables.get("VAR_6"), Some(&"var_64".to_string())); 48 | Ok(()) 49 | } 50 | 51 | #[rstest] 52 | fn given_env_file_when_building_env_then_returns_correct_variables_and_files() -> TreeResult<()> { 53 | let (variables, files, is_dag) = build_env(Path::new( 54 | "./tests/resources/environments/complex/level4.env", 55 | ))?; 56 | let (reference, _) = extract_env(Path::new( 57 | "./tests/resources/environments/complex/result.env", 58 | ))?; 59 | 60 | let filtered_map: BTreeMap<_, _> = variables 61 | .iter() 62 | .filter(|(k, _)| k.starts_with("VAR_")) 63 | .map(|(k, v)| (k.clone(), v.clone())) 64 | .collect(); 65 | println!("variables: {:#?}", filtered_map); 66 | println!("files: {:#?}", files); 67 | 68 | assert_eq!(filtered_map, reference, "The two BTreeMaps are not equal!"); 69 | assert!(!is_dag); 70 | Ok(()) 71 | } 72 | 73 | #[rstest] 74 | fn given_graph_structure_when_building_env_then_returns_correct_dag_variables() -> TreeResult<()> { 75 | let (variables, files, is_dag) = build_env(Path::new( 76 | "./tests/resources/environments/graph/level31.env", 77 | ))?; 78 | let (reference, _) = extract_env(Path::new("./tests/resources/environments/graph/result.env"))?; 79 | println!("variables: {:#?}", variables); 80 | println!("files: {:#?}", files); 81 | println!("reference: {:#?}", reference); 82 | 83 | assert_eq!(variables, reference, "The two BTreeMaps are not equal!"); 84 | assert!(is_dag); 85 | Ok(()) 86 | } 87 | 88 | #[rstest] 89 | fn given_complex_graph_when_building_env_then_returns_correct_dag_variables() -> TreeResult<()> { 90 | let (variables, files, is_dag) = build_env(Path::new( 91 | "./tests/resources/environments/graph2/level21.env", 92 | ))?; 93 | let (reference, _) = extract_env(Path::new( 94 | "./tests/resources/environments/graph2/result1.env", 95 | ))?; 96 | println!("variables: {:#?}", variables); 97 | println!("files: {:#?}", files); 98 | println!("reference: {:#?}", reference); 99 | 100 | assert_eq!(variables, reference, "The two BTreeMaps are not equal!"); 101 | assert!(is_dag); 102 | 103 | let (variables, _, is_dag) = build_env(Path::new( 104 | "./tests/resources/environments/graph2/level22.env", 105 | ))?; 106 | let (reference, _) = extract_env(Path::new( 107 | "./tests/resources/environments/graph2/result2.env", 108 | ))?; 109 | assert_eq!(variables, reference, "The two BTreeMaps are not equal!"); 110 | assert!(is_dag); 111 | Ok(()) 112 | } 113 | 114 | #[rstest] 115 | fn given_valid_env_file_when_building_vars_then_returns_correct_env_string() -> TreeResult<()> { 116 | let env_vars = build_env_vars(Path::new( 117 | "./tests/resources/environments/parallel/test.env", 118 | ))?; 119 | println!("{}", env_vars); 120 | let expected = "export a_var1=a_test_var1 121 | export a_var2=a_test_var2 122 | export b_var1=b_test_var1 123 | export b_var2=b_test_var2 124 | export var1=test_var1 125 | export var2=test_var2\n"; 126 | assert_eq!(env_vars, expected); 127 | Ok(()) 128 | } 129 | 130 | #[rstest] 131 | fn given_invalid_parent_when_building_env_vars_then_returns_error() -> TreeResult<()> { 132 | let original_dir = env::current_dir()?; 133 | let result = build_env_vars(Path::new("./tests/resources/environments/graph2/error.env")); 134 | match result { 135 | Ok(_) => panic!("Expected an error, but got OK"), 136 | Err(e) => { 137 | let re = Regex::new(r"Invalid parent path: .*not-existing.env") 138 | .expect("Invalid regex pattern"); 139 | assert!(re.is_match(&e.to_string())); 140 | } 141 | } 142 | env::set_current_dir(original_dir)?; // error occurs after change directory in extract_env 143 | Ok(()) 144 | } 145 | 146 | #[rstest] 147 | fn given_nonexistent_file_when_building_env_vars_then_returns_error() -> TreeResult<()> { 148 | let result = build_env_vars(Path::new("xxx")); 149 | match result { 150 | Ok(_) => panic!("Expected an error, but got OK"), 151 | Err(e) => { 152 | assert!(matches!(e, TreeError::FileNotFound(_))); 153 | } 154 | } 155 | Ok(()) 156 | } 157 | 158 | #[rstest] 159 | fn test_print_files() -> TreeResult<()> { 160 | print_files(Path::new( 161 | "./tests/resources/environments/complex/level4.env", 162 | ))?; 163 | Ok(()) 164 | } 165 | 166 | #[rstest] 167 | fn given_parent_child_files_when_linking_then_creates_correct_relationship( 168 | temp_dir: PathBuf, 169 | ) -> TreeResult<()> { 170 | let parent = temp_dir.join("a/level3.env"); 171 | let child = temp_dir.join("level1.env"); 172 | link(&parent, &child)?; 173 | 174 | let child_content = fs::read_to_string(&child)?; 175 | assert!(child_content.contains("# rsenv: a/level3.env")); 176 | Ok(()) 177 | } 178 | 179 | #[rstest] 180 | fn given_linked_file_when_unlinking_then_removes_relationship(temp_dir: PathBuf) -> TreeResult<()> { 181 | let child = temp_dir.join("a/level3.env"); 182 | unlink(&child)?; 183 | 184 | let child_content = fs::read_to_string(&child)?; 185 | assert!(child_content.contains("# rsenv:\n")); 186 | Ok(()) 187 | } 188 | 189 | #[rstest] 190 | fn given_multiple_files_when_linking_all_then_creates_correct_hierarchy( 191 | temp_dir: PathBuf, 192 | ) -> TreeResult<()> { 193 | let parent = temp_dir.join("a/level3.env"); 194 | let intermediate = temp_dir.join("level2.env"); 195 | let child = temp_dir.join("level1.env"); 196 | let nodes = vec![parent.clone(), intermediate.clone(), child.clone()]; 197 | link_all(&nodes); 198 | 199 | let child_content = fs::read_to_string(&child)?; 200 | assert!(child_content.contains("# rsenv: level2.env")); 201 | 202 | let child_content = fs::read_to_string(&intermediate)?; 203 | assert!(child_content.contains("# rsenv: a/level3.env")); 204 | 205 | let child_content = fs::read_to_string(&parent)?; 206 | assert!(child_content.contains("# rsenv:\n")); 207 | Ok(()) 208 | } 209 | 210 | #[rstest] 211 | fn given_tree_structure_when_checking_dag_then_returns_false() -> TreeResult<()> { 212 | assert!(!is_dag(Path::new( 213 | "./tests/resources/environments/complex" 214 | ))?); 215 | assert!(!is_dag(Path::new( 216 | "./tests/resources/environments/parallel" 217 | ))?); 218 | Ok(()) 219 | } 220 | 221 | #[rstest] 222 | fn given_graph_structure_when_checking_dag_then_returns_true() -> TreeResult<()> { 223 | assert!(is_dag(Path::new("./tests/resources/environments/graph"))?); 224 | Ok(()) 225 | } 226 | 227 | #[rstest] 228 | #[ignore = "Only for interactive exploration"] 229 | fn given_symlinked_file_when_extracting_env_then_handles_symlink_correctly() -> TreeResult<()> { 230 | let original_dir = env::current_dir()?; 231 | env::set_current_dir("./tests/resources/environments/complex")?; 232 | 233 | // 1. Create a symbolic link 234 | symlink("level4.env", "symlink.env")?; 235 | // 3. Run extract_env function 236 | let _ = extract_env(Path::new("./symlink.env")); 237 | let _ = fs::remove_file("./symlink.env"); 238 | 239 | // Reset to the original directory 240 | env::set_current_dir(original_dir)?; 241 | Ok(()) 242 | } 243 | 244 | #[rstest] 245 | fn given_symlinked_file_when_extracting_env_then_outputs_warning() -> TreeResult<()> { 246 | let original_dir = env::current_dir()?; 247 | env::set_current_dir("./tests/resources/environments/complex")?; 248 | let _ = fs::remove_file("./symlink.env"); 249 | symlink("level4.env", "symlink.env")?; 250 | env::set_current_dir(original_dir)?; 251 | 252 | // Step 2: Run the Rust binary as a subprocess 253 | let output = Command::new("cargo") 254 | .args([ 255 | "run", 256 | "--", 257 | "build", 258 | "./tests/resources/environments/complex/symlink.env", 259 | ]) 260 | .output() 261 | .expect("Failed to execute command"); 262 | 263 | // Step 3: Check stderr for the symlink warning 264 | let stderr_output = String::from_utf8(output.stderr).expect("invalid utf8 string"); 265 | println!("stderr_output: {}", stderr_output); 266 | assert!(stderr_output.contains("Warning: The file")); 267 | 268 | // Step 4: Cleanup by removing the symbolic link 269 | fs::remove_file("./tests/resources/environments/complex/symlink.env")?; 270 | Ok(()) 271 | } 272 | -------------------------------------------------------------------------------- /rsenv/tests/test_tree.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | 3 | use std::collections::{BTreeMap, HashMap}; 4 | use std::{env, fs}; 5 | use std::path::{Path, PathBuf}; 6 | use anyhow::Result; 7 | use fs_extra::{copy_items, dir}; 8 | use rsenv::{build_env, build_env_vars, extract_env, link, link_all, print_files, tree_traits, unlink}; 9 | use termtree::Tree; 10 | use rsenv::builder::TreeBuilder; 11 | use rsenv::arena::TreeArena; 12 | use generational_arena::Index; 13 | use rstest::rstest; 14 | use rsenv::util::path; 15 | use rsenv::util::path::normalize_path_separator; 16 | 17 | #[rstest] 18 | fn given_invalid_parent_path_when_building_trees_then_returns_error() -> Result<()> { 19 | let original_dir = env::current_dir()?; 20 | let mut builder = TreeBuilder::new(); 21 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/fail")); 22 | assert!(trees.is_err()); 23 | 24 | // Get the error message and print it for debugging 25 | let err_msg = trees.err().unwrap().to_string(); 26 | println!("Actual error message: {}", err_msg); 27 | 28 | // Check for PathResolution error with "not-existing.env" 29 | assert!(err_msg.contains("not-existing.env")); 30 | assert!(err_msg.contains("No such file or directory")); 31 | 32 | env::set_current_dir(original_dir)?; 33 | Ok(()) 34 | } 35 | 36 | 37 | #[rstest] 38 | fn given_complex_hierarchy_when_building_trees_then_returns_correct_depth_and_leaves() -> Result<()> { 39 | let mut builder = TreeBuilder::new(); 40 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/complex"))?; 41 | println!("trees: {:#?}", trees); 42 | for tree in &trees { 43 | println!("Depth of tree: {}", tree.depth()); 44 | assert_eq!(tree.depth(), 5); 45 | } 46 | for tree in &trees { 47 | let leaf_nodes = tree.leaf_nodes(); 48 | println!("Leaf nodes:"); 49 | for leaf in &leaf_nodes { 50 | println!("{}", leaf); 51 | } 52 | assert_eq!(leaf_nodes.len(), 1); 53 | assert!(leaf_nodes[0].ends_with("level4.env")); 54 | } 55 | Ok(()) 56 | } 57 | 58 | #[rstest] 59 | fn given_tree_structure_when_building_trees_then_returns_correct_hierarchy() -> Result<()> { 60 | let mut builder = TreeBuilder::new(); 61 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/tree"))?; 62 | println!("trees: {:#?}", trees); 63 | for tree in &trees { 64 | println!("Depth of tree: {}", tree.depth()); 65 | assert_eq!(tree.depth(), 4); 66 | } 67 | for tree in &trees { 68 | assert_eq!(trees.len(), 1); 69 | println!("Tree Root:"); 70 | 71 | let mut leaf_nodes = tree.leaf_nodes(); 72 | leaf_nodes.sort(); 73 | println!("Tree paths:"); 74 | for leaf in &leaf_nodes { 75 | println!("{}", leaf); 76 | } 77 | assert_eq!(leaf_nodes.len(), 4); 78 | assert!(leaf_nodes[0].ends_with("level11.env")); 79 | } 80 | Ok(()) 81 | } 82 | 83 | #[rstest] 84 | fn given_partial_root_match_when_printing_leaf_paths_then_handles_prefix_correctly() -> Result<()> { 85 | let mut builder = TreeBuilder::new(); 86 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/max_prefix/confguard/xxx"))?; 87 | assert_eq!(trees.len(), 1); 88 | for tree in &trees { 89 | let leaf_nodes = tree.leaf_nodes(); 90 | println!("Tree paths:"); 91 | for path in leaf_nodes { 92 | println!("{}", path); 93 | } 94 | } 95 | Ok(()) 96 | } 97 | 98 | #[rstest] 99 | fn given_non_root_location_when_printing_leaf_paths_then_resolves_paths_correctly()-> Result<()> { 100 | let mut builder = TreeBuilder::new(); 101 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/tree2/confguard"))?; 102 | assert_eq!(trees.len(), 1); 103 | 104 | for tree in &trees { 105 | let leaf_nodes = tree.leaf_nodes(); 106 | println!("Tree paths:"); 107 | assert_eq!(tree.depth(), 4); 108 | for path in &leaf_nodes { 109 | println!("{}", path); 110 | } 111 | 112 | let mut leaf_nodes = path::relativize_paths(leaf_nodes, "tests/"); 113 | 114 | // Sort for consistent comparison 115 | let mut expected = vec![ 116 | "tests/resources/environments/tree2/confguard/level11.env", 117 | "tests/resources/environments/tree2/confguard/level13.env", 118 | "tests/resources/environments/tree2/confguard/level21.env", 119 | "tests/resources/environments/tree2/confguard/subdir/level32.env", 120 | ]; 121 | expected.sort(); 122 | leaf_nodes.sort(); 123 | 124 | assert_eq!(leaf_nodes, expected); 125 | } 126 | Ok(()) 127 | } 128 | 129 | #[rstest] 130 | fn test_print() { 131 | let mut builder = TreeBuilder::new(); 132 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/complex")).unwrap(); 133 | for tree in &trees { 134 | for (idx, node) in tree.iter() { 135 | println!("{:?}: {}", idx, node.data.file_path.display()); 136 | } 137 | } 138 | // todo: assert order? 139 | } 140 | 141 | #[rstest] 142 | fn test_try_tree() { 143 | let mut tree1 = Tree::new("111"); 144 | let mut tree2 = Tree::new("222"); 145 | 146 | let mut tree = Tree::new("xxx"); 147 | tree.push(Tree::new("yyy")); 148 | tree.push(Tree::new("zzz")); 149 | 150 | tree2.push(tree); 151 | tree1.push(tree2); 152 | println!("{}", tree1); 153 | } 154 | 155 | #[rstest] 156 | fn test_print_tree() { 157 | let mut builder = TreeBuilder::new(); 158 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/complex")).unwrap(); 159 | for tree in trees { 160 | if let Some(root_idx) = tree.root() { 161 | if let Some(root_node) = tree.get_node(root_idx) { 162 | println!("{}", Tree::new(&root_node.data.file_path.to_string_lossy())); 163 | // todo: what should it show? 164 | } 165 | } 166 | } 167 | } 168 | 169 | #[rstest] 170 | #[ignore = "Only for interactive exploration"] 171 | fn test_print_tree_recursive() { 172 | // let trees = build_trees(Utf8Path::new("./tests/resources/environments/complex")).unwrap(); 173 | // let trees = build_trees(Utf8Path::new("./tests/resources/environments/tree")).unwrap(); 174 | let mut builder = TreeBuilder::new(); 175 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/parallel")).unwrap(); 176 | for tree in &trees { 177 | if let Some(root_idx) = tree.root() { 178 | if let Some(root_node) = tree.get_node(root_idx) { 179 | let mut tree_repr = Tree::new(root_node.data.file_path.to_string_lossy().to_string()); 180 | tree_traits::build_tree_representation(tree, root_idx, &mut tree_repr); 181 | println!("{}", tree_repr); 182 | } 183 | } 184 | } 185 | } 186 | 187 | #[rstest] 188 | fn given_complex_structure_when_printing_tree_then_shows_nested_hierarchy() { 189 | let expected = "tests/resources/environments/complex/dot.envrc 190 | └── tests/resources/environments/complex/level1.env 191 | └── tests/resources/environments/complex/level2.env 192 | └── tests/resources/environments/complex/a/level3.env 193 | └── tests/resources/environments/complex/level4.env\n"; 194 | 195 | let mut builder = TreeBuilder::new(); 196 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/complex")).unwrap(); 197 | assert_eq!(trees.len(), 1); 198 | for tree in &trees { 199 | if let Some(root_idx) = tree.root() { 200 | if let Some(root_node) = tree.get_node(root_idx) { 201 | let mut tree_repr = Tree::new(root_node.data.file_path.to_string_lossy().to_string()); 202 | tree_traits::build_tree_representation(tree, root_idx, &mut tree_repr); 203 | let tree_str = tree_repr.to_string(); 204 | // Convert absolute paths to relative using path helper 205 | let relative_str = path::relativize_tree_str(&tree_str, "tests/"); 206 | println!("{}", relative_str); 207 | assert_eq!(normalize_path_separator(&relative_str), normalize_path_separator(expected)); 208 | } 209 | } 210 | } 211 | } 212 | 213 | #[rstest] 214 | fn given_parallel_structure_when_printing_tree_then_shows_correct_hierarchy() { 215 | let expected = "tests/resources/environments/parallel/a_test.env 216 | └── tests/resources/environments/parallel/b_test.env 217 | └── tests/resources/environments/parallel/test.env\n"; 218 | 219 | let mut builder = TreeBuilder::new(); 220 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/parallel")).unwrap(); 221 | assert_eq!(trees.len(), 3); 222 | for tree in &trees { 223 | if let Some(root_idx) = tree.root() { 224 | if let Some(root_node) = tree.get_node(root_idx) { 225 | let mut tree_repr = Tree::new(root_node.data.file_path.to_string_lossy().to_string()); 226 | tree_traits::build_tree_representation(tree, root_idx, &mut tree_repr); 227 | let tree_str = tree_repr.to_string(); 228 | let relative_str = path::relativize_tree_str(&tree_str, "tests/"); 229 | println!("{}", relative_str); 230 | if relative_str.contains("test.env") { 231 | assert_eq!(normalize_path_separator(&relative_str), normalize_path_separator(expected)); 232 | } 233 | } 234 | } 235 | } 236 | } 237 | 238 | #[rstest] 239 | fn given_tree_structure_when_printing_complete_tree_then_shows_all_branches() { 240 | let expected = "tests/resources/environments/tree/root.env 241 | ├── tests/resources/environments/tree/level11.env 242 | ├── tests/resources/environments/tree/level12.env 243 | │ ├── tests/resources/environments/tree/level21.env 244 | │ └── tests/resources/environments/tree/level22.env 245 | │ └── tests/resources/environments/tree/level32.env 246 | └── tests/resources/environments/tree/level13.env\n"; 247 | 248 | let mut builder = TreeBuilder::new(); 249 | let trees = builder.build_from_directory(Path::new("./tests/resources/environments/tree")).unwrap(); 250 | assert_eq!(trees.len(), 1); 251 | for tree in &trees { 252 | if let Some(root_idx) = tree.root() { 253 | if let Some(root_node) = tree.get_node(root_idx) { 254 | let mut tree_repr = Tree::new(root_node.data.file_path.to_string_lossy().to_string()); 255 | tree_traits::build_tree_representation(tree, root_idx, &mut tree_repr); 256 | let tree_str = tree_repr.to_string(); 257 | let relative_str = path::relativize_tree_str(&tree_str, "tests/"); 258 | println!("{}", relative_str); 259 | assert_eq!(normalize_path_separator(&relative_str), normalize_path_separator(expected)); 260 | } 261 | } 262 | } 263 | } 264 | 265 | -------------------------------------------------------------------------------- /rsenv/tests/test_update_dot_envrc.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::Read; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use rstest::{fixture, rstest}; 6 | use tempfile::tempdir; 7 | use fs_extra::{copy_items, dir}; 8 | use rsenv::build_env_vars; 9 | use rsenv::envrc::{delete_section, update_dot_envrc, END_SECTION_DELIMITER, START_SECTION_DELIMITER}; 10 | use rsenv::errors::TreeResult; 11 | 12 | #[fixture] 13 | fn temp_dir() -> PathBuf { 14 | let tempdir = tempdir().unwrap(); 15 | let options = dir::CopyOptions::new(); 16 | copy_items( 17 | &[ 18 | "tests/resources/environments/complex/dot.envrc", 19 | ], 20 | tempdir.path(), 21 | &options, 22 | ).expect("Failed to copy test project directory"); 23 | 24 | tempdir.into_path() 25 | } 26 | 27 | fn get_file_contents(path: &Path) -> TreeResult { 28 | let mut file = fs::File::open(path)?; 29 | let mut contents = String::new(); 30 | file.read_to_string(&mut contents)?; 31 | Ok(contents) 32 | } 33 | 34 | #[rstest] 35 | fn given_envrc_file_when_updating_then_adds_correct_section(temp_dir: PathBuf) -> TreeResult<()> { 36 | let path = temp_dir.join("dot.envrc"); 37 | let data = build_env_vars(Path::new("./tests/resources/environments/complex/level4.env"))?; 38 | 39 | update_dot_envrc(&path, &data)?; 40 | 41 | let file_contents = get_file_contents(&path)?; 42 | let conf_guard_start = file_contents 43 | .find(START_SECTION_DELIMITER) 44 | .unwrap(); 45 | let conf_guard_end = file_contents 46 | .find(END_SECTION_DELIMITER) 47 | .unwrap(); 48 | let conf_guard_section = &file_contents[conf_guard_start..conf_guard_end]; 49 | 50 | assert!(conf_guard_section.contains(START_SECTION_DELIMITER)); 51 | assert!(conf_guard_section.contains(&data)); 52 | assert!(path.exists()); 53 | println!("file_contents: {}", file_contents); 54 | Ok(()) 55 | } 56 | 57 | #[rstest] 58 | fn given_envrc_with_section_when_deleting_then_removes_section(temp_dir: PathBuf) -> TreeResult<()> { 59 | let path = temp_dir.join("dot.envrc"); 60 | let data = build_env_vars(Path::new("./tests/resources/environments/complex/level4.env"))?; 61 | 62 | // Given: section has been added 63 | update_dot_envrc(&path, &data)?; 64 | 65 | // When: section is deleted 66 | delete_section(&path)?; 67 | 68 | let file_contents = get_file_contents(&path)?; 69 | assert!(!file_contents.contains(START_SECTION_DELIMITER)); 70 | assert!(!file_contents.contains(&data)); 71 | println!("file_contents: {}", file_contents); 72 | Ok(()) 73 | } 74 | 75 | #[rstest] 76 | fn given_multiple_updates_when_updating_envrc_then_maintains_single_section(temp_dir: PathBuf) -> TreeResult<()> { 77 | let path = temp_dir.join("dot.envrc"); 78 | let data1 = build_env_vars(Path::new("./tests/resources/environments/complex/level4.env"))?; 79 | let data2 = build_env_vars(Path::new("./tests/resources/environments/complex/a/level3.env"))?; 80 | 81 | // First update 82 | update_dot_envrc(&path, &data1)?; 83 | // Second update should replace the first section 84 | update_dot_envrc(&path, &data2)?; 85 | 86 | let file_contents = get_file_contents(&path)?; 87 | assert!(file_contents.contains(&data2)); 88 | assert!(!file_contents.contains(&data1)); 89 | 90 | // Should only have one set of delimiters 91 | assert_eq!(file_contents.matches(START_SECTION_DELIMITER).count(), 1); 92 | assert_eq!(file_contents.matches(END_SECTION_DELIMITER).count(), 1); 93 | 94 | Ok(()) 95 | } -------------------------------------------------------------------------------- /scripts/print_env.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | """Script to test plugin 4 | """ 5 | 6 | 7 | def print_sorted_env_vars(): 8 | """ 9 | Print the environment variables in sorted order. 10 | """ 11 | # Get environment variables 12 | env_vars = os.environ 13 | 14 | # Sort the environment variables by their names 15 | sorted_env_vars = sorted(env_vars.items()) 16 | 17 | # Print each environment variable and its value 18 | for name, value in sorted_env_vars: 19 | if name.startswith("LESS_TERMCAP"): # color output 20 | continue 21 | if name.startswith("BASH_FUNC"): 22 | continue 23 | if name.startswith("DIRENV"): 24 | continue 25 | if name.startswith("is_"): 26 | continue 27 | if name.startswith("_"): 28 | continue 29 | print(f"{name}: {value}") 30 | 31 | if name == "RUN_ENV" and value == "local": 32 | if name == "AWS_PROFILE": 33 | assert value == "xxx" 34 | elif name == "RUN_ENV" and value == "test": 35 | if name == "AWS_PROFILE": 36 | assert value == "e4m-test-userfull" 37 | 38 | 39 | # Example usage 40 | if __name__ == "__main__": 41 | print_sorted_env_vars() 42 | -------------------------------------------------------------------------------- /scripts/rsenv.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # DO NOT DELETE 4 | # 5 | [[ -f "$SOPS_PATH/environments/${RUN_ENV:-local}.env" ]] && rsenv build "$SOPS_PATH/environments/${RUN_ENV:-local}.env" 6 | -------------------------------------------------------------------------------- /src/example_package/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysid/rs-env/86e82958e53092fc6be7d0b91e4688faf50a8feb/src/example_package/__init__.py -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysid/rs-env/86e82958e53092fc6be7d0b91e4688faf50a8feb/tests/__init__.py --------------------------------------------------------------------------------