aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--flake.lock27
-rw-r--r--flake.nix15
-rw-r--r--hm-reposync.nix206
-rwxr-xr-xreposync-functions.sh83
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