diff options
| -rw-r--r-- | flake.lock | 27 | ||||
| -rw-r--r-- | flake.nix | 15 | ||||
| -rw-r--r-- | hm-reposync.nix | 206 | ||||
| -rwxr-xr-x | reposync-functions.sh | 83 |
4 files changed, 331 insertions, 0 deletions
diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..42374b1 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1766736597, + "narHash": "sha256-BASnpCLodmgiVn0M1MU2Pqyoz0aHwar/0qLkp7CjvSQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "f560ccec6b1116b22e6ed15f4c510997d99d5852", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2ed7a4b --- /dev/null +++ b/flake.nix @@ -0,0 +1,15 @@ +{ + description = "Sync imperative git repositories with home manager"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + }; + + outputs = { + self, + nixpkgs, + ... + }: { + hmModules.reposync = import ./hm-reposync.nix; + }; +} diff --git a/hm-reposync.nix b/hm-reposync.nix new file mode 100644 index 0000000..3a4bf50 --- /dev/null +++ b/hm-reposync.nix @@ -0,0 +1,206 @@ +{ + config, + lib, + pkgs, + ... +}: let + cfg = config.reposync; + stowType = lib.types.submodule ( + {name, ...}: { + options = { + enable = lib.mkEnableOption "call stow for this target stow command"; + targetPrefix = lib.mkOption { + type = lib.types.path; + default = config.home.homeDirectory; + description = "absolute prefix for the stow target"; + }; + target = lib.mkOption { + type = lib.types.str; + default = "."; + description = "path to stow target relative to targetPrefix (home directory by default)"; + }; + packages = lib.mkOption { + type = lib.types.str; + default = name; + defaultText = "<name>"; + example = "vim zsh"; + description = "packages to stow from repository. use '.' to automatically stow all"; + }; + }; + config = { + enable = lib.mkDefault true; + }; + } + ); + outOfStoreGitRepositoryType = lib.types.submodule ( + { + name, + config, + ... + }: { + options = { + enable = lib.mkEnableOption "whether to download this repository"; + server = lib.mkOption { + type = lib.types.str; + default = "https://github.com/"; + example = "git@github.com:"; + description = "git server suffixed with the repo delimiter (e.g. '/' or ':')"; + }; + repository = lib.mkOption { + type = lib.types.str; + default = name; + defaultText = "<name>"; + example = "nix-community/home-manager"; + description = "git repository url"; + }; + # TODO test to add this remote feature + #remotes = lib.mkOption { + # type = lib.types.attrsOf lib.types.str; + # default = {}; + # example = { + # fork = "https://github.com/tjkeller-xyz/home-manager.git"; + # }; + # description = "alternative remotes to the git repository"; + #}; + #defaultRemote = lib.mkOption { + # type = lib.types.nullOr lib.types.str; + # default = null; + # example = "fork"; + # description = "default remote to use"; + #}; + #branch = lib.mkOption { + # type = lib.types.nullOr lib.types.str; + # default = null; + # example = "master"; + # description = "pull from this branch. set to null for default branch"; + #}; + targetPrefix = lib.mkOption { + type = lib.types.path; + default = config.home.homeDirectory; + description = "absolute prefix for the git repository path"; + }; + target = lib.mkOption { + type = lib.types.str; + default = name; + defaultText = "<name>"; + description = "path to git repository relative to targetPrefix (home directory by default)"; + }; + stow = lib.mkOption { + type = lib.types.attrsOf stowType; + default = {}; + description = "stow packages from the repository"; + }; + extraCloneOptions = lib.mkOption { + type = lib.types.str; + default = ""; + example = "--recurse-submodules"; + description = "extra command flags to add to git clone"; + }; + extraPullOptions = lib.mkOption { + type = lib.types.str; + default = ""; + example = "--recurse-submodules"; + description = "extra command flags to add to git pull"; + }; + extraCommands = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + extra commands to run each time repo is synced. + + IMPORANT: all commands should be idempotent to prevent breaking + after repeated syncs. + like `home.activation` scripts, extra commands should respect the + DRY_RUN environment variable and use $VERBOSE_ARG in case the + verbose flag is set. + commands can be prefixed with `run` to automatically respect the + DRY_RUN function. + ''; + }; + generatedCalls = lib.mkOption { + type = lib.types.listOf lib.types.str; + readOnly = true; + description = "generated calls for reposync.sh script"; + }; + }; + config = let + # generate calls + callClone = url: target: flags: ''clonemissing "${url}" "${target}" "${flags}"''; + callPull = target: flags: ''pull "${target}" "${flags}"''; + callRemoteAdd = target: remote: url: ''remoteadd "${target}" "${remote}" "${url}"''; + callStowRepo = dir: target: packages: ''stowrepo "${dir}" "${target}" "${packages}"''; + # misc + stowToCalls = s: callStowRepo target (combineTarget s.targetPrefix s.target) s.packages; + combineTarget = prefix: target: prefix + "/" + target; + target = combineTarget config.targetPrefix config.target; + url = config.server + config.repository; + in { + enable = lib.mkDefault true; + generatedCalls = lib.lists.flatten [ + (callClone url target config.extraCloneOptions) + (callPull target config.extraPullOptions) + #(lib.mapAttrsToList (name: url: callRemoteAdd name url) config.remotes) + (lib.mapAttrsToList (name: s: lib.mkIf s.enable (stowToCalls s)) config.stow) + config.extraCommands + ]; + }; + } + ); +in { + options.reposync.outOfStoreGitRepository = lib.mkOption { + type = lib.types.attrsOf outOfStoreGitRepositoryType; + default = {}; + description = "imperative git repositories to be cloned"; + }; + + config = let + repocfg = cfg.outOfStoreGitRepository; + lines = lineslist: lib.strings.concatStringsSep "\n" lineslist; + fname = name: ''_sync-${name}''; + wrapRepoCalls = name: r: '' + function ${fname name}() { + echo "Syncing repository '${name}'" + ${lines r.generatedCalls} + echo "Done" + } + ''; + allRepoCallFuncs = lines (lib.mapAttrsToList (name: r: wrapRepoCalls name r) repocfg); + syncAllFunc = '' + function all() { + ${lines (lib.mapAttrsToList (name: r: fname name) repocfg)} + } + ''; + argCases = '' + ${lines (lib.mapAttrsToList (name: r: "${name}) ${fname name} ;;") repocfg)} + -a|--all) all ;; + ''; + src = pkgs.writeShellScriptBin "reposync" '' + export PATH="${pkgs.git}/bin:$PATH" + export PATH="${pkgs.stow}/bin:$PATH" + ${builtins.readFile ./reposync-functions.sh} + ${allRepoCallFuncs} + ${syncAllFunc} + for arg in "$@"; do + case "$arg" in + ${argCases} + esac + done + ''; + hm-reposync = pkgs.stdenv.mkDerivation rec { + name = "hm-reposync"; + + inherit src; + dontUnpack = true; + + buildInputs = with pkgs; [git stow]; + + installPhase = '' + mkdir -p $out/bin + cp $src/bin/reposync $out/bin + chmod +x $out/bin/reposync + ''; + }; + in { + home.packages = [hm-reposync]; + }; +} diff --git a/reposync-functions.sh b/reposync-functions.sh new file mode 100755 index 0000000..efb4e9b --- /dev/null +++ b/reposync-functions.sh @@ -0,0 +1,83 @@ +set -e + +# handle base args +for arg in "$@"; do + case "$arg" in + -v|--verbose) VERBOSE=1 ;; + -d|--dry-run) DRY_RUN=1 ;; + esac +done + +[ -n "$VERBOSE" ] && VERBOSE_ARG=--verbose +run() { [ -n "$DRY_RUN" ] && echo "DRY RUN:" $@ || $@; } + +# define base functions +clonemissing() { + url="$1" + target="$2" + flags="$3" + #branch="$3" + + # return if already existing + if [ -d "${target}/.git" ]; then + return + fi + + # clone url to target + echo "cloning $url to $target" + run mkdir -p $VERBOSE_ARG "$target" + run git clone $VERBOSE_ARG $flags "$url" "$target" +} + +pull() { + target="$1" + flags="$2" + + cd "$target" + + # check if local changes exist before running pull + if ! (git diff --quiet && git diff --cached --quiet); then + echo "$target: local changes exist. won't attempt to pull" + return + fi + + # pull changes + echo "$target: pulling latest changes" + run git pull $VERBOSE_ARG $flags +} + +remoteadd() { + target="$1" + remote="$2" + url="$3" + + cd "$target" + + # check if remote exists + remoteurl="$(git $VERBOSE_ARG remote get-url "$remote" 2>/dev/null)" + if [ $? = 0 ]; then + # verify remote url + if [ "$remoteurl" = "$url" ]; then + return + fi + echo "$target: the url of remote $remote ($remoteurl) does not match the expected value ($url)" + return + fi + + # add remote + echo "$target: adding remote $remote ($remoteurl)" + run git remote $VERBOSE_ARG add "$remote" "$url" +} + +stowrepo() { + dir="$1" + target="$2" + packages="$3" + + cd "$dir" + + echo "$target: stowing packages ($packages) to $target" + run stow $VERBOSE_ARG --restow --target="$target" $packages +} + +# define repo specific sync funcs |
