lib.fileset.gitTracked/gitTrackedWith: init

A configuration parameter for gitTrackedWith will be introduced in the
next commit
This commit is contained in:
Silvan Mosberger 2023-11-03 01:24:55 +01:00
parent 91c993afb9
commit 2dfb1d36cf
4 changed files with 255 additions and 1 deletions

View File

@ -12,6 +12,7 @@ let
_printFileset
_intersection
_difference
_mirrorStorePath
;
inherit (builtins)
@ -596,4 +597,91 @@ in {
# We could also return the original fileset argument here,
# but that would then duplicate work for consumers of the fileset, because then they have to coerce it again
actualFileset;
/*
Create a file set containing all [Git-tracked files](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) in a repository.
This function behaves like [`gitTrackedWith { }`](#function-library-lib.fileset.gitTrackedWith) - using the defaults.
Type:
gitTracked :: Path -> FileSet
Example:
# Include all files tracked by the Git repository in the current directory
gitTracked ./.
# Include only files tracked by the Git repository in the parent directory
# that are also in the current directory
intersection ./. (gitTracked ../.)
*/
gitTracked =
/*
The [path](https://nixos.org/manual/nix/stable/language/values#type-path) to the working directory of a local Git repository.
This directory must contain a `.git` file or subdirectory.
*/
path:
# See the gitTrackedWith implementation for more explanatory comments
let
fetchResult = builtins.fetchGit path;
in
if ! isPath path then
throw "lib.fileset.gitTracked: Expected the argument to be a path, but it's a ${typeOf path} instead."
else if ! pathExists (path + "/.git") then
throw "lib.fileset.gitTracked: Expected the argument (${toString path}) to point to a local working tree of a Git repository, but it's not."
else
_mirrorStorePath path fetchResult.outPath;
/*
Create a file set containing all [Git-tracked files](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) in a repository.
The first argument allows configuration with an attribute set,
while the second argument is the path to the Git working tree.
If you don't need the configuration,
you can use [`gitTracked`](#function-library-lib.fileset.gitTracked) instead.
This is equivalent to the result of [`unions`](#function-library-lib.fileset.unions) on all files returned by [`git ls-files`](https://git-scm.com/docs/git-ls-files)
(which uses [`--cached`](https://git-scm.com/docs/git-ls-files#Documentation/git-ls-files.txt--c) by default).
:::{.warning}
Currently this function is based on [`builtins.fetchGit`](https://nixos.org/manual/nix/stable/language/builtins.html#builtins-fetchGit)
As such, this function causes all Git-tracked files to be unnecessarily added to the Nix store,
without being re-usable by [`toSource`](#function-library-lib.fileset.toSource).
This may change in the future.
:::
Type:
gitTrackedWith :: { } -> Path -> FileSet
Example:
# Include all files tracked by the Git repository in the current directory
gitTracked { } ./.
*/
gitTrackedWith =
{
}:
/*
The [path](https://nixos.org/manual/nix/stable/language/values#type-path) to the working directory of a local Git repository.
This directory must contain a `.git` file or subdirectory.
*/
path:
let
# This imports the files unnecessarily, which currently can't be avoided
# because `builtins.fetchGit` is the only function exposing which files are tracked by Git.
# With the [lazy trees PR](https://github.com/NixOS/nix/pull/6530),
# the unnecessarily import could be avoided.
# However a simpler alternative still would be [a builtins.gitLsFiles](https://github.com/NixOS/nix/issues/2944).
fetchResult = builtins.fetchGit {
url = path;
};
in
if ! isPath path then
throw "lib.fileset.gitTrackedWith: Expected the second argument to be a path, but it's a ${typeOf path} instead."
# We can identify local working directories by checking for .git,
# see https://git-scm.com/docs/gitrepository-layout#_description.
# Note that `builtins.fetchGit` _does_ work for bare repositories (where there's no `.git`),
# even though `git ls-files` wouldn't return any files in that case.
else if ! pathExists (path + "/.git") then
throw "lib.fileset.gitTrackedWith: Expected the second argument (${toString path}) to point to a local working tree of a Git repository, but it's not."
else
_mirrorStorePath path fetchResult.outPath;
}

View File

@ -825,4 +825,23 @@ rec {
${baseNameOf root} =
fromFile (baseNameOf root) rootType;
};
# Mirrors the contents of a Nix store path relative to a local path as a file set.
# Some notes:
# - The store path is read at evaluation time.
# - The store path must not include files that don't exist in the respective local path.
#
# Type: Path -> String -> FileSet
_mirrorStorePath = localPath: storePath:
let
recurse = focusedStorePath:
mapAttrs (name: type:
if type == "directory" then
recurse (focusedStorePath + "/${name}")
else
type
) (builtins.readDir focusedStorePath);
in
_create localPath
(recurse storePath);
}

View File

