chore: initial commit

This commit is contained in:
technofab 2025-09-17 11:59:53 +02:00
commit db488d8f41
No known key found for this signature in database
26 changed files with 1077 additions and 0 deletions

3
lib/default.nix Normal file
View file

@ -0,0 +1,3 @@
_: {
devshellModule = ./modules;
}

5
lib/flake.nix Normal file
View file

@ -0,0 +1,5 @@
{
outputs = _i: {
devshellModule = ./modules;
};
}

59
lib/modules/cocogitto.nix Normal file
View file

@ -0,0 +1,59 @@
{
lib,
pkgs,
config,
...
}: let
inherit (lib) mkEnableOption mkOption mkIf types;
cfg = config.cocogitto;
# we need a newer version than in nixpkgs, since the PR which adds `--config`
# didn't land in a release yet
cocogitto = pkgs.cocogitto.overrideAttrs (_prev: {
version = "2025-09-11";
src = pkgs.fetchFromGitHub {
owner = "oknozor";
repo = "cocogitto";
rev = "031cc238cb3e3e8aa3a525c1df089c3e70020efc";
hash = "sha256-fyhugacBLJPMqHWxoxBTFhIE3wHDB9xdrqJYzJc36I0=";
};
});
configFile = (pkgs.formats.toml {}).generate "cog.toml" cfg.config;
cogAlias = pkgs.writeTextFile {
name = "cog-alias";
destination = "/bin/${cfg.alias}";
executable = true;
text =
# sh
''
${cocogitto}/bin/cog --config "${configFile}" ''${@:1}
'';
};
in {
options.cocogitto = {
enable =
mkEnableOption "Cocogitto"
// {
default = cfg.config != {};
};
alias = mkOption {
type = types.str;
default = "cog";
description = ''
Alias for `cog`.
'';
};
config = mkOption {
type = types.attrs;
default = {};
description = ''
Configure cocogitto here.
'';
};
};
config = mkIf cfg.enable {
packages = [cogAlias];
};
}

View file

@ -0,0 +1,28 @@
{
ntlib,
devshell,
...
}: let
module = ./cocogitto.nix;
in {
suites."Cocogitto" = {
pos = __curPos;
tests = [
{
name = "basic";
type = "script";
script = let
shell = devshell.mkShell {
imports = [module];
cocogitto.enable = true;
};
in
# sh
''
${ntlib.helpers.scriptHelpers}
assert "-f ${shell}/bin/cog" "/bin/cog should exist"
'';
}
];
};
}

7
lib/modules/default.nix Normal file
View file

@ -0,0 +1,7 @@
{
imports = [
./lefthook.nix
./taskfile.nix
./cocogitto.nix
];
}

152
lib/modules/lefthook.nix Normal file
View file

