Force clean working directory before building NixOS

Hi,

tl;dr: I’d like nixos-rebuild to fail if my git repo has uncommitted changes.

I’d like to force myself to commit changes I do to my configuration.nix. I regularly do stuff in my config, test if it works, and then “forget” to commit it. Then, when I come back to change other stuff, I don’t know anymore what I did before or maybe why I did that, and so I “will look at it later” and first try out, what I have currently in mind. It’s a death spiral…

What I’d like to achieve is that nixos-rebuild fails to build while I have uncommitted changes. This would force me to do a commit describing what I’m about to do. When it does not work, I’d have to change and amend my commit before I rebuild.

Not only would that ensure that all changes are versioned correctly but would also make it possible for me to go back to one of the earlier tries (I know how to recover detached commits and I could also work on branches and squash them when I’m done).

Is it possible to check from the configuration.nix if the working directory is clean and fail if it isn’t?

To check from bash, one can do

if [ -z "$(git status --porcelain)" ]; then
    echo "Clean"
else
    echo "Not clean"
fi

or, if untracked files should not be considered:

if $(git diff-index --quiet HEAD); then
    echo "Clean"
else
    echo "Not clean"
fi

But how would I do this check when building my configuration?

If you use flakes, nix itself has a setting named allow-dirty (defaults to true) for this.

Also a flakes-related answer, but in a flake, you can check self.rev to find out if you’re building from a dirty worktree.

@ilkecan @tejing Thanks for your answers :heart:

However, I haven’t converted to flakes yet. Also, if I understand it right, a dirty working directory is allowed per default and I had to explicitly pass --allow-dirty=false (or similar) to make it fail. This is obviously not what I want because it is extra effort just to force me to do the right thing.

The self.rev might be what I was looking for but as I said, I’m not on flakes yet.

I came up with a solution that works with or without flakes. It is on the on hand somewhat hacky, but on the other hand I find it quite elegant :sweat_smile:

{ config, pkgs,... }:

let
  nixos-rebuild-wrapper =
    pkgs.writeShellScriptBin "nixos-rebuild" ''
      orig_PWD="$PWD"
      check=false
      if [ $1 == '--force-plz-i-fcked-up' ]; then
          shift
      else
          for arg in "$@"; do
              # Allow only arguments starting with a minus (to support e.g. `--help`) and `dry-*` commands
              if [[ ! "$arg" =~ ^- ]] && [[ ! "$arg" =~ ^dry- ]]; then
                  check=true
              fi
          done
      fi
      if [[ $check == true ]]; then
          nixosConfig="$(echo $NIX_PATH | tr : $'\n' | awk '/^nixos-config=/ { st = index($0, "="); print substr($0, st+1) }')"
          configDir="$(dirname "$nixosConfig")"
          cd "$configDir"
          if ! $(git rev-parse --is-inside-work-tree >/dev/null 2>&1); then
              # Warn if config is not in a git repository
              echo >&2 -ne "\n $(tput bold; tput setab 226; tput setaf 0)  WARNING  $(tput sgr0) "
              echo >&2 -e "No git repository found in \"''${configDir}\".\n"
          elif [ -n "$(git status --porcelain)" ]; then
              # Fail when dirty
              echo >&2 -ne "\n $(tput bold; tput setab 124; tput setaf 255)  ERROR  $(tput sgr0) "
              echo >&2 -e "Uncommitted changes in \"''${configDir}\". Please commit them first.\n"
              exit 1
          fi
      fi
      cd "$orig_PWD"
      ${pkgs.nixos-rebuild}/bin/nixos-rebuild "$@"
    '';
in {
  # ...

  environment.systemPackages = with pkgs; [
    nixos-rebuild-wrapper

    # ...
  ];

  # ...
}

What I do here is to create a new derivation with a script that replaces and wraps the nixos-rebuild executable, and add it to the system packages.

The script checks if the repository is dirty and if so, it exits with an error message. If the config is not inside a git repository, a warning will be displayed and the build continues (a loophole!).

To find the git repository, the script gets the location of the configuration.nix from the $NIX_PATH environment variable and assumes that it is located inside the repository (must not be in the top level folder).

To make sure nixos-rebuild --help still works, I do the dirty check only if the script was called with any arguments that don’t start with a minus. Doing so, I make sure that the check only applies when using one of the build commands. I decided to also allow any dry- commands.

I included a flag --force-plz-i-fcked-up that has to be passed as first parameter. It enables me to build my system even when the working directory is dirty. Just in case. I might remove this flag if I find myself abusing it. I could still rename the .git directory to disable the check. But the inhibition threshold to doing this is (at least for me) much higher.

Sounds like it works sufficiently well for your purposes, though I wanted to clarify one thing:

You could set allow-dirty = false permanently in nix.conf, so it’s no extra effort either. The main difference between our 2 approaches is whether it applies to all flakes or just one specific one.

1 Like

That’s good to know, thanks! :smiley_cat: