diff --git a/.gitlab/renovate.json5 b/.gitlab/renovate.json5 index 88197fc..174045d 100644 --- a/.gitlab/renovate.json5 +++ b/.gitlab/renovate.json5 @@ -16,7 +16,7 @@ }, "postUpgradeTasks": { "commands": [ - "nix-portable nix run .#soonix:update" + "nix-portable nix run .#soonix -- update --skip-gitignore" ] } } diff --git a/lib/default.nix b/lib/default.nix index ae29377..0ce2d3f 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -22,4 +22,5 @@ in rec { }; mkShellHook = userConfig: (make userConfig).config.shellHook; + mkCLI = userConfig: (make userConfig).config.packages.soonix; } diff --git a/lib/module.nix b/lib/module.nix index 4546319..3ea44aa 100644 --- a/lib/module.nix +++ b/lib/module.nix @@ -146,15 +146,14 @@ in { Packages for updating the hooks without a devshell. (readonly) ''; example = { - "soonix:update" = ""; + "soonix" = ""; }; }; }; config = let hooks = config.hooks; - hookNames = builtins.attrNames hooks; - + # allow excluding gitignore since stuff like renovate can't use/commit it anyways runHooks = concatMapStringsSep "\n" (hookName: let hook = hooks.${hookName}; modes = { @@ -163,8 +162,12 @@ in { '' if [[ ! -L "${hook.output}" ]] || [[ "$(readlink "${hook.output}")" != "${hook.generatedDerivation}" ]]; then _soonix_log "info" "${hookName}" "Creating symlink: ${hook.output} -> ${hook.generatedDerivation}" - mkdir -p "$(dirname "${hook.output}")" - ln -sf "${hook.generatedDerivation}" "${hook.output}" + if [[ "$CHECK_MODE" != "true" ]]; then + mkdir -p "$(dirname "${hook.output}")" + ln -sf "${hook.generatedDerivation}" "${hook.output}" + else + _soonix_log "info" "${hookName}" "Would create symlink: ${hook.output} -> ${hook.generatedDerivation}" + fi _changed=true else _soonix_log "info" "${hookName}" "Symlink up to date: ${hook.output}" @@ -175,10 +178,14 @@ in { '' if [[ ! -f "${hook.output}" ]] || ! cmp -s "${hook.generatedDerivation}" "${hook.output}"; then _soonix_log "info" "${hookName}" "Copying file: ${hook.generatedDerivation} -> ${hook.output}" - mkdir -p "$(dirname "${hook.output}")" - # required since they're read only - rm -f "${hook.output}" - cp "${hook.generatedDerivation}" "${hook.output}" + if [[ "$CHECK_MODE" != "true" ]]; then + mkdir -p "$(dirname "${hook.output}")" + # required since they're read only + rm -f "${hook.output}" + cp "${hook.generatedDerivation}" "${hook.output}" + else + _soonix_log "info" "${hookName}" "Would copy file: ${hook.generatedDerivation} -> ${hook.output}" + fi _changed=true else _soonix_log "info" "${hookName}" "File up to date: ${hook.output}" @@ -192,46 +199,63 @@ in { _soonix_add_to_gitignore "${hook.output}" '' else ""; + + isGitignored = + if hook.hook.gitignore + then "true" + else "false"; in + builtins.addErrorContext "[soonix] while generating script for ${hookName}" # sh '' # Process hook: ${hookName} - while IFS= read -r line; do - case "$line" in - UPDATED) _soonix_updated+=("${hookName}") ;; - UPTODATE) _soonix_uptodate+=("${hookName}") ;; - *) echo "$line" ;; - esac - done < <( - set -euo pipefail - _changed=false + # Skip if SKIP_GITIGNORE is set and this hook is gitignored + if [[ "$SKIP_GITIGNORE" == "true" && "${isGitignored}" == "true" ]]; then + : # skip + else + while IFS= read -r line; do + case "$line" in + UPDATED) _soonix_updated+=("${hookName}") ;; + UPTODATE) _soonix_uptodate+=("${hookName}") ;; + *) echo "$line" ;; + esac + done < <( + set -euo pipefail + _changed=false - ${modes.${hook.hook.mode} or (throw "Mode ${hook.hook.mode} doesnt exist")} + ${modes.${hook.hook.mode} or (throw "Mode ${hook.hook.mode} doesnt exist")} - # Add to gitignore if requested - ${optionalGitignore} + if [[ "$CHECK_MODE" != "true" ]]; then + # Add to gitignore if requested + ${optionalGitignore} - # Run extra commands if file changed - if [[ "$_changed" == "true" && -n "${hook.hook.extra}" ]]; then - _soonix_log "info" "${hookName}" "Running extra command: ${hook.hook.extra}" - eval "${hook.hook.extra}" - fi + # Run extra commands if file changed + if [[ "$_changed" == "true" && -n "${hook.hook.extra}" ]]; then + _soonix_log "info" "${hookName}" "Running extra command: ${hook.hook.extra}" + eval "${hook.hook.extra}" + fi + fi - if [[ "$_changed" == "true" ]]; then - echo "UPDATED" - else - echo "UPTODATE" - fi - ) || { - _soonix_log "error" "${hookName}" "Failed to process hook" - _soonix_failed+=("${hookName}") - } + if [[ "$_changed" == "true" ]]; then + echo "UPDATED" + else + echo "UPTODATE" + fi + ) || { + _soonix_log "error" "${hookName}" "Failed to process hook" + _soonix_failed+=("${hookName}") + } + fi '') - hookNames; + (builtins.attrNames hooks); - generatedShellHook = + generateShellHook = + builtins.addErrorContext "[soonix] while generating shell hook" # sh '' + CHECK_MODE=''${CHECK_MODE:-false} + SKIP_GITIGNORE=''${SKIP_GITIGNORE:-false} + _soonix_log() { local level="$1" local hook="$2" @@ -272,6 +296,9 @@ in { if [[ ''${#_soonix_updated[@]} -gt 0 ]]; then output="$output [updated: ''${_soonix_updated[*]}]" >&2 + if [[ "$CHECK_MODE" == "true" ]]; then + status=2 + fi fi if [[ ''${#_soonix_uptodate[@]} -gt 0 ]]; then output="$output [unchanged: ''${_soonix_uptodate[*]}]" >&2 @@ -283,8 +310,8 @@ in { printf "%s\E[m\n" "$output" >&2 - if [[ $status -eq 1 ]]; then - exit 1 + if [[ $status -ne 0 ]]; then + exit $status fi ''; @@ -297,8 +324,8 @@ in { in rec { # nothing to do if no hooks exist shellHook = - if (builtins.length hookNames > 0) - then generatedShellHook + if (builtins.length (builtins.attrNames config.hooks) > 0) + then generateShellHook else ""; shellHookFile = pkgs.writeShellScript "shellHook" shellHook; devshellModule = { @@ -307,11 +334,102 @@ in { }; finalFiles = buildAllFiles allFiles; # make it simpler to update the hooks without any devshell - packages."soonix:update" = pkgs.writeShellScriptBin "soonix:update" '' - function _soonix() { - ${shellHook} - } - _soonix - ''; + packages = { + soonix = + pkgs.writeShellScriptBin "soonix" + # sh + '' + set -euo pipefail + + SKIP_GITIGNORE=false + CHECK_MODE=false + COMMAND="" + + show_help() { + cat << EOF + soonix - Declarative file management tool + + USAGE: + soonix [OPTIONS] + + COMMANDS: + update Update all managed files + check Check if all files are up to date (dry-run) + list List all managed file targets + help Show this help message + + OPTIONS: + --skip-gitignore Skip files that would be added to .gitignore + + EXAMPLES: + soonix update # Update all files + soonix update --skip-gitignore # Update only non-gitignored files + soonix list # List all targets + soonix list --skip-gitignore # List only non-gitignored targets + EOF + } + + while [[ $# -gt 0 ]]; do + case "$1" in + update|check|list|help) + COMMAND="$1" + shift + ;; + --skip-gitignore) + SKIP_GITIGNORE=true + shift + ;; + *) + echo "Error: Unknown argument '$1'" >&2 + echo "" + show_help + exit 1 + ;; + esac + done + + if [[ -z "$COMMAND" ]]; then + echo "Error: No command specified" >&2 + echo "" + show_help + exit 1 + fi + + case "$COMMAND" in + help) + show_help + ;; + list) + ${concatMapStringsSep "\n" (hookName: let + hook = hooks.${hookName}; + in '' + if [[ "$SKIP_GITIGNORE" == "true" && "${ + if hook.hook.gitignore + then "true" + else "false" + }" == "true" ]]; then + : # skip + else + echo "${hook.output}" + fi + '') (builtins.attrNames hooks)} + ;; + update) + function _soonix() { + ${generateShellHook} + } + _soonix + ;; + check) + CHECK_MODE=true + echo "Checking files..." + function _soonix() { + ${generateShellHook} + } + _soonix + ;; + esac + ''; + }; }; } diff --git a/nix/repo/devShells.nix b/nix/repo/devShells.nix index c473189..272f851 100644 --- a/nix/repo/devShells.nix +++ b/nix/repo/devShells.nix @@ -41,8 +41,10 @@ in { } { name = "soonix"; + files = "nix run .#soonix -- list --skip-gitignore"; stage_fixed = true; - run = "nix run .#soonix:update"; + # {files} is needed so lefthook git add's them + run = "nix run .#soonix -- update --skip-gitignore; #{files}"; } ]; }; diff --git a/nix/repo/soonix.nix b/nix/repo/soonix.nix index fa17fd8..8ca5f8c 100644 --- a/nix/repo/soonix.nix +++ b/nix/repo/soonix.nix @@ -14,7 +14,7 @@ in data = { extends = ["config:recommended"]; postUpgradeTasks.commands = [ - "nix-portable nix run .#soonix:update" + "nix-portable nix run .#soonix -- update --skip-gitignore" ]; lockFileMaintenance = { enabled = true; diff --git a/tests/soonix_test.nix b/tests/soonix_test.nix index e59498e..39b6bbf 100644 --- a/tests/soonix_test.nix +++ b/tests/soonix_test.nix @@ -69,6 +69,27 @@ in { assert_file_contains ${shellHook} "gomplate" ''; } + { + name = "packages"; + type = "script"; + script = let + conf = (soonix.make {inherit hooks;}).config; + soonixBin = conf.packages.soonix + "/bin/soonix"; + in + # sh + '' + ${ntlib.helpers.path [pkgs.gnugrep]} + ${ntlib.helpers.scriptHelpers} + + assert -f "${soonixBin}" "should exist" + + assert_file_contains "${soonixBin}" "gotmpl" + assert_file_contains "${soonixBin}" "test.json" + + assert_file_contains "${soonixBin}" "SKIP_GITIGNORE" + assert_file_contains "${soonixBin}" "CHECK_MODE" + ''; + } ]; }; }