@ -0,0 +1,152 @@
{
lib,
pkgs,
config,
...
}: let
inherit (lib) concatStringsSep concatMapStringsSep subtractLists mkEnableOption mkOption types mkIf;
cfg = config.lefthook;
allHookNames = [
"applypatch-msg"
"pre-applypatch"
"post-applypatch"
"pre-commit"
"prepare-commit-msg"
"commit-msg"
"post-commit"
"pre-rebase"
"post-rewrite"
"post-checkout"
"post-merge"
"pre-push"
"pre-auto-gc"
"post-update"
"sendemail-validate"
"fsmonitor-watchman"
"p4-changelist"
"p4-prepare-changelist"
"p4-post-changelist"
"p4-pre-submit"
"post-index-change"
"pre-receive"
"update"
"proc-receive"
"reference-transaction"
"push-to-checkout"
"pre-merge-commit"
];
currentHookNames = builtins.filter (h: builtins.elem h allHookNames) (builtins.attrNames cfg.config);
unusedNixHookNames = subtractLists allHookNames currentHookNames;
unusedNixHookNamesStr = concatStringsSep " " unusedNixHookNames;
hookContent = hookName:
pkgs.writeShellScript hookName ''
if [ "$LEFTHOOK" = "0" ]; then
exit 0
fi
${lefthookAlias}/bin/${cfg.alias} run "${hookName}" "$@"
'';
lefthookConfig = (pkgs.formats.yaml {}).generate "lefthook.yaml" cfg.config;
lefthookAlias = pkgs.writeShellScriptBin cfg.alias ''
if [ "$1" = "install" ]; then
echo "Warning, using 'lefthook install' should not be used, use the shellHook instead"
fi
if [ "$1" = "install" ]; then
echo "Warning, using 'lefthook install' should not be used, use the shellHook instead"
fi
if [ "$1" = "install" ]; then
echo "Warning, using 'lefthook install' should not be used, use the shellHook instead"
fi
LEFTHOOK_CONFIG="${lefthookConfig}" ${pkgs.lefthook}/bin/lefthook ''${@:1}
'';
in {
options.lefthook = {
enable =
mkEnableOption "Lefthook"
// {
default = cfg.config != {};
};
shellHook = mkOption {
type = types.bool;
default = true;
description = ''
Whether to add a shell hook which automatically installs the git hooks.
'';
};
alias = mkOption {
type = types.str;
default = "lefthook";
example = "hooks";
description = ''
Alias for the lefthook command.
'';
};
config = mkOption {
type = types.attrs;
default = {};
description = ''
Config for lefthook. See https://lefthook.dev/configuration/.
'';
example = {
pre-commit = {
parallel = true;
jobs = [
{
name = "hello";
run = "echo world";
}
];
};
};
};
outputs = {
shellHook = mkOption {
type = types.str;
readOnly = true;
description = ''
The script to run on shell activation.
It automatically installs the git hooks and removes unused/previously used ones.
'';
};
};
};
config = {
lefthook.outputs.shellHook =
# sh
''
${builtins.readFile ./lefthook_helpers.sh}
# ensure git is available and we are in a Git repository
if ! command -v git &> /dev/null; then
__log ERROR "Git command not found. Cannot manage Git hooks." >&2
return 1
fi
local GIT_REAL_DIR
GIT_REAL_DIR=$(git rev-parse --git-dir 2>/dev/null)
if [ $? -ne 0 ]; then
__log INFO "Not inside a Git repository. Skipping Git hook setup." >&2
return 0
fi
# Use realpath to handle .git file for worktrees, and resolve relative path
GIT_REAL_DIR=$(realpath "$GIT_REAL_DIR")
# clean up unused hooks
cleanup_git_hooks "${unusedNixHookNamesStr}"
${concatMapStringsSep "\n" (hook: ''
setup_git_hook "${hook}" "${hookContent hook}"
'')
currentHookNames}
'';
packages = mkIf cfg.enable [lefthookAlias];
enterShellCommands."lefthook" = mkIf (cfg.enable && cfg.shellHook) {
text = cfg.outputs.shellHook;
deps = ["env"];
};
};
}

View file