@ -95,8 +95,9 @@ expectEqual() {
# Usage: expectStorePath NIX
expectStorePath() {
local expr=$1
if ! result=$(nix-instantiate --eval --strict --json --read-write-mode --show-trace \
if ! result=$(nix-instantiate --eval --strict --json --read-write-mode --show-trace 2>"$tmp"/stderr \
--expr "$prefixExpression ($expr)"); then
cat "$tmp/stderr" >&2
die "$expr failed to evaluate, but it was expected to succeed"
fi
# This is safe because we assume to get back a store path in a string
@ -1251,6 +1252,150 @@ expectEqual 'trace (intersection ./a (fromSource (lib.cleanSourceWith {
}))) null' 'trace ./a/b null'
rm -rf -- *
## lib.fileset.gitTracked/gitTrackedWith
# The first/second argument has to be a path
expectFailure 'gitTracked null' 'lib.fileset.gitTracked: Expected the argument to be a path, but it'\''s a null instead.'
expectFailure 'gitTrackedWith {} null' 'lib.fileset.gitTrackedWith: Expected the second argument to be a path, but it'\''s a null instead.'
# The path has to contain a .git directory
expectFailure 'gitTracked ./.' 'lib.fileset.gitTracked: Expected the argument \('"$work"'\) to point to a local working tree of a Git repository, but it'\''s not.'
expectFailure 'gitTrackedWith {} ./.' 'lib.fileset.gitTrackedWith: Expected the second argument \('"$work"'\) to point to a local working tree of a Git repository, but it'\''s not.'
# Checks that `gitTrackedWith` contains the same files as `git ls-files`
# for the current working directory.
# If --recurse-submodules is passed, the flag is passed through to `git ls-files`
# and as `recurseSubmodules` to `gitTrackedWith`
checkGitTrackedWith() {
# All files listed by `git ls-files`
expectedFiles=()
while IFS= read -r -d $'\0' file; do
# If there are submodules but --recurse-submodules isn't passed,
# `git ls-files` lists them as empty directories,
# we need to filter that out since we only want to check/count files
if [[ -f "$file" ]]; then
expectedFiles+=("$file")
fi
done < <(git ls-files -z)
storePath=$(expectStorePath 'toSource { root = ./.; fileset = gitTrackedWith { } ./.; }')
# Check that each expected file is also in the store path with the same content
for expectedFile in "${expectedFiles[@]}"; do
if [[ ! -e "$storePath"/"$expectedFile" ]]; then
die "Expected file $expectedFile to exist in $storePath, but it doesn't.\nGit status:\n$(git status)\nStore path contents:\n$(find "$storePath")"
fi
if ! diff "$expectedFile" "$storePath"/"$expectedFile"; then
die "Expected file $expectedFile to have the same contents as in $storePath, but it doesn't.\nGit status:\n$(git status)\nStore path contents:\n$(find "$storePath")"
fi
done
# This is a cheap way to verify the inverse: That all files in the store path are also expected
# We just count the number of files in both and verify they're the same
actualFileCount=$(find "$storePath" -type f -printf . | wc -c)
if [[ "${#expectedFiles[@]}" != "$actualFileCount" ]]; then
die "Expected ${#expectedFiles[@]} files in $storePath, but got $actualFileCount.\nGit status:\n$(git status)\nStore path contents:\n$(find "$storePath")"
fi
}
# Runs checkGitTrackedWith, this will make more sense in the next commit
checkGitTracked() {
checkGitTrackedWith
}
createGitRepo() {
git init -q "$1"
# Only repo-local config
git -C "$1" config user.name "Nixpkgs"
git -C "$1" config user.email "nixpkgs@nixos.org"
# Get at least a HEAD commit, needed for older Nix versions
git -C "$1" commit -q --allow-empty -m "Empty commit"
}
# Go through all stages of Git files
# See https://www.git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository
# Empty repository
createGitRepo .
checkGitTracked
# Untracked file
echo a > a
checkGitTracked
# Staged file
git add a
checkGitTracked
# Committed file
git commit -q -m "Added a"
checkGitTracked
# Edited file
echo b > a
checkGitTracked
# Removed file
git rm -f -q a
checkGitTracked
rm -rf -- *
# gitignored file
createGitRepo .
echo a > .gitignore
touch a
git add -A
checkGitTracked
# Add it regardless (needs -f)
git add -f a
checkGitTracked
rm -rf -- *
# Directory
createGitRepo .
mkdir -p d1/d2/d3
touch d1/d2/d3/a
git add d1
checkGitTracked
rm -rf -- *
# Submodules
createGitRepo .
createGitRepo sub
# Untracked submodule
git -C sub commit -q --allow-empty -m "Empty commit"
checkGitTracked
# Tracked submodule
git submodule add ./sub sub >/dev/null
checkGitTracked
# Untracked file
echo a > sub/a
checkGitTracked
# Staged file
git -C sub add a
checkGitTracked
# Committed file
git -C sub commit -q -m "Add a"
checkGitTracked
# Changed file
echo b > sub/b
checkGitTracked
# Removed file
git -C sub rm -f -q a
checkGitTracked
rm -rf -- *
# TODO: Once we have combinators and a property testing library, derive property tests from https://en.wikipedia.org/wiki/Algebra_of_sets
echo >&2 tests ok

View File

@ -25,11 +25,13 @@ let
];
nativeBuildInputs = [
nix
pkgs.gitMinimal
] ++ lib.optional pkgs.stdenv.isLinux pkgs.inotify-tools;
strictDeps = true;
} ''
datadir="${nix}/share"
export TEST_ROOT=$(pwd)/test-tmp
export HOME=$(mktemp -d)
export NIX_BUILD_HOOK=
export NIX_CONF_DIR=$TEST_ROOT/etc
export NIX_LOCALSTATE_DIR=$TEST_ROOT/var