@ -0,0 +1,149 @@
# Usage: __log <LEVEL> "message"
# LEVEL can be: TRACE, INFO, WARN, ERROR
__log() {
local level="$1" msg="$2"
local format=$'\E[mlefthook: \E[38;5;8m%s\E[m\n'
case "$level" in
TRACE)
if [[ -n "${LEFTHOOK_VERBOSE:-}" ]]; then
# shellcheck disable=SC2059
printf "$format" "[TRACE] ${msg}" >&2
fi
;;
INFO)
if [[ -n "${LEFTHOOK_VERBOSE:-}" ]]; then
# shellcheck disable=SC2059
printf "$format" "$msg" >&2
fi
;;
WARN)
# shellcheck disable=SC2059
printf "$format" "[WARN] ${msg}" >&2
;;
ERROR)
# shellcheck disable=SC2059
printf "$format" "[ERROR] ${msg}" >&2
;;
esac
}
# Helper to get the current Git hooks directory, supporting worktrees.
# Returns the absolute path to the hooks directory.
_get_git_hooks_dir() {
# Check if we are inside a Git repository
if ! command -v git &> /dev/null; then
__log ERROR "Git command not found. Cannot resolve Git hooks directory."
return 1
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
__log ERROR "Not inside a Git repository. Cannot resolve Git hooks directory."
return 1
fi
local GIT_DIR_PATH
GIT_DIR_PATH=$(git rev-parse --git-dir)
GIT_DIR_PATH=$(realpath "$GIT_DIR_PATH") # Resolve to absolute path
echo "${GIT_DIR_PATH}/hooks"
return 0
}
# Function to set up a Git hook as a symlink to a Nix store path.
# Usage: setup_git_hook <hook_name> <nix_store_hook_path>
setup_git_hook() {
local HOOK_NAME="$1"
local NIX_HOOK_PATH="$2"
local NIX_STORE_PATTERN="/nix/store/" # Pattern to identify Nix store paths
local HOOK_DIR
HOOK_DIR=$(_get_git_hooks_dir)
if [ $? -ne 0 ]; then
return 1 # Error from _get_git_hooks_dir
fi
local HOOK_FILE="${HOOK_DIR}/${HOOK_NAME}"
local BACKUP_FILE="${HOOK_FILE}.old"
# Ensure the hooks directory exists
mkdir -p "$HOOK_DIR"
if [ -e "$HOOK_FILE" ]; then # Check if anything exists at the hook path
if [ -L "$HOOK_FILE" ]; then # Existing hook is a symlink
local CURRENT_TARGET
CURRENT_TARGET=$(readlink "$HOOK_FILE")
if [[ "$CURRENT_TARGET" == "$NIX_HOOK_PATH" ]]; then
__log TRACE "Hook '$HOOK_NAME' already exists and points to the correct Nix store path. Doing nothing."
return 0
elif [[ "$CURRENT_TARGET" == $NIX_STORE_PATTERN* ]]; then
__log TRACE "Hook '$HOOK_NAME' is a symlink to a different Nix store path. Replacing it with the new symlink."
rm "$HOOK_FILE"
else
__log INFO "Hook '$HOOK_NAME' is a symlink but not to a Nix store path. Backing up to '$BACKUP_FILE'."
mv "$HOOK_FILE" "$BACKUP_FILE"
fi
elif [ -f "$HOOK_FILE" ]; then # Existing hook is a regular file
# Assumption: any non-symlink hook file is not ours
__log INFO "Hook '$HOOK_NAME' is an existing regular file (not managed by us). Backing up to '$BACKUP_FILE'."
mv "$HOOK_FILE" "$BACKUP_FILE"
else # Existing hook is neither a symlink nor a regular file (e.g., directory)
__log WARN "Hook '$HOOK_NAME' exists but is not a regular file or symlink. Please check your hooks and remove the hook manually just in case."
exit 1
fi
fi
__log INFO "Creating symlink for '$HOOK_NAME' to '$NIX_HOOK_PATH'."
ln -s "$NIX_HOOK_PATH" "$HOOK_FILE"
}
# Function to clean up specific Git hooks that are no longer used by Nix.
# It removes symlinks that point to the Nix store for the given hook names
# and restores any .old backups if they exist.
# Usage: cleanup_git_hooks <space-separated-list-of-unused-hook-names>
cleanup_git_hooks() {
local UNUSED_HOOK_NAMES_STR="$1"
local NIX_STORE_PATTERN="/nix/store/"
if [ -z "$UNUSED_HOOK_NAMES_STR" ]; then
__log TRACE "No unused hooks specified for cleanup. Doing nothing."
return 0
fi
local HOOK_DIR
HOOK_DIR=$(_get_git_hooks_dir)
if [ $? -ne 0 ]; then
return 1 # Error from _get_git_hooks_dir
fi
if [ ! -d "$HOOK_DIR" ]; then
__log TRACE "No hooks directory found at '$HOOK_DIR'. Nothing to clean up."
return 0
fi
__log TRACE "Cleaning up unused Git hooks in '$HOOK_DIR'..."
for HOOK_NAME in $UNUSED_HOOK_NAMES_STR; do
local HOOK_FILE_PATH="${HOOK_DIR}/${HOOK_NAME}"
if [ -L "$HOOK_FILE_PATH" ]; then # Only consider symlinks
local TARGET
TARGET=$(readlink "$HOOK_FILE_PATH")
# Check if the symlink points to a Nix store path
if [[ "$TARGET" == $NIX_STORE_PATTERN* ]]; then
__log INFO "Removing unused Nix-managed hook: '$HOOK_NAME' (points to '$TARGET')"
rm "$HOOK_FILE_PATH"
# Check for a corresponding .old file and restore it if it exists
local OLD_HOOK="${HOOK_FILE_PATH}.old"
if [ -e "$OLD_HOOK" ]; then
__log INFO "Restoring backup: '$OLD_HOOK' to '${HOOK_FILE_PATH}'"
mv "$OLD_HOOK" "$HOOK_FILE_PATH"
fi
else
__log TRACE "Hook '$HOOK_NAME' is a symlink but not to a Nix store path. Skipping cleanup (it's not our managed symlink)."
fi
elif [ -e "$HOOK_FILE_PATH" ]; then
__log TRACE "Hook '$HOOK_NAME' exists but is not a symlink (not managed by us). Skipping cleanup."
else
__log TRACE "Hook '$HOOK_NAME' does not exist. Skipping cleanup."
fi
done
}

View file

@ -0,0 +1,46 @@
{
ntlib,
devshell,
...
}: let
module = ./lefthook.nix;
in {
suites."Lefthook" = {
pos = __curPos;
tests = [
{
name = "basic";
type = "script";
script = let
shell = devshell.mkShell {
imports = [module];
lefthook.enable = true;
};
in
# sh
''
${ntlib.helpers.scriptHelpers}
assert "-f ${shell}/bin/lefthook" "/bin/lefthook should exist"
'';
}
{
name = "alias";
type = "script";
script = let
shell = devshell.mkShell {
imports = [module];
lefthook = {
enable = true;
alias = "hooks";
};
};
in
# sh
''
${ntlib.helpers.scriptHelpers}
assert "-f ${shell}/bin/hooks" "/bin/hooks should exist"
'';
}
];
};
}

102
lib/modules/taskfile.nix Normal file
View file

@ -0,0 +1,102 @@
{
lib,
pkgs,
config,
...
}: let
inherit (lib) mapAttrs mkEnableOption mkOption mkIf types;
cfg = config.task;
patchedTasks = mapAttrs (_name: value: let
taskDir = value.dir or "";
absolutePathOrTemplate = (builtins.substring 0 1 taskDir) == "/" || (builtins.substring 0 1 taskDir) == "{";
in
value
// {
dir =
if absolutePathOrTemplate
then taskDir
else ''{{env "TASK_ROOT_DIR" | default .USER_WORKING_DIR}}/${taskDir}'';
})
cfg.tasks;
generator = name: value:
pkgs.writeTextFile {
inherit name;
text = builtins.toJSON value;
};
# NOTE: this requires python just to convert json to yaml, since json is valid yaml we just ignore that
# generator = (pkgs.formats.yaml {}).generate;
taskfile = generator "taskfile" {
version = 3;
inherit (cfg) interval;
tasks = patchedTasks;
};
# when using a , as alias for example the store path looks weird.
# This way it can be identified as being the task alias
taskAlias = pkgs.writeTextFile {
name = "task-alias";
destination = "/bin/${cfg.alias}";
executable = true;
text = let
taskfileScript =
if cfg.lazy
then
# sh
"$(nix build '${builtins.unsafeDiscardOutputDependency taskfile.drvPath}^*' --no-link --print-out-paths)"
else taskfile;
in
# sh
''
TASKFILE="${taskfileScript}"
STATE_DIR="''${REN_STATE:-''${DEVENV_STATE:-''${PRJ_CACHE_HOME}}}"
ROOT_DIR="''${REN_ROOT:-''${DEVENV_ROOT:-''${PRJ_ROOT}}}"
TASK_TEMP_DIR="''${STATE_DIR}/.task" \
TASK_ROOT_DIR="$ROOT_DIR" \
${pkgs.go-task}/bin/task --taskfile "$TASKFILE" ''${@:1}
'';
};
in {
options.task = {
enable =
mkEnableOption "Task"
// {
default = cfg.tasks != {};
};
lazy = mkOption {
type = types.bool;
default = true;
description = ''
Whether the taskfile should be built on-demand/lazily.
It will probably not land in the gcroot and thus might get cleaned up with every gc.
On the other hand, this way loading the devshell is faster. Decide for yourself :)
'';
};
alias = mkOption {
type = types.str;
default = "task";
description = ''
Alias for `task`, eg. set to `,` to be able to run `, --list-all`.
'';
};
interval = mkOption {
type = types.str;
default = "5000ms";
description = ''
Interval for `task` to check for filesystem changes/watcher updates.
'';
};
tasks = mkOption {
type = types.attrs;
default = {};
description = ''
Configure all your tasks here.
'';
};
};
config = mkIf cfg.enable {
packages = [taskAlias];
};
}

View file

@ -0,0 +1,46 @@
{
ntlib,
devshell,
...
}: let
module = ./taskfile.nix;
in {
suites."Taskfile" = {
pos = __curPos;
tests = [
{
name = "basic";
type = "script";
script = let
shell = devshell.mkShell {
imports = [module];
task.enable = true;
};
in
# sh
''
${ntlib.helpers.scriptHelpers}
assert "-f ${shell}/bin/task" "/bin/task should exist"
'';
}
{
name = "alias";
type = "script";
script = let
shell = devshell.mkShell {
imports = [module];
task = {
enable = true;
alias = ",";
};
};
in
# sh
''
${ntlib.helpers.scriptHelpers}
assert "-f ${shell}/bin/," "/bin/, should exist"
'';
}
];
};
}